Skip to content

Commit

Permalink
Merge pull request #16 from ar90n/feature/add-enqueue-support
Browse files Browse the repository at this point in the history
feat: Add enqueue support for rendergin
  • Loading branch information
ar90n authored Apr 29, 2022
2 parents 0d5581e + dc4aafc commit 0053bb8
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 41 deletions.
4 changes: 2 additions & 2 deletions alfort/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
except PackageNotFoundError:
__version__: str = "unknown"

from .app import Alfort, Dispatch, Effect, Init, Mount, Update, View
from .app import Alfort, Dispatch, Effect, Init, Update, View

__all__ = ["Alfort", "Dispatch", "Effect", "View", "Update", "Init", "Mount"]
__all__ = ["Alfort", "Dispatch", "Effect", "View", "Update", "Init"]
26 changes: 16 additions & 10 deletions alfort/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@

Dispatch: TypeAlias = Callable[[M], None]
Effect: TypeAlias = Callable[[Dispatch[M]], None]
View: TypeAlias = Callable[[S], VDom | None]
Update: TypeAlias = Callable[[M, S], tuple[S, list[Effect[M]]]]
Init: TypeAlias = Callable[[], tuple[S, list[Effect[M]]]]
Mount: TypeAlias = Callable[[N], None]
View: TypeAlias = Callable[[S], VDom]
Update: TypeAlias = Callable[[M, S], tuple[S, list[Effect[M]]]]
Enqueue: TypeAlias = Callable[[Callable[[], None]], None]


@dataclass(slots=True, frozen=True)
Expand Down Expand Up @@ -185,20 +185,26 @@ def _main(
init: Init[S, M],
view: View[S],
update: Update[M, S],
mount: Mount[N],
root_node: N,
enqueue: Enqueue = lambda render: render(),
) -> None:
state, effects = init()
root = None
root = NodeDomElement(tag="__root__", props={}, children=[], node=root_node)

def rooted_view(state: S) -> VDom:
return VDomElement("__root__", {}, [view(state)])

def render() -> None:
nonlocal state
nonlocal root
(root, _) = cls.patch(dispatch, root, rooted_view(state))

def dispatch(msg: M) -> None:
nonlocal state
nonlocal root
(state, effects) = update(msg, state)
enqueue(render)
cls._run_effects(dispatch, effects)
(root, _) = cls.patch(dispatch, root, view(state))

enqueue(render)
cls._run_effects(dispatch, effects)
(root, _) = cls.patch(dispatch, None, view(state))

if root is not None and root.node is not None:
mount(root.node)
9 changes: 4 additions & 5 deletions alfort/vdom.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

T = TypeVar("T")

Props: TypeAlias = MutableMapping[str, Any]


@dataclass(slots=True, frozen=True)
class PatchProps:
remove_keys: list[str]
add_props: "Props"
add_props: Props


@dataclass(slots=True, frozen=True)
Expand Down Expand Up @@ -37,7 +39,7 @@ def apply(self, patch: Patch) -> None:
@dataclass(slots=True, frozen=True)
class Element(Generic[T]):
tag: str
props: "Props"
props: Props
children: list[T]


Expand All @@ -49,9 +51,6 @@ class VDomElement(Element["VDom"]):
VDom = VDomElement | str


Props: TypeAlias = MutableMapping[str, Any]


