Skip to content

Commit

Permalink
port first IDOM article to docs
Browse files Browse the repository at this point in the history
  • Loading branch information
rmorshea committed Jun 21, 2021
1 parent d1a4903 commit 1a581eb
Show file tree
Hide file tree
Showing 9 changed files with 1,051 additions and 11 deletions.
383 changes: 383 additions & 0 deletions docs/source/_static/idom-flow-diagram.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/live-examples-in-docs.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
425 changes: 425 additions & 0 deletions docs/source/_static/mvc-flow-diagram.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/source/_static/npm-download-trends.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
207 changes: 207 additions & 0 deletions docs/source/architectural-patterns.rst
@@ -0,0 +1,207 @@
Architectural Patterns
======================

Over the `past 5 years <NPM-trends>`__ front-end developers seem to have concluded that
programs written with a declarative_ style or framework tend to be easier to understand
and maintain than those done imperatively. Put more simply, mutable state in programs
can quickly lead to unsustainable complexity. This trend is largely evidenced by the
`rise <Frontend-Frameworks-Popularity>`_ of Javascript frameworks like Vue_ and React_
which describe the logic of computations without explicitly stating their control flow.

.. _React: https://reactjs.org
.. _NPM-trends: https://www.npmtrends.com/react-vs-angular-vs-vue
.. _Vue: https://vuejs.org
.. _Declarative: https://www.youtube.com/watch?v=yGh0bjzj4IQ
.. _Frontend-Frameworks-Popularity: https://gist.github.com/tkrotoff/b1caa4c3a185629299ec234d2314e190

.. image:: _static/npm-download-trends.png

So what does this have to do with Python and IDOM? Well, because browsers are the de
facto "operating system of the internet", even back-end languages like Python have had
to figure out clever ways to integrate with them. While standard REST_ APIs are well
suited to applications built using HTML templates, modern browser users expect a higher
degree of interactivity than this alone can achieve.

.. _REST: https://en.wikipedia.org/wiki/Representational_state_transfer

A variety of Python packages have since been created to help solve this problem:

- IPyWidgets_ - Adds interactive widgets to `Jupyter Notebooks`_
- Dash_ - Allows data scientists to produces enterprise-ready analytic apps
- Streamlit_ - Turns simple Python scripts into interactive dashboards
- Bokeh_ - An interactive visualization library for modern web browsers

.. _IPyWidgets: https://github.com/jupyter-widgets/ipywidgets
.. _Jupyter Notebooks: https://jupyter.org/
.. _Dash: https://plotly.com/dash/
.. _Streamlit: https://www.streamlit.io/
.. _Bokeh: https://docs.bokeh.org/

However they each have drawbacks that can make them difficult to use.

3. **Restrictive ecosystems** - UI components developed for one framework cannot be
easily ported to any of the others because their APIs are either too complex,
undocumented, or are structurally inaccesible.

1. **Imperative paradigm** - IPyWidgets and Bokeh have not embraced the same declarative
design principles pioneered by front-end developers. Streamlit and Dash on the
otherhand, are declarative, but fall short of the features provided by React or Vue.

1. **Limited layouts** - At their initial inception, the developers of these libraries
were driven by the visualization needs of data scientists so the ability to create
complex UI layouts may not have been a primary engineering goal.

As a result, IDOM was developed to help solve these problems.


Ecosystem Independence
----------------------

IDOM has a flexible set of :ref:`core abstractions <Core Concepts>` that allow it to
interface with its peers. At the time of writing Jupyter, Dash, and Bokeh (via Panel)
are supported, while Streamlit is in the works:

- idom-jupyter_ (try it now with Binder_)
- idom-dash_
- `IDOM in Panel`_

.. _Panel: https://panel.holoviz.org/Comparisons.html#comparing-panel-and-bokeh
.. _idom-jupyter: https://github.com/idom-team/idom-jupyter
.. _Binder: https://mybinder.org/v2/gh/idom-team/idom-jupyter/main?filepath=notebooks%2Fintroduction.ipynb
.. _idom-dash: https://github.com/idom-team/idom-dash
.. _IDOM in Panel: https://panel.holoviz.org/reference/panes/IDOM.html#panes-gallery-idom

