Skip to content

Commit 274ef3f

Browse files
feat: add support for components as classes (#3)
1 parent 5bde53b commit 274ef3f

7 files changed

+448
-26
lines changed

class_components.ipynb

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"metadata": {},
7+
"outputs": [],
8+
"source": [
9+
"import ipywidgets as widgets\n",
10+
"import ipyvue as vue\n",
11+
"from traitlets import (Dict, Unicode, List, Instance, observe)"
12+
]
13+
},
14+
{
15+
"cell_type": "code",
16+
"execution_count": null,
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"class SubComponent(vue.VueTemplate):\n",
21+
" input = Unicode().tag(sync=True)\n",
22+
" something = Unicode('defaultvalue').tag(sync=True)\n",
23+
" template = Unicode('''\n",
24+
" <div style=\"border: 1px solid green; margin: 2px\" @click=\"append_one()\">\n",
25+
" [{{ something }}] input: {{ input }} \n",
26+
" </div>\n",
27+
" ''').tag(sync=True)\n",
28+
" \n",
29+
" def vue_append_one(self, *args):\n",
30+
" self.input = f'{self.input}1'\n",
31+
" \n",
32+
"SubComponent(input='some text') "
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"metadata": {
39+
"scrolled": true
40+
},
41+
"outputs": [],
42+
"source": [
43+
"class MainComponent(vue.VueTemplate):\n",
44+
" texts = List(['xxxx', 'yyyy']).tag(sync=True)\n",
45+
" direct = Unicode('aaa').tag(sync=True)\n",
46+
" template = Unicode('''\n",
47+
" <div>\n",
48+
" <div style=\"border: 1px solid black\">\n",
49+
" <sub-component v-for=\"t in texts\" :input=\"t\" :key=\"t\" />\n",
50+
" </div>\n",
51+
" <sub-component :input=\"direct\" something=\"---\"/>\n",
52+
" <w-button v-for=\"t in texts\" :description=\"t\" :key=\"t\" />\n",
53+
" <w-button description=\"no v-for\"/>\n",
54+
" </div>\n",
55+
" ''').tag(sync=True)\n",
56+
" \n",
57+
" components=Dict({\n",
58+
" 'sub-component': SubComponent,\n",
59+
" 'w-button': widgets.Button\n",
60+
" }).tag(sync=True, **vue.VueTemplate.class_component_serialization)\n",
61+
"\n",
62+
"mainComponent = MainComponent()\n",
63+
"mainComponent"
64+
]
65+
},
66+
{
67+
"cell_type": "code",
68+
"execution_count": null,
69+
"metadata": {},
70+
"outputs": [],
71+
"source": [
72+
"mainComponent.texts=['xxxx', 'zzzz']"
73+
]
74+
},
75+
{
76+
"cell_type": "code",
77+
"execution_count": null,
78+
"metadata": {},
79+
"outputs": [],
80+
"source": [
81+
"mainComponent._component_instances"
82+
]
83+
},
84+
{
85+
"cell_type": "code",
86+
"execution_count": null,
87+
"metadata": {},
88+
"outputs": [],
89+
"source": [
90+
"mainComponent.direct = 'bbb'"
91+
]
92+
},
93+
{
94+
"cell_type": "markdown",
95+
"metadata": {},
96+
"source": [
97+
"# Non serializable properties"
98+
]
99+
},
100+
{
101+
"cell_type": "code",
102+
"execution_count": null,
103+
"metadata": {},
104+
"outputs": [],
105+
"source": [
106+
"class Database():\n",
107+
" def __init__(self, url):\n",
108+
" self.url = url\n",
109+
"\n",
110+
"class DatabaseInfo(vue.VueTemplate):\n",
111+
" db = Instance(Database)\n",
112+
" info = Unicode().tag(sync=True)\n",
113+
" label = Unicode().tag(sync=True)\n",
114+
" \n",
115+
" template = Unicode('''\n",
116+
" <div style=\"border: 1px solid green; margin: 2px\">\n",
117+
" [{{ label }}] Db URL: {{ info }}\n",
118+
" </div>\n",
119+
" ''').tag(sync=True)\n",
120+
" \n",
121+
" @observe('db')\n",
122+
" def db_changed(self, change):\n",
123+
" self.info = self.db.url"
124+
]
125+
},
126+
{
127+
"cell_type": "code",
128+
"execution_count": null,
129+
"metadata": {},
130+
"outputs": [],
131+
"source": [
132+
"class MyApp(vue.VueTemplate):\n",
133+
" \n",
134+
" customer_db = Instance(Database).tag(sync_ref=True)\n",
135+
" supplier_db_collection = Dict().tag(sync_ref=True)\n",
136+
" \n",
137+
" template = Unicode('''\n",
138+
" <div>\n",
139+
" <database-info label=\"customers\" :db=\"customer_db\" />\n",
140+
" \n",
141+
" <database-info\n",
142+
" label=\"supplier\"\n",
143+
" v-for=\"supplier in supplier_db_collection.preferred\"\n",
144+
" :db=\"supplier\"\n",
145+
" :key=\"supplier.id\" />\n",
146+
" \n",
147+
" <database-info label=\"function\" :db=\"{functionRef: 'db_factory', args: ['localhost/function1']}\" />\n",
148+
" </div>\n",
149+
" ''').tag(sync=True)\n",
150+
" \n",
151+
" components = Dict({\n",
152+
" 'database-info': DatabaseInfo\n",
153+
" }).tag(sync=True, **vue.VueTemplate.class_component_serialization)\n",
154+
" \n",
155+
" def db_factory(self, url):\n",
156+
" return Database(url)\n",
157+
"\n",
158+
"my_app = MyApp(\n",
159+
" customer_db = Database('localhost/customers'),\n",
160+
" supplier_db_collection = {'preferred': [Database('localhost/intel')]}\n",
161+
")\n",
162+
"my_app "
163+
]
164+
},
165+
{
166+
"cell_type": "code",
167+
"execution_count": null,
168+
"metadata": {},
169+
"outputs": [],
170+
"source": [
171+
"my_app.customer_db = Database('remote/customers_v2')"
172+
]
173+
},
174+
{
175+
"cell_type": "code",
176+
"execution_count": null,
177+
"metadata": {},
178+
"outputs": [],
179+
"source": [
180+
"my_app.supplier_db_collection = {'preferred': [Database('remote/amd')]}"
181+
]
182+
},
183+
{
184+
"cell_type": "code",
185+
"execution_count": null,
186+
"metadata": {},
187+
"outputs": [],
188+
"source": []
189+
}
190+
],
191+
"metadata": {
192+
"kernelspec": {
193+
"display_name": "Python 3",
194+
"language": "python",
195+
"name": "python3"
196+
},
197+
"language_info": {
198+
"codemirror_mode": {
199+
"name": "ipython",
200+
"version": 3
201+
},
202+
"file_extension": ".py",
203+
"mimetype": "text/x-python",
204+
"name": "python",
205+
"nbconvert_exporter": "python",
206+
"pygments_lexer": "ipython3",
207+
"version": "3.7.3"
208+
}
209+
},
210+
"nbformat": 4,
211+
"nbformat_minor": 2
212+
}

