-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor!: make widget composable by allowing children (#52)
* refactor!: make widget composable by allowing children BREAKING CHANGE: this is a breaking change because of renaming set_value to setValue on the frontend side. This commit makes several changes, instead of having a single ReactWidget we now have a Widget (with no default value trait) and a ValueWidget (with a default value trait). Furthermore, by specificing _module and _type instead of _esm we can now render any React component from any ES module, or even standard html components like <div> or <span>. The main (wrapper) component is now created in the model, which makes it easier to obtain the components of children. Once the main wrapper component is created, the while children tree is also resolved, and a synchroneous render can be made in one go. * fix: create components out of children instead of elements to pass down the rootView * polish and docs * suggestion by mario for changing wording * add tests
- Loading branch information
1 parent
3011c5b
commit eb18699
Showing
13 changed files
with
1,133 additions
and
535 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,14 @@ React for ipywidgets that just works. No webpack, no npm, no hassle. Just write | |
|
||
Build on top of [AnyWidget](https://anywidget.dev/). | ||
|
||
## Why | ||
|
||
Ipyreact adds composability, allowing you to add children to your widget, which will render the whole react tree in | ||
a single react context, without adding extra divs or creating a new react context. | ||
|
||
This allows wrapping libraries such as [Material UI](https://mui.com/), [Ant Design](https://ant.design/) and even | ||
[React-three-fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction). | ||
|
||
## Tutorial | ||
|
||
This tutorial will walk you through the steps of building a complete ipywidget with react. | ||
|
@@ -13,9 +21,11 @@ This tutorial will walk you through the steps of building a complete ipywidget w | |
|
||
Just click the JupyterLite or Binder link to start the interactive walkthrough. | ||
|
||
## Goal | ||
## Goals | ||
|
||
Take any [Material UI example](https://mui.com/material-ui/react-rating/), copy/paste the code, and it should work in Jupyter Notebook, Jupyter Lab, Voila, and more specifically, [Solara](https://github.com/widgetti/solara). | ||
- Take any [Material UI example](https://mui.com/material-ui/react-rating/), copy/paste the code, and it should work in Jupyter Notebook, Jupyter Lab, Voila, and more specifically, [Solara](https://github.com/widgetti/solara). | ||
- Wrap a library such as [Ant Design](https://ant.design/) giving the options to customize any JSON<->JavaScript Object (de)serialization, such as the [DatePicker](https://ant.design/components/date-picker) which uses a dayjs object internally, which cannot be serialized over the wire to Python. | ||
- Compose widgets together to form a single react tree, with the same react context (e.g. useContext). | ||
|
||
## Examples | ||
|
||
|
@@ -25,13 +35,13 @@ Take any [Material UI example](https://mui.com/material-ui/react-rating/), copy/ | |
import ipyreact | ||
|
||
|
||
class ConfettiWidget(ipyreact.ReactWidget): | ||
class ConfettiWidget(ipyreact.ValueWidget): | ||
_esm = """ | ||
import confetti from "canvas-confetti"; | ||
import * as React from "react"; | ||
export default function({value, set_value, debug}) { | ||
return <button onClick={() => confetti() && set_value(value + 1)}> | ||
export default function({value, setValue}) { | ||
return <button onClick={() => confetti() && setValue(value + 1)}> | ||
{value || 0} times confetti | ||
</button> | ||
};""" | ||
|
@@ -40,6 +50,8 @@ ConfettiWidget() | |
|
||
![initial-30-fps-compressed](https://user-images.githubusercontent.com/1765949/233469170-c659b670-07f5-4666-a201-80dea01ebabe.gif) | ||
|
||
(_NOTE: in the recording we used on_value, we now use setValue_) | ||
|
||
### Hot reloading | ||
|
||
Create a tsx file: | ||
|
@@ -49,9 +61,9 @@ Create a tsx file: | |
import confetti from "canvas-confetti"; | ||
import * as React from "react"; | ||
|
||
export default function ({ value, set_value, debug }) { | ||
export default function ({ value, setValue }) { | ||
return ( | ||
<button onClick={() => confetti() && set_value(value + 1)}> | ||
<button onClick={() => confetti() && setValue(value + 1)}> | ||
{value || 0} times confetti | ||
</button> | ||
); | ||
|
@@ -65,7 +77,7 @@ import ipyreact | |
import pathlib | ||
|
||
|
||
class ConfettiWidget(ipyreact.ReactWidget): | ||
class ConfettiWidget(ipyreact.ValueWidget): | ||
_esm = pathlib.Path("confetti.tsx") | ||
|
||
ConfettiWidget() | ||
|
@@ -75,6 +87,8 @@ Now edit, save, and see the changes in your browser/notebook. | |
|
||
![hot-reload-compressed](https://user-images.githubusercontent.com/1765949/233470113-b2aa9284-71b9-44f0-bd52-906a08b06e14.gif) | ||
|
||
(_NOTE: in the recording we used on_value, we now use setValue_) | ||
|
||
### IPython magic | ||
|
||
First load the ipyreact extension: | ||
|
@@ -90,8 +104,8 @@ Then use the `%%react` magic to directly write jsx/tsx in your notebook: | |
import confetti from "canvas-confetti"; | ||
import * as React from "react"; | ||
|
||
export default function({value, set_value, debug}) { | ||
return <button onClick={() => confetti() && set_value(value + 1)}> | ||
export default function({value, setValue}) { | ||
return <button onClick={() => confetti() && setValue(value + 1)}> | ||
{value || 0} times confetti | ||
</button> | ||
}; | ||
|
@@ -101,6 +115,8 @@ Access the underlying widget with the name `_last_react_widget` (e.g. `_last_rea | |
|
||
![magic-optimized](https://user-images.githubusercontent.com/1765949/233471041-62e807d6-c16d-4fc5-af5d-13c0acb2c677.gif) | ||
|
||
(_NOTE: in the recording we used on_value, we now use setValue_) | ||
|
||
## Installation | ||
|
||
You can install using `pip`: | ||
|
@@ -111,19 +127,97 @@ pip install ipyreact | |
|
||
## Usage | ||
|
||
## Facts | ||
### Summary | ||
|
||
- The ReactWidget has an `value` trait, which is a `traitlets.Any` trait. Use this to pass data to your react component, or to get data back from your react component. | ||
- All traits are added as props to your react component (e.g. `{value, ...}` in th example above. | ||
- For every trait `ipyreact` automatically provides a `set_<traitname>` callback, which you can use to set the trait value from your react component (e.g. `set_value` in the example above). (_Note: we used `on_value` before, this is now deprecated_) | ||
- The `ValueWidget` has an `value` trait, which is a `traitlets.Any` trait. Use this to pass data to your react component, or to get data back from your react component (since it inherits from ipywidgets.ValueWidget it | ||
can be used in combination with ipywidgets' [interact](https://ipywidgets.readthedocs.io/en/latest/examples/Using%20Interact.html)). | ||
- The `ipyreact.Widget` does not have the `value` trait. | ||
- All traits are added as props to your react component (e.g. `{value, setValue...}` pairs in the example above. | ||
- For every trait `ipyreact` automatically provides a `set<Traitname>` callback, which you can use to set the trait value from your react component (e.g. `setValue` in the example above). (_Note: we used `on_value` before, this is now deprecated_) | ||
- Props can de passed as `Widget(props={"title": "My title"})`, and contrary to a trait, will not add a `setTitle` callable to the props. | ||
- Children can be passed using `Widget(children=['text', or_widget])` supporting text, widgets, and un-interrupted rendering of ipyreact widgets. | ||
- Your code gets transpiled using [sucrase](https://github.com/alangpierce/sucrase) in the frontend, no bundler needed. | ||
- Your code should be written in ES modules. | ||
- Set `debug=True` to get more debug information in the browser console (also accessible in the props). | ||
- Set `_debug=True` to get more debug information in the browser console. | ||
- Make sure you export a default function from your module (e.g. `export default function MyComponent() { ... }`). This is the component that will be rendered. | ||
- Pass `events={"onClick": handler}` to the constructor or add a method with the name `event_onClick(self, data=None)` to add a `onClick` callback to your props. | ||
|
||
### HTML elements | ||
|
||
You do not need to provide the module code to create built-in HTML elements, ipyreact supports the same API as [React's createElement](https://react.dev/reference/react/createElement) | ||
allowing creation of buttons for instance. | ||
|
||
```python | ||
import ipyreact | ||
ipyreact.Widget(_type="button", children=["click me"]) | ||
``` | ||
|
||
Note that in addition to all native browser elements, also web components are supported. | ||
|
||
### Children | ||
|
||
As shown in the above example, we also support children, which supports a list of strings (text), `ipyreact.Widget` widgets that will be rendered as an uninterrupted react tree, or | ||
any other `ipywidgets` | ||
|
||
```python | ||
import ipyreact | ||
import ipywidgets as widgets | ||
ipyreact.Widget(_type="div", children=[ | ||
"normal text", | ||
ipyreact.Widget(_type="button", children=["nested react widgets"]), | ||
widgets.FloatSlider(description="regular ipywidgets") | ||
]) | ||
``` | ||
|
||
[![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=children.ipynb) | ||
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Fchildren.ipynb) | ||
|
||
### Events | ||
|
||
Events can be passed via the event argument. In this case `onClick` will be added as a prop to the button element. | ||
|
||
```python | ||
import ipyreact | ||
ipyreact.Widget(_type="button", children=["click me"], events={"onClick": print}) | ||
``` | ||
|
||
Subclasses can also add an `event_onClick` method, which will also add a `onClick` event handler to the props. | ||
|
||
[![JupyterLight](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://widgetti.github.io/ipyreact/lab/?path=events.ipynb) | ||
[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/widgetti/ipyreact/HEAD?labpath=examples%2Fevents.ipynb) | ||
|
||
### Importing external modules | ||
|
||
Writing JSX code without having to compile/bundle is great, but so is using external libraries. | ||
|
||
Ipyreact uses ES modules, which allows native importing of external libraries when written as an ES module. | ||
In the example below, we use https://esm.sh/ which exposes many JS libraries as ES modules that | ||
we can directly import. | ||
|
||
```python | ||
import ipyreact | ||
|
||
ipyreact.ValueWidget( | ||
_esm=""" | ||
import confetti from "https://esm.sh/[email protected]"; | ||
import * as React from "react"; | ||
export default function({value, setValue}) { | ||
return <button onClick={() => confetti() && setValue(value + 1)}> | ||
{value || 0} times confetti | ||
</button> | ||
}; | ||
""" | ||
) | ||
``` | ||
|
||
### Import maps | ||
|
||
For every widget, you can provide an `_import_map`, which is a dictionary of module names to urls. By default we support `react` and `react-dom` which is prebundled. | ||
However, the above code now has a direct link to "https://esm.sh/[email protected]" which makes the code very specific to esm.sh. | ||
|
||
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. | ||
|
||
Apart from `react`, the default we provide is: | ||
|
||
|
@@ -139,30 +233,55 @@ _import_map = { | |
} | ||
``` | ||
|
||
Which means we can copy paste _most_ of the examples from [mui](https://mui.com/) | ||
Which means we can now write our ConfettiButton as: | ||
|
||
```python | ||
import ipyreact | ||
|
||
ipyreact.ValueWidget( | ||
_esm=""" | ||
import confetti from "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]", | ||
}, | ||
|
||
} | ||
) | ||
``` | ||
|
||
And it also means we can copy paste _most_ of the examples from [mui](https://mui.com/) | ||
|
||
```tsx | ||
%%react -n my_widget -d | ||
import Button from "@mui/material/Button"; | ||
import confetti from "canvas-confetti"; | ||
import * as React from "react"; | ||
|
||
export default function({ value, set_value, debug }) { | ||
if(debug) { | ||
console.log("value=", value, set_value); | ||
} | ||
export default function({ value, setValue}) { | ||
console.log("value=", value); | ||
return ( | ||
<Button | ||
variant="contained" | ||
onClick={() => confetti() && set_value(value + 1)} | ||
onClick={() => confetti() && setValue(value + 1)} | ||
> | ||
{value || 0} times confetti | ||
</Button> | ||
); | ||
} | ||
``` | ||
|
||
We add the https://github.com/guybedford/es-module-shims shim to the browser page for the import maps functionality. | ||
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. | ||
|
||
## Development Installation | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
{ | ||
"cells": [ | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "a828d6c2", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [ | ||
"import ipyreact\n", | ||
"import ipywidgets as widgets\n", | ||
"\n", | ||
"ipyreact.Widget(_type=\"div\", children=[\n", | ||
" \"normal text\",\n", | ||
" ipyreact.Widget(_type=\"button\", children=[\"nested react widgets\"]),\n", | ||
" widgets.FloatSlider(description=\"regular ipywidgets\")\n", | ||
"])" | ||
] | ||
}, | ||
{ | ||
"cell_type": "code", | ||
"execution_count": null, | ||
"id": "6b9e745b", | ||
"metadata": {}, | ||
"outputs": [], | ||
"source": [] | ||
} | ||
], | ||
"metadata": { | ||
"kernelspec": { | ||
"display_name": "Python 3 (ipykernel)", | ||
"language": "python", | ||
"name": "python3" | ||
}, | ||
"language_info": { | ||
"codemirror_mode": { | ||
"name": "ipython", | ||
"version": 3 | ||
}, | ||
"file_extension": ".py", | ||
"mimetype": "text/x-python", | ||
"name": "python", | ||
"nbconvert_exporter": "python", | ||
"pygments_lexer": "ipython3", | ||
"version": "3.9.16" | ||
} | ||
}, | ||
"nbformat": 4, | ||
"nbformat_minor": 5 | ||
} |
Oops, something went wrong.