By providing well defined interfaces and straighforward protocols, IDOM makes it easy to
swap out any part of the stack with an alternate implementation if you want to. For
example, if you need a different web server for your application, IDOM already has
several options to choose from or, use as blueprints to create your own:

- :ref:`Sanic <Sanic Servers>`
- :ref:`FastAPI <FastAPI Servers>`
- :ref:`Tornado <Tornado Servers>`
- :ref:`Flask <Flask Servers>`

You can even target your usage of IDOM in your production-grade applications with IDOM's
Javascript `React client library <idom-client-react>`_. Just install it in your
front-end app and connect to a back-end websocket that's serving up IDOM models. This
documentation acts as a prime example for this targeted usage - most of the page is
static HTML, but embedded in it are :ref:`interactive examples <examples>` that feature
live views being served from a web socket:

.. _idom-client-react: https://github.com/idom-team/idom/tree/main/src/idom/client/packages/idom-client-react

.. image:: _static/live-examples-in-docs.gif


Declarative Components
----------------------

IDOM, by adopting the :ref:`Hook <Life Cycle Hooks>` design pattern from React_,
inherits many of its aesthetic and functional characteristics. For those unfamiliar with
hooks, user interfaces are composed of basic HTML elements that are constructed and
returned by special functions called "components". Then, through the magic of hooks,
those component functions can be made to have state. Consider the component below which
displays a basic representation of an AND-gate:

.. example:: simple_and_gate

Note that the code never explicitely describes how to evolve the frontend view when
events occur. Instead, it declares that, given a particular state, this is how the view
should look. It's then IDOM's responsibility to figure out how to bring that declaration
into being. This behavior of defining outcomes without stating the means by which to
achieve them is what makes components in IDOM and React "declarative". For comparison, a
hypothetical, and a more imperative approach to defining the same interface might look
similar to the following:

.. code-block::
layout = Layout()
def make_and_gate():
state = {"input_1": False, "input_2": False}
output_text = html.pre()
update_output_text(output_text, state)
def toggle_input(index):
state[f"input_{index}"] = not state[f"input_{index}"]
update_output_text(output_text, state)
return html.div(
html.input({"type": "checkbox", "onClick": lambda event: toggle_input(1)}),
html.input({"type": "checkbox", "onClick": lambda event: toggle_input(2)}),
output_text,
)
def update_output_text(text, state):
text.update(
children="{input_1} AND {input_2} = {output}".format(
input_1=state["input_1"],
input_2=state["input_2"],
output=state["input_1"] and state["input_2"],
)
)
layout.add_element(make_and_gate())
layout.run()
In this imperative incarnation there are several disadvantages:

1. **Refactoring is difficult** - Functions are much more specialized to their
particular usages in ``make_and_gate`` and thus cannot be easily generalized. By
comparison, ``use_toggle`` from the declarative implementation could be applicable to
any scenario where boolean indicators are toggled on and off.

2. **No clear static relations** - There is no one section of code through which to
discern the basic structure and behaviors of the view. This issue is exemplified by
the fact that we must call ``update_output_text`` from two different locations. Once
in the body of ``make_and_gate`` and again in the body of the callback
``toggle_input``. This means that, to understand what the ``output_text`` might
contain, we must also understand all the business logic that surrounds it.

3. **Referential links cause complexity** - To evolve the view, various callbacks must
hold references to all the elements that they will update. At the outset this makes
writing programs difficult since elements must be passed up and down the call stack
wherever they are needed. Considered further though, it also means that a function
layers down in the call stack can accidentally or intentionally impact the behavior
of ostensibly unrelated parts of the program.


Communication Scheme
--------------------

To communicate between its back-end Python server and Javascript client, IDOM uses
something called a Virtual Document Object Model (:ref:`VDOM <VDOM Mimetype>`) to
construct a representation of the view. The VDOM is constructed on the Python side by
components. Then, as it evolves, IDOM's layout computes VDOM-diffs and wires them to its
Javascript client where it is ultimately displayed:

.. image:: _static/idom-flow-diagram.svg

