This section covers core features of IDOM that are used in making interactive interfaces.
As in most programming paradigms, so many of the problems come down to how we manage state. The first tool in encouraging its proper curation is the usage of pure functions. The benefit of a pure function is that there's no state. Similar to the addage "the best code is no code at all," we make the related claim that "the best way to manage state is to have no state at all."
With IDOM the core of your application will be built on the back of basic functions and coroutines that return VDOM <VDOM Mimetype>
models and which do so without state and without side effects. We call these kinds of model rendering functions Pure Components
. For example, one might want a function which accepted a list of strings and turned it into a series of paragraph elements:
def paragraphs(list_of_text):
return idom.html.div([idom.html.p(text) for text in list_of_text])
A Stateful Component is one which uses a Life Cycle Hooks
. These life cycle hooks allow you to add state to otherwise stateless functions. To create a stateful component you'll need to apply the ~idom.core.component.component
decorator to a coroutine whose body contains a hook usage. We'll demonstrate that with a simple click counter
:
import idom
@idom.component def ClickCount(): count, set_count = idom.hooks.use_state(0)
- return idom.html.button(
{"onClick": lambda event: set_count(count + 1)}, [f"Click count: {count}"],
)
Displaying components requires you to turn them into VDOM <VDOM Mimetype>
-this is done using a ~idom.core.layout.Layout
. Layouts are responsible for rendering components (turning them into VDOM) and scheduling their re-renders when they ~idom.core.layout.Layout.update
. To create a layout, you'll need a ~idom.core.component.Component
instance, which will become its root, and won't ever be removed from the model. Then you'll just need to call and await a ~idom.core.layout.Layout.render
which will return a JSON Patch
:
- async with idom.Layout(ClickCount()) as layout:
patch = await layout.render()
The layout also handles the triggering of event handlers. Normally this is done automatically by a Dispatcher <Layout Dispatcher>
, but for now we'll do it manually. We can use a trick to hard-code the event_handler_id
so we can pass it, and a fake event, to the layout's ~idom.core.layout.Layout.dispatch
method. Then we just have to re-render the layout and see what changed:
from idom.core.layout import LayoutEvent
@idom.component def ClickCount(): count, set_count = idom.hooks.use_state(0) return idom.html.button( {"onClick": lambda event: set_count(count + 1)}, [f"Click count: {count}"], )
- async with idom.Layout(ClickCount(key="something")) as layout:
patch_1 = await layout.render()
fake_event = LayoutEvent("/something/onClick", [{}]) await layout.dispatch(fake_event) patch_2 = await layout.render()
- for change in patch_2.changes:
- if change["path"] == "/children/0":
count_did_increment = change["value"] == "Click count: 1"
assert count_did_increment
An ~idom.core.dispatcher.AbstractDispatcher
implementation is a relatively thin layer of logic around a ~idom.core.layout.Layout
which drives the triggering of events and layout updates by scheduling an asynchronous loop that will run forever -effectively animating the model. To execute the loop, the dispatcher's ~idom.core.dispatcher.AbstractDispatcher.run
method accepts two callbacks. One is a "send" callback to which the dispatcher passes updates, while the other is "receive" callback that's called by the dispatcher to events it should execute.
import asyncio
from idom.core import SingleViewDispatcher, EventHandler from idom.core.layout import LayoutEvent
sent_patches = []
- async def send(patch):
sent_patches.append(patch) if len(sent_patches) == 5: # if we didn't cancel the dispatcher would continue forever raise asyncio.CancelledError()
- async def recv():
event = LayoutEvent("/my-component/onClick", [{}])
# We need this so we don't flood the render loop with events. # In practice this is never an issue since events won't arrive # as quickly as in this example. await asyncio.sleep(0)
return event
- async with SingleViewDispatcher(
idom.Layout(ClickCount(key="my-component"))
- ) as dispatcher:
context = None # see note below await dispatcher.run(send, recv, context)
assert len(sent_patches) == 5
Note
context
is information that's specific to the ~idom.core.dispatcher.AbstractDispatcher
implementation. In the case of the ~idom.core.dispatcher.SingleViewDispatcher
it doesn't require any context. On the other hand the ~idom.core.dispatcher.SharedViewDispatcher
requires a client ID as its piece of contextual information.
The Dispatcher <Layout Dispatcher>
allows you to animate the layout, but we still need to get the models on the screen. One of the last steps in that journey is to send them over the wire. To do that you need an ~idom.server.base.AbstractRenderServer
implementation. Presently, IDOM comes with support for the following web servers:
sanic.app.Sanic
(pip install idom[sanic]
)idom.server.sanic.PerClientStateServer
idom.server.sanic.SharedClientStateServer
- fastapi.FastAPI (
pip install idom[fastapi]
)idom.server.fastapi.PerClientStateServer
idom.server.fastapi.SharedClientStateServer
flask.Flask
(pip install idom[flask]
)idom.server.flask.PerClientStateServer
tornado.web.Application
(pip install idom[tornado]
)idom.server.tornado.PerClientStateServer
However, in principle, the base server class is capable of working with any other async enabled server framework. Potential candidates range from newer frameworks like vibora and starlette to aiohttp.
Note
If using or implementing a bridge between IDOM and an async server not listed here interests you, post an issue.
The main thing to understand about server implementations is that they can function in two ways - as a standalone application or as an extension to an existing application.
The implementation constructs a default application that's used to serve the dispatched models:
import idom
from idom.server.sanic import PerClientStateServer
@idom.component
def View(self):
return idom.html.h1(["Hello World"])
app = PerClientStateServer(View)
app.run("localhost", 5000)
The implementation registers hooks into the application to serve the model once run:
import idom
from idom.server.sanic import PerClientState
from sanic import Sanic
app = Sanic()
@idom.component
def View(self):
return idom.html.h1(["Hello World"])
per_client_state = PerClientStateServer(View)
per_client_state.register(app)
app.run("localhost", 5000)