def el(
tag: str,
props: Props | None = None,
Expand Down
100 changes: 76 additions & 24 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from dataclasses import dataclass
from typing import Any
from typing import Any, Callable, Generic, TypeVar

import pytest

from alfort import Alfort, Dispatch, Effect, Init, Update, View
from alfort.app import Mount, NodeDom, NodeDomElement, NodeDomText
from alfort.app import Enqueue, NodeDom, NodeDomElement, NodeDomText
from alfort.vdom import (
Node,
Patch,
Expand All @@ -17,6 +17,8 @@
el,
)

T = TypeVar("T", bound=Node)


def to_vnode(node_dom: NodeDom) -> VDom:
match node_dom:
Expand All @@ -26,6 +28,20 @@ def to_vnode(node_dom: NodeDom) -> VDom:
return el(tag, props, [to_vnode(child) for child in children])


class RootNode(Node, Generic[T]):
child: T | None

def __init__(self) -> None:
self.child = None

def apply(self, patch: Patch) -> None:
match patch:
case PatchInsertChild(child):
self.child = child
case _:
raise ValueError(f"Invalid patch: {patch}")


class MockNode(Node):
patches: list[Patch]

Expand Down Expand Up @@ -61,12 +77,7 @@ def main(
update: Update[Any, dict[str, Any]],
) -> None:

cls._main(
init=init,
view=view,
update=update,
mount=lambda node: None,
)
cls._main(init=init, view=view, update=update, root_node=cls.mock_target)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -194,7 +205,7 @@ def apply(self, patch: Patch) -> None:
raise ValueError(f"Invalid patch: {patch}")


class AlfortText(Alfort[dict[str, Any], Msg, TextNode]):
class AlfortText(Alfort[dict[str, Any], Msg, TextNode | RootNode[TextNode]]):
@classmethod
def create_text(
cls,
Expand All @@ -208,24 +219,27 @@ def create_element(
cls,
tag: str,
props: Props,
children: list[TextNode],
children: list[TextNode | RootNode[TextNode]],
dispatch: Dispatch[Msg],
) -> TextNode:
return TextNode("", dispatch)
) -> RootNode[TextNode]:
return RootNode()

@classmethod
def main(
cls,
init: Init[dict[str, Any], Msg],
view: View[dict[str, Any]],
update: Update[Msg, dict[str, Any]],
mount: Mount[TextNode],
root_node: RootNode[TextNode],
enqueue: Enqueue = lambda render: render(),
) -> None:
cls._main(init=init, view=view, update=update, mount=mount)
cls._main(
init=init, view=view, update=update, root_node=root_node, enqueue=enqueue
)


def test_update_state() -> None:
root = TextNode("", lambda _: None)
root = RootNode[TextNode]()

def view(state: dict[str, int]) -> VDom:
return str(state["count"])
Expand All @@ -242,14 +256,52 @@ def update(
case CountDown(value):
return ({"count": state["count"] - value}, [])

def mount(target: TextNode) -> None:
nonlocal root
root = target
AlfortText.main(init=init, view=view, update=update, root_node=root)

assert root.child is not None
assert root.child.text == "3"
root.child.dispatch(CountUp())
assert root.child.text == "4"
root.child.dispatch(CountDown())
assert root.child.text == "3"


def test_enqueue() -> None:
root = RootNode[TextNode]()

view_values: list[str | None] = []

def capture(_: Dispatch[Msg]) -> None:
nonlocal view_values
if root.child is None:
view_values.append(None)
else:
view_values.append(root.child.text)

rendering_queue: list[Callable[[], None]] = []

def enqueue(f: Callable[[], None]) -> None:
rendering_queue.append(f)

def render() -> None:
for f in rendering_queue:
f()

def view(state: dict[str, int]) -> VDom:
return str(state["count"])

def init() -> tuple[dict[str, int], list[Effect[Msg]]]:
return ({"count": 5}, [capture, lambda d: enqueue(lambda: capture(d))])

def update(
msg: Msg, state: dict[str, int]
) -> tuple[dict[str, int], list[Effect[Msg]]]:
return (state, [])

AlfortText.main(init=init, view=view, update=update, mount=mount)
AlfortText.main(
init=init, view=view, update=update, root_node=root, enqueue=enqueue
)

assert root.text == view({"count": 3})
root.dispatch(CountUp())
assert root.text == view({"count": 4})
root.dispatch(CountDown())
assert root.text == view({"count": 3})
assert view_values == [None]
render()
assert view_values == [None, "5"]

0 comments on commit 0053bb8

Please sign in to comment.