diff --git a/alfort_dom/dom.py b/alfort_dom/dom.py
index abf10e4..2b0b395 100644
--- a/alfort_dom/dom.py
+++ b/alfort_dom/dom.py
@@ -26,7 +26,7 @@ def dom_effect(
) -> Callable[[Callable[[HTMLElement, Dispatch[M]], None]], Effect[M]]:
def _wrapper(fun: Callable[[HTMLElement, Dispatch[M]], None]) -> Effect[M]:
@functools.wraps(fun)
- def _wrapped(dispatch: Dispatch[M]) -> None:
+ async def _wrapped(dispatch: Dispatch[M]) -> None:
def _f(_: Any) -> None:
dom = document.getElementById(dom_id)
fun(dom, dispatch)
diff --git a/docs/examples/index.html b/docs/examples/index.html
index 1469ce2..51b1d32 100755
--- a/docs/examples/index.html
+++ b/docs/examples/index.html
@@ -8,6 +8,7 @@
diff --git a/docs/examples/simple_api/index.html b/docs/examples/simple_api/index.html
new file mode 100755
index 0000000..a0fb0f9
--- /dev/null
+++ b/docs/examples/simple_api/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+
Alfort-Dom • Simple Counter
+
+
+
+
+
+
+
diff --git a/docs/examples/simple_api/main.py b/docs/examples/simple_api/main.py
new file mode 100755
index 0000000..fd87452
--- /dev/null
+++ b/docs/examples/simple_api/main.py
@@ -0,0 +1,157 @@
+from dataclasses import dataclass
+from typing import Any, Callable, Coroutine, TypeAlias
+from urllib.parse import ParseResult as URL
+from urllib.parse import urlparse
+
+import pyodide.http
+from alfort import Dispatch, Effect
+from alfort.vdom import VDom, el
+
+from alfort_dom import AlfortDom
+
+
+@dataclass(frozen=True)
+class Photo:
+ album_id: int
+ id: int
+ title: str
+ url: URL
+ thumbnail_url: URL
+
+
+@dataclass(frozen=True)
+class State:
+ album_id: int
+ is_fetching: bool
+ photos: list[Photo]
+
+
+@dataclass(frozen=True)
+class SelectAlbum:
+ alumb_id: int
+
+
+@dataclass(frozen=True)
+class ReceivePhotos:
+ photos: list[Photo]
+
+
+Msg: TypeAlias = SelectAlbum | ReceivePhotos
+
+
+async def fetch_photos(album_id: int) -> list[Photo]:
+ url = f"https://jsonplaceholder.typicode.com/albums/{album_id}/photos"
+ res = await pyodide.http.pyfetch(url)
+ return [
+ Photo(
+ album_id=obj["albumId"],
+ id=obj["id"],
+ title=obj["title"],
+ url=urlparse(obj["url"]),
+ thumbnail_url=urlparse(obj["thumbnailUrl"]),
+ )
+ for obj in await res.json()
+ ]
+
+
+def title(text: str) -> VDom:
+ return el("h1", {}, [text])
+
+
+def album_selector(album_id: int) -> VDom:
+ def _on_change(e: Any) -> Msg:
+ return SelectAlbum(e.target.value)
+
+ return el(
+ "div",
+ {"style": {"margin": "15px"}},
+ [
+ el("label", {"style": {"margin-right": "8px"}}, ["Album ID"]),
+ el(
+ "select",
+ {
+ "onchange": _on_change,
+ },
+ [
+ el(
+ "option",
+ {
+ "value": i,
+ "selected": album_id == i,
+ },
+ [str(i)],
+ )
+ for i in range(15)
+ ],
+ ),
+ ],
+ )
+
+
+def album_photos(photos: list[Photo]) -> VDom:
+ photo_elms: list[VDom] = []
+ for photo in photos:
+ photo_elms.append(
+ el(
+ "img",
+ {"src": photo.thumbnail_url.geturl(), "style": {"margin": "3px"}},
+ [],
+ )
+ )
+
+ return el("div", {"style": {"width": "75%", "line-height": "0px"}}, photo_elms)
+
+
+def fetching_dialog() -> VDom:
+ return el("div", {}, ["Loading..."])
+
+
+def view(state: State) -> VDom:
+ return el(
+ "div",
+ {
+ "style": {
+ "display": "flex",
+ "justify-content": "center",
+ "align-items": "center",
+ "flex-flow": "column",
+ }
+ },
+ [
+ title("Simple Photo Album"),
+ album_selector(state.album_id),
+ fetching_dialog() if state.is_fetching else album_photos(state.photos),
+ ],
+ )
+
+
+def create_fetch_effect(
+ album_id: int,
+) -> Callable[[Dispatch[Msg]], Coroutine[None, None, Any]]:
+ async def _fetch(dispatch: Dispatch[Msg]) -> None:
+ recv_photos = await fetch_photos(album_id=album_id)
+ dispatch(ReceivePhotos(recv_photos))
+
+ return _fetch
+
+
+def init() -> tuple[State, list[Effect[Msg]]]:
+ return (State(album_id=1, is_fetching=True, photos=[]), [create_fetch_effect(1)])
+
+
+def update(msg: Msg, state: State) -> tuple[State, list[Effect[Msg]]]:
+ match msg:
+ case SelectAlbum(album_id):
+ state = State(album_id=album_id, is_fetching=True, photos=[])
+ return (state, [create_fetch_effect(album_id=album_id)])
+ case ReceivePhotos(photos):
+ state = State(album_id=state.album_id, is_fetching=False, photos=photos)
+ return (state, [])
+
+
+app = AlfortDom[State, Msg](
+ init=init,
+ view=view,
+ update=update,
+)
+app.main(root="root")
diff --git a/docs/examples/simple_api/pkg_install.py b/docs/examples/simple_api/pkg_install.py
new file mode 100755
index 0000000..04a30ae
--- /dev/null
+++ b/docs/examples/simple_api/pkg_install.py
@@ -0,0 +1,7 @@
+import micropip # type: ignore # noqa: F401
+
+try:
+ # try to use development version
+ await micropip.install("../dist/alfort_dom-0.0.0.dev0-py3-none-any.whl") # type: ignore # noqa: F704
+except ValueError:
+ await micropip.install("alfort-dom") # type: ignore # noqa: F704
diff --git a/docs/examples/todomvc/main.py b/docs/examples/todomvc/main.py
index 18156a9..ed76367 100755
--- a/docs/examples/todomvc/main.py
+++ b/docs/examples/todomvc/main.py
@@ -30,7 +30,11 @@ def with_local_storage(update: Update["Msg", "Model"]) -> Update["Msg", "Model"]
@functools.wraps(update)
def _update(msg: Msg, model: Model) -> tuple[Model, list[Effect[Msg]]]:
model, effects = update(msg, model)
- return model, [lambda _: save_model(model), *effects]
+
+ async def _save_model(_: Dispatch[Msg]) -> None:
+ save_model(model)
+
+ return model, [_save_model, *effects]
return _update
diff --git a/pyproject.toml b/pyproject.toml
index d3b8b55..deeb900 100755
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ include = ["alfort_dom/py.typed"]
[tool.poetry.dependencies]
python = "^3.10"
-alfort = "^0.1.8"
+alfort = "^0.1.9"
[tool.poetry.group.dev.dependencies]
poethepoet = "^0.13.1"
diff --git a/stubs/pyodide/http/__init__.pyi b/stubs/pyodide/http/__init__.pyi
new file mode 100644
index 0000000..ecb0a50
--- /dev/null
+++ b/stubs/pyodide/http/__init__.pyi
@@ -0,0 +1,37 @@
+# derived from https://github.com/pyodide/pyodide/blob/main/src/py/pyodide/http.py
+
+from io import StringIO
+from typing import Any, BinaryIO, TextIO
+
+from .. import JsProxy
+
+def open_url(url: str) -> StringIO: ...
+
+class FetchResponse:
+ def __init__(self, url: str, js_response: JsProxy) -> None: ...
+ @property
+ def body_used(self) -> bool: ...
+ @property
+ def ok(self) -> bool: ...
+ @property
+ def redirected(self) -> bool: ...
+ @property
+ def status(self) -> str: ...
+ @property
+ def status_text(self) -> str: ...
+ @property
+ def type(self) -> str: ...
+ @property
+ def url(self) -> str: ...
+ def clone(self) -> "FetchResponse": ...
+ async def buffer(self) -> JsProxy: ...
+ async def string(self) -> str: ...
+ async def json(self, **kwargs: Any) -> Any: ...
+ async def memoryview(self) -> memoryview: ...
+ async def bytes(self) -> bytes: ...
+ async def _into_file(self, f: TextIO | BinaryIO) -> None: ...
+ async def unpack_archive(
+ self, *, extract_dir: str | None, format: str | None
+ ) -> None: ...
+
+async def pyfetch(url: str, **kwargs: Any) -> FetchResponse: ...