ipyvue/VueTemplateWidget.py

+93-4
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,88 @@
33
from ipywidgets.widgets.widget import widget_serialization
44
from ._version import semver
55
from .ForceLoad import force_load_instance
6+
import inspect
7+
from importlib import import_module
68

9+
OBJECT_REF = 'objectRef'
10+
FUNCTION_REF = 'functionRef'
711

812
class Events(object):
913
def __init__(self, **kwargs):
1014
self.on_msg(self._handle_event)
1115
self.events = [item[4:] for item in dir(self) if item.startswith("vue_")]
1216

1317
def _handle_event(self, _, content, buffers):
14-
event = content.get("event", "")
15-
data = content.get("data", {})
16-
getattr(self, 'vue_' + event)(data)
18+
def resolve_ref(value):
19+
if isinstance(value, dict):
20+
if OBJECT_REF in value.keys():
21+
obj = getattr(self, value[OBJECT_REF])
22+
for path_item in value.get('path', []):
23+
obj = obj[path_item]
24+
return obj
25+
if FUNCTION_REF in value.keys():
26+
fn = getattr(self, value[FUNCTION_REF])
27+
args = value.get('args', [])
28+
kwargs = value.get('kwargs', {})
29+
return fn(*args, **kwargs)
30+
return value
31+
32+
if 'create_widget' in content.keys():
33+
module_name = content['create_widget'][0]
34+
class_name = content['create_widget'][1]
35+
props = {k: resolve_ref(v) for k, v in content['props'].items()}
36+
module = import_module(module_name)
37+
widget = getattr(module, class_name)(**props, model_id=content['id'])
38+
self._component_instances = [*self._component_instances, widget]
39+
elif 'update_ref' in content.keys():
40+
widget = DOMWidget.widgets[content['id']]
41+
prop = content['prop']
42+
obj = resolve_ref(content['update_ref'])
43+
setattr(widget, prop, obj)
44+
elif 'destroy_widget' in content.keys():
45+
self._component_instances = [w for w in self._component_instances
46+
if w.model_id != content['destroy_widget']]
47+
elif 'event' in content.keys():
48+
event = content.get("event", "")
49+
data = content.get("data", {})
50+
getattr(self, 'vue_' + event)(data)
51+
52+
53+
def _value_to_json(x, obj):
54+
if inspect.isclass(x):
55+
return {
56+
'class': [x.__module__, x.__name__],
57+
'props': x.class_trait_names()
58+
}
59+
return widget_serialization['to_json'](x, obj)
60+
61+
62+
def _class_to_json(x, obj):
63+
if not x:
64+
return widget_serialization['to_json'](x, obj)
65+
return {k: _value_to_json(v, obj) for k, v in x.items()}
66+
67+
68+
def as_refs(name, data):
69+
def to_ref_structure(obj, path):
70+
if isinstance(obj, list):
71+
return [to_ref_structure(item, [*path, index]) for index, item in enumerate(obj)]
72+
if isinstance(obj, dict):
73+
return {k: to_ref_structure(v, [*path, k]) for k, v in obj.items()}
74+
75+
# add object id to detect a new object in the same structure
76+
return {OBJECT_REF: name, 'path': path, 'id': id(obj)}
77+
78+
return to_ref_structure(data, [])
1779

