Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,60 @@ widget = VueTemplate(
)
```

Explicit sync with $emit
------------------------

Every trait of a `VueTemplate` is two-way bound: the template can assign to it
directly and the change is synced back to Python. In addition, the template
can sync a trait explicitly with `$emit("update:<name>", value)` (the same
contract as vue's `.sync` modifier / `v-model`), and send any event listed in
`events` with `$emit("<name>", value)` instead of calling it as a method:

```python
import traitlets
from ipyvue import VueTemplate

class Counter(VueTemplate):
template = traitlets.Unicode('''
<template>
<button @click="$emit('update:count', count + 1)">
clicked {{ count }} times
</button>
</template>
''').tag(sync=True)
count = traitlets.Int(0).tag(sync=True)
```

This style makes the data flow explicit (read the value, emit the change),
matches how pure Vue components are written, and ports cleanly to vue3's
`defineModel()` semantics.

To go fully vue-like, props declared in the template's `<script>` block can be
honored (they are ignored by default, since existing templates rely on that):
matching traits are then passed as one-way vue props — assignment no longer
syncs back (vue warns instead), only `$emit("update:<name>", value)` does.
Enable with `IPYVUE_TEMPLATE_PROPS_SUPPORT=1`, `ipyvue.template_props_support
= True`, or per widget:

```python
class Counter(VueTemplate):
template = traitlets.Unicode('''
<template>
<button @click="$emit('update:count', count + 1)">
clicked {{ count }} times
</button>
</template>
<script>
export default {
props: ["count"],
};
</script>
''').tag(sync=True)
count = traitlets.Int(0).tag(sync=True)

