diff --git a/docs/source/_static/idom-flow-diagram.svg b/docs/source/_static/idom-flow-diagram.svg new file mode 100644 index 000000000..27c78b0b2 --- /dev/null +++ b/docs/source/_static/idom-flow-diagram.svg @@ -0,0 +1,383 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + layout + + + + + component + + server + + + + view + + client + + + + event + VDOM diff + VDOM + diff --git a/docs/source/_static/live-examples-in-docs.gif b/docs/source/_static/live-examples-in-docs.gif new file mode 100644 index 000000000..96a04d68b Binary files /dev/null and b/docs/source/_static/live-examples-in-docs.gif differ diff --git a/docs/source/_static/mvc-flow-diagram.svg b/docs/source/_static/mvc-flow-diagram.svg new file mode 100644 index 000000000..a1acbc2cb --- /dev/null +++ b/docs/source/_static/mvc-flow-diagram.svg @@ -0,0 +1,425 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + model + + + + + controller + + server + + + + + model + + + + + view + + client + + + + + event + sync + change + render + diff --git a/docs/source/_static/npm-download-trends.png b/docs/source/_static/npm-download-trends.png new file mode 100644 index 000000000..cf5140b0d Binary files /dev/null and b/docs/source/_static/npm-download-trends.png differ diff --git a/docs/source/architectural-patterns.rst b/docs/source/architectural-patterns.rst new file mode 100644 index 000000000..4e9d5292a --- /dev/null +++ b/docs/source/architectural-patterns.rst @@ -0,0 +1,207 @@ +Architectural Patterns +====================== + +Over the `past 5 years `__ 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 `_ 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 ` 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 ` +- :ref:`FastAPI ` +- :ref:`Tornado ` +- :ref:`Flask ` + +You can even target your usage of IDOM in your production-grade applications with IDOM's +Javascript `React client library `_. 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 ` 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 ` 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 `) 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 diff --git a/docs/source/core-concepts.rst b/docs/source/core-concepts.rst index dacb48252..fa1bb5ce5 100644 --- a/docs/source/core-concepts.rst +++ b/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 diff --git a/docs/source/examples/simple_and_gate.py b/docs/source/examples/simple_and_gate.py new file mode 100644 index 000000000..36ad60fd8 --- /dev/null +++ b/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) diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst index 0f7a1f269..f2d9a5ab7 100644 --- a/docs/source/getting-started.rst +++ b/docs/source/getting-started.rst @@ -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 ` -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 @@ -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:: diff --git a/docs/source/index.rst b/docs/source/index.rst index 8686a7f49..3fb55fb2c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -13,10 +13,11 @@ IDOM .. toctree:: :hidden: - :caption: Advanced Usage + :caption: Advanced Topics core-concepts javascript-components + architectural-patterns specifications .. toctree::