Skip to content

Instantly share code, notes, and snippets.

@rmorshea
Last active January 3, 2021 17:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rmorshea/24d369fac53c2e1a07557850a0e7ff13 to your computer and use it in GitHub Desktop.
Save rmorshea/24d369fac53c2e1a07557850a0e7ff13 to your computer and use it in GitHub Desktop.
IDOM - highly interactive web applications in pure Python

Contents

  1. Background
  2. Enter IDOM
  3. IDOM vs IPyWidgets
  4. Features of IDOM
  5. Conclusion

Background

Over the past 5 years front-end developers seem to have arrived at the conclusion that declarative programming is usually bettern than imperative. 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.

npm download trends

So what does this have to do with Python? 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 it. While standard REST APIs are well suited to applications built using HTML templates, they don't always enable the degree of interactivity expected by modern browser users. Additionally, the learning curve for Python beginners is steepened since knowing Python isn't enough on its own to build a full-stack application.

In the wake of these and other problems, Python-based packages like IPyWidgets, Streamlit and Plotly Dash have sprung up to solve them. However they have their own limitations:

  1. IPyWidgets and Streamlit are imperative frameworks. So many of the lessons learned by developers of front-end frameworks have not been translated into their Python-based counterparts.

  2. At their initial inception, most of these libraries were driven by the visualization needs of data scientists, so the ability to design arbitrary UI layouts remains stunted.

  3. All are limited to specific ecosystems. For example IPyWidgets requires a back-end Jupyter Kernel in order to send data back and forth between Python and the front-end.

Enter IDOM

A new declarative Python package for building user interfaces.

idom logo

IDOM takes obvious inspiration from React, and wherever possible, attempts to achieve parity with the features it copies more directly. Nowhere is this more evident than the version of React's often lauded "Hooks" that IDOM implements in Python.

At a glance, the similarities between IDOM and React are rather striking. Below is a React component which defines a simple Slideshow displaying an image that updates when a user clicks on it, and immediately below it is the same view implemented in Python using IDOM:

import React, { useState } from react;

function Slideshow() {
  const [index, setIndex] = useState(0);
  return (
    <img
      src={ `https://picsum.photos/800/300?image=${index}` }
      onClick={ () => setIndex(index + 1) }
      style={ {cursor: "pointer"} }
    />
  )
}
import idom

@idom.element
def Slideshow():
    index, set_index = idom.hooks.use_state(0)
    return idom.html.img(
        {
            "src": f"https://picsum.photos/800/300?image={index}",
            "onClick": lambda event: set_index(index + 1),
            "style": {"cursor": "pointer"},
        }
    )

idom.run(Slideshow)

IDOM vs IPyWidgets

Just so we can see what makes IDOM so unique among Python's UI packages, let's take a look at what it would take to recreate the same Slideshow from the prior section using IPywidgets (the most popular of the three alternatives mentioned above):

from ipywidgets import widgets
from ipyevents import Event
from IPython.display import display

def slideshow():
    data = {"index": 0}

    img_widget = widgets.HTML()
    update_slideshow_widget(img_widget, data)

    on_click = Event(source=img_widget, watched_events=["click"])

    def click_handler(*args, **kwargs):
        img_widget.value = update_slideshow_widget(img_widget, data)

    on_click.on_dom_event(click_handler)

    return img_widget

def update_slideshow_widget(widget, data):
    new_html = html_slideshow_img(data["index"])
    widget.value = new_html
    data["index"] += 1

def html_slideshow_img(index):
    url = f"https://picsum.photos/800/300?image={index}"
    return html_img(src=url, style={"cursor": "pointer"})

def html_img(src, style):
    style_str = ";".join(f"{k}:{v}" for k, v in style.items())
    return f"<img src={src!r} style={style_str!r} />"

display(slideshow())

There's several important differences to note about the IPyWidgets implementation versus the one done using IDOM...

Event Handling

from ipyevents import Event

IPyWidgets does not provide a built-in way to register event handlers to standard HTML elements. To do so requires implementing a custom widget the explanation of which would require its own lengthy blog post. Thus we need to use ipyevents.