By contrast, IDOM's peers take an approach that aligns fairly closely with the
Model-View-Controller_ design pattern - the controller lives server-side (though not
always), the model is what's synchronized between the server and client, and the view is
run client-side in Javascript. To draw it out might look something like this:

.. image:: _static/mvc-flow-diagram.svg

.. _Model-View-Controller: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller


Javascript Integration
----------------------

If you're thinking critically about IDOM's use of a virtual DOM, you may have thought...

Isn't wiring a virtual representation of the view to the client, even if its diffed,
expensive?

And yes, while the performance of IDOM is sufficient for most use cases, there are
inevitably scenarios where this could be an issue. Thankfully though, just like its
peers, IDOM makes it possible to seemlesly integrate :ref:`Custom Javascript Components`.
They can be custom built for your use case, or you can just leverage the existing
Javascript ecosystem without any extra work:

.. example:: material_ui_slider
4 changes: 2 additions & 2 deletions docs/source/core-concepts.rst
@@ -1,8 +1,8 @@
Core Concepts
=============

This section covers core features of IDOM that are used in making
interactive interfaces.
This section covers core features of IDOM that are used in making interactive
interfaces.


Pure Components
Expand Down
24 changes: 24 additions & 0 deletions docs/source/examples/simple_and_gate.py
@@ -0,0 +1,24 @@
import idom


@idom.component
def AndGate():
input_1, toggle_1 = use_toggle()
input_2, toggle_2 = use_toggle()
return idom.html.div(
idom.html.input({"type": "checkbox", "onClick": lambda event: toggle_1()}),
idom.html.input({"type": "checkbox", "onClick": lambda event: toggle_2()}),
idom.html.pre(f"{input_1} AND {input_2} = {input_1 and input_2}"),
)


def use_toggle():
state, set_state = idom.hooks.use_state(False)

def toggle_state():
set_state(lambda old_state: not old_state)

return state, toggle_state


idom.run(AndGate)
16 changes: 8 additions & 8 deletions docs/source/getting-started.rst
Expand Up @@ -15,7 +15,7 @@ Since it's likely a lot to take in at once, we'll break it down piece by piece:
:linenos:

The ``idom.component`` decorator creates a :ref:`Component <Stateful Components>`
constructor which is "rendered" by the function below it. To create a Component instance
constructor whose "renderer" is the function below it. To create a Component instance
we call ``Slideshow()`` with the same arguments as its render function. The render
function of a Component returns a data structure that depicts a user interface, or in
more technical terms a Document Object Model (DOM). We call this structural
Expand All @@ -35,22 +35,22 @@ Calling a Hook inside a Component's render function (one decorated by ``idom.com
adds some local state to it. IDOM will preserve the state added by Hooks between calls
to the Component's render function.

The ``use_state`` hook returns two values - the *current* state value and a function
The ``use_state`` hook returns two values - the **current** state value and a function
that let's you update that value. In the case of ``Slideshow`` the value of the
``use_state`` hook determines which image is shown to the user, while its update
function allow us to change it. The one required argument of ``use_state`` is the
*initial* state value.
**initial** state value.

.. literalinclude:: /examples/slideshow.py
:lineno-start: 8
:lines: 8,9
:linenos:

The coroutine above will get added as an event handler to the resulting view. When it
responds to an event it will use the update function returned by the ``use_state`` Hook
to change which image is shown to the user. Calling the update function will schedule
the Component to be re-rendered. That is, the Component's render function will be called
again, and its new result will be displayed.
The function above will get added as an event handler to the resulting view. When it
responds to an event it will use ``set_state`` (the update function returned by the
``use_state`` Hook) to change which image is shown to the user. Calling the update
function will schedule the Component to be re-rendered. That is, the Component's render
function will be called again, and its new result will be displayed.

.. note::

Expand Down
3 changes: 2 additions & 1 deletion docs/source/index.rst
Expand Up @@ -13,10 +13,11 @@ IDOM

.. toctree::
:hidden:
:caption: Advanced Usage
:caption: Advanced Topics

core-concepts
javascript-components
architectural-patterns
specifications

.. toctree::
Expand Down

0 comments on commit 1a581eb

Please sign in to comment.