Javascript Components

While IDOM is a great tool for displaying HTML and responding to browser events with pure Python, there are other projects which already allow you to do this inside Jupyter Notebooks or in webpages. The real power of IDOM comes from its ability to seamlessly leverage the existing ecosystem of React components. This can be accomplished in different ways for different reasons:

Integration Method

Use Case

Custom Javascript Components

You want to create polished software that can be easily shared with others.

Dynamically Install Javascript (requires NPM)

You want to quickly experiment with IDOM and the Javascript ecosystem.

Custom Javascript Components

For projects that will be shared with others we recommend bundling your Javascript with rollup or webpack into a web module. IDOM also provides a template repository that can be used as a blueprint to build a library of React components.

The core benefit of loading Javascript in this way is that users of your code won’t need to have NPM installed. Rather, they can use pip to install your Python package without any other build steps because the bundled Javascript you distributed with it will be symlinked into the IDOM client at runtime.

To work as intended, the Javascript bundle must export the following named functions:

type createElement = (component: any, props: Object) => any;
type renderElement = (element: any, container: HTMLElement) => void;
type unmountElement = (element: HTMLElement) => void;

These functions can be thought of as being analogous to those from React.

And will be called in the following manner:

// on every render
renderElement(createElement(type, props), container);
// on unmount
unmountElement(container);

Once you’ve done this, you can distribute the bundled javascript in your Python package and integrate it into IDOM by defining Module objects that load them from source:

import idom
my_js_package = idom.Module("my-js-package", source_file="/path/to/my/bundle.js")

The simplest way to try this out yourself though, is to hook in simple a hand-crafted Javascript module that has the requisite interface. In the example to follow we’ll create a very basic SVG line chart. The catch though is that we are limited to using Javascript that can run directly in the browser. This means we can’t use fancy syntax like JSX and instead will use htm to simulate JSX in plain Javascript.

from pathlib import Path

import idom


path_to_source_file = Path(__file__).parent / "super_simple_chart.js"
ssc = idom.Module("super-simple-chart", source_file=path_to_source_file)


idom.run(
    idom.component(
        lambda: ssc.SuperSimpleChart(
            {
                "data": [
                    {"x": 1, "y": 2},
                    {"x": 2, "y": 4},
                    {"x": 3, "y": 7},
                    {"x": 4, "y": 3},
                    {"x": 5, "y": 5},
                    {"x": 6, "y": 9},
                    {"x": 7, "y": 6},
                ],
                "height": 300,
                "width": 500,
                "color": "royalblue",
                "lineWidth": 4,
                "axisColor": "silver",
            }
        )
    )
)
import { h, Component, render } from "https://unpkg.com/preact?module";
import htm from "https://unpkg.com/htm?module";

const html = htm.bind(h);

export { h as createElement, render as renderElement };

export function unmountElement(container) {
  preactRender(null, container);
}

export function SuperSimpleChart(props) {
  const data = props.data;
  const lastDataIndex = data.length - 1;

  const options = {
    height: props.height || 100,
    width: props.width || 100,
    color: props.color || "blue",
    lineWidth: props.lineWidth || 2,
    axisColor: props.axisColor || "black",
  };

  const xData = data.map((point) => point.x);
  const yData = data.map((point) => point.y);

  const domain = {
    xMin: Math.min(...xData),
    xMax: Math.max(...xData),
    yMin: Math.min(...yData),
    yMax: Math.max(...yData),
  };

  return html`<svg
    width="${options.width}px"
    height="${options.height}px"
    viewBox="0 0 ${options.width} ${options.height}"
  >
    ${makePath(props, domain, data, options)} ${makeAxis(props, options)}
  </svg>`;
}

function makePath(props, domain, data, options) {
  const { xMin, xMax, yMin, yMax } = domain;
  const { width, height } = options;
  const getSvgX = (x) => ((x - xMin) / (xMax - xMin)) * width;
  const getSvgY = (y) => height - ((y - yMin) / (yMax - yMin)) * height;

  let pathD = "M " + getSvgX(data[0].x) + " " + getSvgY(data[0].y) + " ";
  pathD += data.map((point, i) => {
    return "L " + getSvgX(point.x) + " " + getSvgY(point.y) + " ";
  });

  return html`<path
    d="${pathD}"
    style=${{
      stroke: options.color,
      strokeWidth: options.lineWidth,
      fill: "none",
    }}
  />`;
}

function makeAxis(props, options) {
  return html`<g>
    <line
      x1="0"
      y1=${options.height}
      x2=${options.width}
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
    <line
      x1="0"
      y1="0"
      x2="0"
      y2=${options.height}
      style=${{ stroke: options.axisColor, strokeWidth: options.lineWidth * 2 }}
    />
  </g>`;
}

Dynamically Install Javascript

Warning

  • Before continuing install NPM.

  • Not guaranteed to work in all client implementations (see IDOM_CLIENT_MODULES_MUST_HAVE_MOUNT)

IDOM makes it easy to draft your code when you’re in the early stages of development by using NPM to directly install Javascript packages on the fly. In this example we’ll be using the ubiquitous React-based UI framework Material UI which can be installed using the idom CLI:

idom install @material-ui/core

Or at runtime with idom.client.module.install() (this is useful if you’re working in a REPL or Jupyter Notebook):

import idom
material_ui = idom.install("@material-ui/core")
# or install multiple modules at once
material_ui, *other_modules = idom.install(["@material-ui/core", ...])

Note

Any standard javascript dependency specifier is allowed here.

Once the package has been successfully installed, you can import and display the component:

import idom


material_ui = idom.install("@material-ui/core", fallback="loading...")

idom.run(
    idom.component(
        lambda: material_ui.Button(
            {"color": "primary", "variant": "contained"}, "Hello World!"
        )
    )
)

Passing Props To Javascript Components

So now that we can install and display a Material UI Button we probably want to make it do something. Thankfully there’s nothing new to learn here, you can pass event handlers to the button just as you did when Getting Started. Thus, all we need to do is add an onClick handler to the component:

import json

import idom


material_ui = idom.install("@material-ui/core", fallback="loading...")


@idom.component
def ViewButtonEvents():
    event, set_event = idom.hooks.use_state(None)

    return idom.html.div(
        material_ui.Button(
            {
                "color": "primary",
                "variant": "contained",
                "onClick": lambda event: set_event(event),
            },
            "Click Me!",
        ),
        idom.html.pre(json.dumps(event, indent=2)),
    )


idom.run(ViewButtonEvents)