widget = Counter(template_props_support=True)
```

Sponsors
--------

Expand Down
148 changes: 148 additions & 0 deletions examples/EmitSync.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Two ways of syncing: assign to data, or explicit $emit\n",
"\n",
"Every trait of a `VueTemplate` is two-way bound Vue data: the template can\n",
"assign to it directly and the change syncs back to Python."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import traitlets\n",
"from ipyvue import VueTemplate\n",
"\n",
"\n",
"class DataCounter(VueTemplate):\n",
" template = traitlets.Unicode(\n",
" \"\"\"\n",
" <template>\n",
" <button @click=\"count = count + 1\">\n",
" clicked {{ count }} times\n",
" </button>\n",
" </template>\n",
" \"\"\"\n",
" ).tag(sync=True)\n",
" count = traitlets.Int(0).tag(sync=True)\n",
"\n",
"\n",
"data_counter = DataCounter()\n",
"data_counter"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# the assignment in the template synced back to Python\n",
"data_counter.count"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The template can also sync a trait **explicitly** with\n",
"`$emit(\"update:<name>\", value)` (the same contract as vue's `.sync`\n",
"modifier / `v-model`), and send any event listed in `events` with\n",
"`$emit(\"<name>\", value)`.\n",
"\n",
"This style makes the data flow explicit (read the value, emit the change),\n",
"matches how pure Vue components are written, and ports cleanly to vue3's\n",
"`defineModel()` semantics."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class EmitCounter(VueTemplate):\n",
" template = traitlets.Unicode(\n",
" \"\"\"\n",
" <template>\n",
" <button @click=\"$emit('update:count', count + 1)\">\n",
" clicked {{ count }} times\n",
" </button>\n",
" </template>\n",
" \"\"\"\n",
" ).tag(sync=True)\n",
" count = traitlets.Int(0).tag(sync=True)\n",
"\n",
"\n",
"emit_counter = EmitCounter()\n",
"emit_counter"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# the $emit(\"update:count\", ...) synced back to Python,\n",
"# and setting the trait still flows down into the prop\n",
"emit_counter.count = 10"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Fully vue-like: with `template_props_support`, props declared in the\n",
"template's `<script>` block are honored \u2014 the trait arrives as a one-way\n",
"prop, assignment no longer syncs back (vue warns instead), only\n",
"`$emit(\"update:<name>\", value)` does. Off by default because existing\n",
"templates rely on declared props being ignored."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"class PropsCounter(VueTemplate):\n",
" template = traitlets.Unicode('''\n",
" <template>\n",
" <button @click=\"$emit('update:count', count + 1)\">\n",
" clicked {{ count }} times\n",
" </button>\n",
" </template>\n",
" <script>\n",
" export default {\n",
" props: [\"count\"],\n",
" };\n",
" </script>\n",
" ''').tag(sync=True)\n",
" count = traitlets.Int(0).tag(sync=True)\n",
"\n",
"\n",
"props_counter = PropsCounter(template_props_support=True)\n",
"props_counter"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
11 changes: 11 additions & 0 deletions ipyvue/VueComponentRegistry.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import os
from traitlets import Unicode
from ipywidgets import DOMWidget
from ipywidgets.widgets.widget import widget_serialization
from ipywidgets.widgets.widget_layout import Layout
from ipywidgets.widgets.trait_types import InstanceDict
from ._version import semver


class VueComponent(DOMWidget):
# model-only widget (a registry entry): an explicit layout costs a full
# Layout widget (comm_open + close) per registered component, per kernel.
# we can drop this when https://github.com/jupyter-widgets/ipywidgets/pull/3592
# is merged
layout = InstanceDict(Layout, allow_none=True).tag(
sync=True, **widget_serialization
)

_model_name = Unicode("VueComponentModel").tag(sync=True)
_model_module = Unicode("jupyter-vue").tag(sync=True)
_model_module_version = Unicode(semver).tag(sync=True)
Expand Down
22 changes: 22 additions & 0 deletions ipyvue/VueTemplateWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from traitlets import Any, Bool, Unicode, List, Dict, Union, Instance, default
from ipywidgets import DOMWidget
from ipywidgets.widgets.widget import widget_serialization
from ipywidgets.widgets.widget_layout import Layout
from ipywidgets.widgets.trait_types import InstanceDict

from .Template import Template, get_template
from ._version import semver
Expand Down Expand Up @@ -60,6 +62,9 @@ def resolve_ref(value):
else:
getattr(self, "vue_" + event)(data)

def _clear_event_handler(self):
self.on_msg(self._handle_event, remove=True)


def _value_to_json(x, obj):
if inspect.isclass(x):
Expand Down Expand Up @@ -89,6 +94,13 @@ def to_ref_structure(obj, path):


class VueTemplate(DOMWidget, Events):
# like VueWidget: an explicit layout costs a full Layout widget (comm_open
# + close) per template widget; None means "no layout" on the vue side.
# we can drop this when https://github.com/jupyter-widgets/ipywidgets/pull/3592
# is merged
layout = InstanceDict(Layout, allow_none=True).tag(
sync=True, **widget_serialization
)

class_component_serialization = {
"from_json": widget_serialization["to_json"],
Expand Down Expand Up @@ -127,6 +139,12 @@ class VueTemplate(DOMWidget, Events):
def _default_scoped_css_support(self):
return ipyvue.scoped_css_support

template_props_support = Bool(allow_none=False).tag(sync=True)

@default("template_props_support")
def _default_template_props_support(self):
return ipyvue.template_props_support

methods = Unicode(None, allow_none=True).tag(sync=True)

data = Unicode(None, allow_none=True).tag(sync=True)
Expand Down Expand Up @@ -173,5 +191,9 @@ def on_ref_source_change(change):
for traitlet in sync_ref_traitlets:
create_ref_and_observe(traitlet)

def close(self):
self._clear_event_handler()
super().close()


__all__ = ["VueTemplate"]
7 changes: 7 additions & 0 deletions ipyvue/VueWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ def _handle_event(self, _, content, buffers):
data = content.get("data", {})
self._fire_event(event, data)

def _clear_event_handler(self):
self.on_msg(self._handle_event, remove=True)


class VueWidget(DOMWidget, Events):
# we can drop this when https://github.com/jupyter-widgets/ipywidgets/pull/3592
Expand Down Expand Up @@ -192,5 +195,9 @@ def hide(self):

self.class_list.add("d-none")

def close(self):
self._clear_event_handler()
super().close()


__all__ = ["VueWidget"]
8 changes: 8 additions & 0 deletions ipyvue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ def _parse_bool_env(key: str, default: bool = False) -> bool:
# or changed at runtime: ipyvue.scoped_css_support = True
scoped_css_support = _parse_bool_env("IPYVUE_SCOPED_CSS_SUPPORT", False)

# Honor the props declared in a VueTemplate's <script> block: matching traits
# are passed as one-way vue props instead of two-way bound data (sync back
# with $emit("update:<name>", value)). Off by default: templates that declare
# props today rely on them being ignored on the template path.
# Enable with IPYVUE_TEMPLATE_PROPS_SUPPORT=1, at runtime with
# ipyvue.template_props_support = True, or per widget.
template_props_support = _parse_bool_env("IPYVUE_TEMPLATE_PROPS_SUPPORT", False)


def _jupyter_labextension_paths():
return [
Expand Down
1 change: 1 addition & 0 deletions js/src/VueTemplateModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class VueTemplateModel extends DOMWidgetModel {
methods: null,
data: null,
events: null,
template_props_support: false,
_component_instances: null,
},
};
Expand Down
Loading
Loading