Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MEP 0003: UI Elements #3

Merged
merged 19 commits into from
Nov 16, 2022
Prev Previous commit
composite elements
  • Loading branch information
akshayka committed Nov 16, 2022
commit 33d3777b1d982dbdaad4c5c5c8352c523a8c5b99
89 changes: 60 additions & 29 deletions mep-0003.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ kernel, and determining which cells are run when an element's value changes.
## Motivation

Marimo provides a pure Python experience for building experiments and
applications using an extensible, modular, and composable programming framework.
UI elements are key to enabling both rapid experimentation and application
development.
applications using an extensible, modular, and composable programming
framework. UI elements are key to enabling both rapid experimentation and
application development.

## Criteria
We strive for a design that is:
Expand Down Expand Up @@ -66,8 +66,10 @@ The `marimo` library will ship with a library of pre-fabricated UI elements.
These elements will be made available through the module `marimo.ui`. Some
examples include a numeric slider, number picker, text box,
checkbox, radio button and radio group, drop-down menu, file upload box, and
button. Additional elements may include data structure elements, like a list,
dictionary, and set.
button. Additional elements may include elements that build a composite
UI element given a template of constituent elements; for example, elements
that take list and dict templates (with list and dict values), and an element
that takes an HTML template.

We defer the design of `marimo.ui` to a future MEP.

Expand Down Expand Up @@ -197,7 +199,7 @@ For example:

> cell a
```python
l = [mo.ui.text()]
l = [mo.ui.text(), mo.ui.number()]
l
```

Expand All @@ -220,13 +222,16 @@ it is possible for `gc.get_referrers()` to miss some objects, if those
objects don't support garbage collection, though in practice this will rarely happen.

Given the extra complexity that traversing pointers would add to the ruleset
and implementation, we have decided against this path. Instead, we will provide
library functions that wrap common data structures. For example, the previous
example involving a list could be rewritten as
and implementation, we have decided against this path.

**Composite elements from templates.**
We will provide library functions that allow users to build a UI element given
a template of constituent UI elements. For example, the previous example
involving a list could be rewritten as

> cell a
```python
l = mo.ui.list([mo.ui.text()])
l = mo.ui.array(template=[mo.ui.text(), mo.ui.number())])
l
```

Expand All @@ -235,34 +240,58 @@ l
l.value[0]
```

Interacting with `l` would now execute `b`, as desired. If this proves
to be too cumbersome or confusing, depending on user feedback, we may consider extending the ruleset to
include unnamed UI elements.
The element `l` is a single UI element built from a template containing a
`text` element and a `number` element, and its output embeds the output of a
`text` and `number` element. The value of `l` is a list with entries equal to
the values its constituent elements.

Interacting with `l` would now execute `b`, as desired.

We say that `l` is built from a template because its embedded elements are
clones of the passed in elements, not the passed in elements itself.
To make this clear, consider

> cell a
```python
t = mo.ui.text()
l = mo.ui.array(template=[t])
l
```

> cell b
```python
t
```

Interacting with `l` in `a` will _not_ run `b`, since `b` does not reference
`l`. In particular, interacting with `l` does not generate an interaction with
`t`, since `t` is not embedded in `l`; instead, an unnamed clone of `t` is in
`l`.

## Frontend Implementation

### The `ui-element` tag
### The `marimo-ui-element` tag

We define a custom HTML tag, `<ui-element>`, for rendering and sychronizing
We define a custom HTML tag, `<marimo-ui-element>`, for rendering and sychronizing
UI elements on the page and with the kernel; this tag is registered as a
custom element. A `<ui-element>` wraps a single container element at
custom element. A `<marimo-ui-element>` wraps a single container element at
which the UI element is rooted.

**Object ID.**
A `ui-element` has a single attribute, `object-id`, which
A `marimo-ui-element` has a single attribute, `object-id`, which
uniquely identifies a `UIElement` object in the Python kernel. There may
be multiple `<ui-element>` tags on the page with the same object id if the same
be multiple `<marimo-ui-element>` tags on the page with the same object id if the same
(Python) element is shown multiple times on the page.

**Communication.** The `ui-element`'s child sends a custom event,
**Communication.** The `marimo-ui-element`'s child sends a custom event,
`marimo-value-input`, when its value has been updated:

```typescript
type marimoValueInputEventType = CustomEvent<{ value: any }>;
```

In response, the `ui-element` component sends a `marimo-value-update` event
to all other `ui-element`s sharing its object id to inform them that their
In response, the `marimo-ui-element` component sends a `marimo-value-update` event
to all other `marimo-ui-element`s sharing its object id to inform them that their
values need to be updated:

```typescript
Expand All @@ -272,27 +301,29 @@ type marimoValueUpdateEventType = CustomEvent<{ value: any }>;
It also sends a message to the kernel, informing it to send a value update
request to the Python kernel. The value is encoded as JSON before it is sent.

#### UI elements as custom web components
#### UI elements as custom elements

We implement the elements provided in `marimo.ui` as custom web components
We implement the elements provided in `marimo.ui` as custom elements
(*e.g.*, `<marimo-slider start="1" stop="10" step="2"></marimo-slider>`)
that render their content in a shadow DOM. This has the following advantages:

- styles are isolated from the editor
- the implementation of the UI element is separate from the editor code
and is framework agnostic
- UI elements are easily composed using slots
- UI elements are easy to create from Python --- we send over
an HTML tag with arguments sent as attributes
- UI elements can be composed using slots
- UI elements are easy to create from Python (just an HTML tag)
- using web components provides a simple path for others to implement
their own UI elements and register them with marimo, though care must be
taken to ensure that no name is registered more than once
their own UI elements and register them with marimo

**Third-party elements.** Users should be able to implement their
own elements by creating their own custom web components, and using marimo's
event model to publish, and subscribe to, value updates. In the future, we
will provide a mechanism for registering third-party elements at runtime.

Care must be taken to avoid name collisions of custom elements. One way to
do this would be to automatically append the third party's package name
as a prefix to the custom element name.

**UI elements are not sandboxed.**
We note that UI elements are _not_ isolated from the main document:
they may read and modify anything on the page, though in the vast majority of
Expand All @@ -312,7 +343,7 @@ attack vectors aren't present.
## Python Implementation
### The Python `UIElement` class

We introduce a `UIElement` class that wraps HTML text in a `<ui-element>` tag
We introduce a `UIElement` class that wraps HTML text in a `<marimo-ui-element>` tag
and registers the element with the kernel. This class implements the
`formatting.MIME` protocol, and its `__mime__` method returns an output of type
`text/html`. Its value is made available through a property called `value`.
Expand Down