Skip to content

Commit

Permalink
refactor!: add module support and take out import maps from widgets
Browse files Browse the repository at this point in the history
This should be the pre-cursor to what should become ipyesm, which
could be a separate package that ipyvue, ipyreact and possibly
anywidget could depend on and use.

Note that the interface is declerative (you do not explicitly create
widgets), which allows Solara (or other frameworks that render widgets
to handle the import maps and esm loading in a different way.)

For instance, solara could add a import map DOM element, and load the
ESM modules via a script tag.
  • Loading branch information
maartenbreddels committed Feb 6, 2024
1 parent 4a1b2a1 commit 842dea5
Show file tree
Hide file tree
Showing 12 changed files with 19,334 additions and 72 deletions.
132 changes: 113 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,45 +217,42 @@ However, the above code now has a direct link to "https://esm.sh/canvas-confetti

To address this, we also support [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to
write code more independant of where the modules come from.
For every widget, you can provide an `_import_map`, which is a dictionary of module names to urls or other modules. By default we support `react` and `react-dom` which is prebundled.
You can provide an import map using `ipyreact.define_import_map`, which takes a dictionary of module names to urls or other modules. By default we support `react` and `react-dom` which is prebundled.

Apart from `react`, the default we provide is:

```python
_import_map = {
"imports": {
"@mui/material/": "https://esm.sh/@mui/[email protected]/",
"@mui/icons-material/": "https://esm.sh/@mui/icons-material/",
"canvas-confetti": "https://esm.sh/[email protected]",
},
"scopes": {
},
}
define_import_map({
"@mui/material": "https://esm.sh/@mui/[email protected]",
"@mui/material/": "https://esm.sh/@mui/[email protected]/",
"@mui/icons-material/": "https://esm.sh/@mui/icons-material/",
"canvas-confetti": "https://esm.sh/[email protected]",
})

```

Which means we can now write our ConfettiButton as:

```python
import ipyreact

# note that this import_map is already part of the default
ipyreact.define_import_map({
"canvas-confetti": "https://esm.sh/[email protected]",
})


ipyreact.ValueWidget(
_esm="""
import confetti from "confetti";
import confetti from "canvas-confetti";
import * as React from "react";
export default function({value, setValue}) {
return <button onClick={() => confetti() && setValue(value + 1)}>
{value || 0} times confetti
</button>
};
""",
# note that this import_map is already part of the default
_import_map={
"imports": {
"confetti": "https://esm.sh/[email protected]",
},

}
"""
)
```

Expand Down Expand Up @@ -283,6 +280,103 @@ export default function({ value, setValue}) {
We use the https://github.com/guybedford/es-module-shims shim to the browser page for the import maps functionality.
This also means that although import maps can be configured per widget, they configuration of import maps is global.

### Bundled ESM modules

## Creating the ES module

While esm.sh is convenient to use, for production use, we recommend creating a standalone bundle. This will load faster and will not require a direct connection to esm.sh, which might not be available in airgapped or firewalled environments.

We will not create a minimal bundle for https://ant.design/

First create a simple file called `antd-minimal.js` that exports what we need.

```javascript
export { Button, Flex, Slider } from "antd";
```

Next, we install the libraries:

```bash
$ npm install antd
```

And use ESBuild to turn this into a self-contained module/bundle, without react, since ipyreact provides that for us.

```
$ npx esbuild ./antd-minimal.js --bundle --outfile=./antd-minimal.esm.js --format=esm --external:react --external:react-dom --target=esnext
```

Now we can define the module with a custom name (we call it antd-minimal).

```python
import ipyreact
from pathlib import Path

ipyreact.define_module("antd-minimal", Path("./antd-minimal.esm.js"))
```

We can now use the components from this module:

```python
def on_click(event_data):
w.children = ["Clicked"]

w = ipyreact.Widget(_module="antd-minimal", _type="Button", children=["Hi there"], events={"onClick": on_click})
w
```

Or, composing multiple ones:

```python
stack = ipyreact.Widget(_module="antd-minimal", _type="Flex",
props={"vertical": True, "style": {"padding": "24px"}},
children=[
ipyreact.Widget(_module="antd-minimal", _type="Button", children=["Ant Design Button"]),
ipyreact.Widget(_module="antd-minimal", _type="Slider",
props={"defaultValue": 3, "min": 0, "max": 11}),
])
stack
```

Input components might need a little bit of custom code, and subclassing `ValueWidget`. It often means binding the value to the right prop of the input component (in this case the Slider takes the same name, `value`) and coupling the event handler (in this case `onChange`) to the `setValue` function.

```python
import traitlets


class Slider(ipyreact.ValueWidget):
_esm = """
import {Slider} from "antd-minimal"
export default ({value, setValue, ...rest}) => {
return <Slider value={value} onChange={(v) => setValue(v)} {...rest}/>
}
"""
s = Slider(value=2)
s
```

_Note that it depends on the implementation of the event handler if the value is being passed directly, or a (synthetic) event with the data will be passed as argument. An typical example event handler could be `onChange={(event) => setValue(event.target.value)}`._

Now the slider widget is stateful, and we have bi-directional communication using the `.value` trait.
For instance, we can read it:

```python
s.value
```

Or write to it, and it will be reflected directly in the UI.

```python
s.value = 10
```

Test this out in the notebook:
[![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=antd/antd.ipynb)
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Fantd%2Fantd.ipynb)

## Development Installation

Create a dev environment:
Expand Down
Loading

0 comments on commit 842dea5

Please sign in to comment.