Skip to content

Commit

Permalink
polish and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
maartenbreddels committed Feb 2, 2024
1 parent cf95379 commit 0244da6
Show file tree
Hide file tree
Showing 7 changed files with 575 additions and 355 deletions.
159 changes: 139 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand All @@ -30,7 +40,7 @@ class ConfettiWidget(ipyreact.ValueWidget):
import confetti from "canvas-confetti";
import * as React from "react";
export default function({value, setValue, debug}) {
export default function({value, setValue}) {
return <button onClick={() => confetti() && setValue(value + 1)}>
{value || 0} times confetti
</button>
Expand All @@ -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:
Expand All @@ -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>
);
Expand All @@ -65,7 +77,7 @@ import ipyreact
import pathlib


class ConfettiWidget(ipyreact.ReactWidget):
class ConfettiWidget(ipyreact.ValueWidget):
_esm = pathlib.Path("confetti.tsx")

ConfettiWidget()
Expand All @@ -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:
Expand All @@ -90,7 +104,7 @@ 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, setValue, debug}) {
export default function({value, setValue}) {
return <button onClick={() => confetti() && setValue(value + 1)}>
{value || 0} times confetti
</button>
Expand All @@ -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`:
Expand All @@ -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.

### Built in components

You do not need to provide the module code to create a custom component, 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:

Expand All @@ -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

Expand Down
28 changes: 6 additions & 22 deletions examples/Observe_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "aba8b4d5-199f-4fe0-97a2-5a2a3f43cbf2",
"metadata": {},
"outputs": [],
Expand All @@ -13,26 +13,10 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "b11af66b-f256-4441-8c38-0283866024bd",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "a8bc3645b9e14fc7bfeaa919a8d67d75",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"EvenOddWidget()"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"def check_is_even(number):\n",
" if number %2 == 0:\n",
Expand All @@ -56,8 +40,8 @@
" import confetti from \"canvas-confetti\";\n",
" import * as React from \"react\";\n",
"\n",
" export default function({set_count, debug, count, message}) {\n",
" return <div><button onClick={() => confetti() && set_count(count + 1)}>\n",
" export default function({setCount, count, message}) {\n",
" return <div><button onClick={() => confetti() && setCount(count + 1)}>\n",
" {count} times confetti\n",
" </button>\n",
" <br/>\n",
Expand Down Expand Up @@ -93,7 +77,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
"version": "3.9.16"
}
},
"nbformat": 4,
Expand Down
50 changes: 50 additions & 0 deletions examples/children.ipynb
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
}
Loading

0 comments on commit 0244da6

Please sign in to comment.