Normally, installing third party packages to achieve a specialized need is perfectly alright. However in this case I think it's hard to argue that being able to quickly and easily register DOM event handlers is a "specialized" need.

IDOM on the other hand is designed from the ground up with this sort of interactivity in mind. All you need to do is assign a function to an event name in the element's attribute dictionary:

"onClick": lambda event: set_index(index + 1)

Layout Updates

widget.value = new_html

IPyWidgets has to update with UI with raw HTML (unless you have a custom widget) by setting the innerHTML attribute of a DOM element (which is a particularly expensive operation). When running the code above, the image flickers in and out of existence whenever a new image is loaded:

ipywidgets sets innerHTML

IDOM computes the difference between the old and new layout state and only sends what what has changed. Those changes are then sent to a front-end React-based client implemented in React which also optimizes the number of DOM mutations required to evolve the view. These two things together solve the flickering problem seen above:

no flicker in IDOM

Virtual Document Object Model

return f"<img src={src!r} style={style_str!r} />"

IPywidgets requires Python users to construct raw HTML strings when no existing widgets suite their needs. Perhaps if IPyWidgets were a comprehensive templating library (in the way Vue is) this could even be advantageous, but given this is not the case, attempting to programmatically constructing a UI out of strings is quite awkward.

IDOM uses dictionaries to create a "Virtual" representation of the Document Object Model (VDOM). This makes it incredibly easy to write straightforward and composable functions that return VDOM dictionaries. Further the use of standardized VDOM representation means creating third party IDOM client implementations is effortless (see Ecosystem Independence for more info)

Features of IDOM

While continuing to compare IDOM to other Python-based UI packages does highlight some of its capabilites, there's so much more it can do...

Custom Javascript

Even though IDOM is intended to put power into the hands of pure Python developers, the fact that Python is a back-end language means that there will always be some inherent limitations. As a result, you may need to use existing Javascript libraries. Thankfully though, this is dead simple in IDOM. If you're just experimenting use the idom CLI to intall any Javascript you're looking for from NPM:

$ idom install victory

Then you can directly import it into IDOM and use it just like any other element:

idom imports js

If you wish to distribute your own custom Javascript-based components as a Python package. IDOM provides a repository template that can be set up using the popular cookiecutter project. This template represents the most up-to-date and recommended method of distributing Javascript for use by IDOM:

$ pip install cookiecutter
$ cookiecutter https://github.com/idom-team/idom-package-cookiecutter.git

Only Where You Need It

You can target your usage of IDOM in your production-grade applications with IDOM's Javascript client library. Just install the client in your front-end app and connect it to a back-end websocket that's serving up IDOM models - instead of creating your whole application with IDOM, you can use it exactly where you need it. Further, the ability to leverage custom javascript in IDOM means that you can progressively develop React components for your particular use cases. In a fully matured application, IDOM can act primarilly as a simple, yet performant, data synchronization layer.

Ecosystem Independence

The core abstractions of IDOM have well defined interfaces and protocols that enforce clear boundaries of communication.

idom data flow

This means that you aren't locked into any particular layer of the stack. You can swap out the web-server or even the client with relative ease - IDOM's built-in implementations for both the web-server and client are collectively just a few hundred lines of code.

To emphasize the point of independence, look no further than IDOM's integration with IPyWidgets - idom-jupyter is a Python package that natively integrates IDOM into Juyter as a Jupyter Widget with a similarly small amount of Python and Javascript:

idom in jupyter gif

You can try it out right now in Binder!

Comprehensive Documentation

For a fledgling project IDOM's documentation is surprisingly thorough. Check it out:

Conclusion

Building highly interactive web applications as a Python developer has historically been a great challenge. However, IDOM changes that. Knowing just basic HTML, CSS, and Python, you can make everything from slideshows to interactive dashboards. Its usage can be as broad or targeted as you need it to be, and you can use it wherever you need it, whether that's in a Jupyter notebook or standard web application.

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment