Javascript Modules

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 seemlessly leverage the existing ecosystem of React components. This can be accomplished in different ways for different reasons:

Integration Method

Use Case

Dynamically Install Javascript (requires NPM)

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

Import Javascript Bundles

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

Dynamically Install Javascript

Warning

Before continuing install NPM.

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 succesfully installed, you can import and display the element:

import idom

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

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

Passing Props To Javascript

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 element:

import json

import idom


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


@idom.element
def ViewSliderEvents():
    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(ViewSliderEvents)

Import Javascript Bundles

For projects that will be shared with others we recommend bundling your Javascript with rollup or webpack into a web module. Once you’ve done this, you can distribute 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 core benefit of loading Javascript in this way is that users of your code won’t need NPM. 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.

Note

In the future IDOM will come with tools to help author Python packages with bundled Javascript

With that said, if you just want to see how this all works it might be easiest to hook in simple a hand-crafted Javascript module. 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.element(
        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": "#24909d",
                "lineWidth": 4,
                "axisColor": "#bdc3c7",
            }
        )
    )
)
import React from "./react.js";
import htm from "./htm.js";

const html = htm.bind(React.createElement);

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>`;
}