1880

1981
class VueTemplate(DOMWidget, Events):
2082

83+
class_component_serialization = {
84+
'from_json': widget_serialization['to_json'],
85+
'to_json': _class_to_json
86+
}
87+
2188
# Force the loading of jupyter-vue before dependent extensions when in a static context (embed,
2289
# voila)
2390
_jupyter_vue = Any(force_load_instance, read_only=True).tag(sync=True, **widget_serialization)
@@ -44,7 +111,29 @@ class VueTemplate(DOMWidget, Events):
44111

45112
events = List(Unicode(), default_value=None, allow_none=True).tag(sync=True)
46113

47-
components = Dict(default_value=None, allow_none=True).tag(sync=True, **widget_serialization)
114+
components = Dict(default_value=None, allow_none=True).tag(
115+
sync=True, **class_component_serialization)
116+
117+
_component_instances = List().tag(sync=True, **widget_serialization)
118+
119+
def __init__(self, *args, **kwargs):
120+
super().__init__(*args, **kwargs)
121+
122+
sync_ref_traitlets = [v for k, v in self.traits().items()
123+
if 'sync_ref' in v.metadata.keys()]
124+
125+
def create_ref_and_observe(traitlet):
126+
data = traitlet.get(self)
127+
ref_name = traitlet.name + '_ref'
128+
self.add_traits(**{ref_name: Any(as_refs(traitlet.name, data)).tag(sync=True)})
129+
130+
def on_ref_source_change(change):
131+
setattr(self, ref_name, as_refs(traitlet.name, change['new']))
132+
133+
self.observe(on_ref_source_change, traitlet.name)
134+
135+
for traitlet in sync_ref_traitlets:
136+
create_ref_and_observe(traitlet)
48137

49138

50139
__all__ = ['VueTemplate']

0 commit comments

Comments
 (0)