diff --git a/ipywidgets_jsonschema/__init__.py b/ipywidgets_jsonschema/__init__.py index c5337b4..f28c533 100644 --- a/ipywidgets_jsonschema/__init__.py +++ b/ipywidgets_jsonschema/__init__.py @@ -1,3 +1,3 @@ from ipywidgets_jsonschema.form import Form -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/ipywidgets_jsonschema/form.py b/ipywidgets_jsonschema/form.py index 0c54c65..9eb5007 100644 --- a/ipywidgets_jsonschema/form.py +++ b/ipywidgets_jsonschema/form.py @@ -19,6 +19,13 @@ class FormError(Exception): ) +def as_tuple(obj): + if isinstance(obj, collections.abc.Iterable) and not isinstance(obj, str): + return obj + else: + return (obj,) + + class Form: def __init__(self, schema, vertically_place_labels=False, use_sliders=False): """Create a form with Jupyter widgets from a JSON schema @@ -51,6 +58,9 @@ def __init__(self, schema, vertically_place_labels=False, use_sliders=False): self.vertically_place_labels = vertically_place_labels self.use_sliders = use_sliders + # Store a list of registered observers to add them to runtime-generated widgets + self._observers = [] + # Construct the widgets self._form_element = self._construct(schema, root=True, label=None) @@ -72,6 +82,7 @@ def construct_element( def observe(self, handler, names=traitlets.All, type="change"): """Register a change handler with all the widgets that support it""" + self._observers.append((handler, names, type)) self._form_element.register_observer(handler, names, type) @property @@ -331,8 +342,15 @@ def add_entry(_): if len(vbox.children) == schema["maxItems"] + 1: return - elements.insert(0, self._construct(schema["items"], label=None)) + newelem = self._construct(schema["items"], label=None) + elements.insert(0, newelem) item = elements[0].widgets[0] + + # Register existing observers + for h, n, t in self._observers: + newelem.register_observer(h, n, t) + + # Add array controls to our new element trash = ipywidgets.Button( icon="trash", layout=ipywidgets.Layout(width="33%") ) @@ -343,6 +361,25 @@ def add_entry(_): icon="arrow-down", layout=ipywidgets.Layout(width="33%") ) + def trigger_observers(): + # Adding or removing an entry to this widget should trigger all value change handlers. + # As we do not have a proper widget to register the handler, we trigger it + # ourselves. This should make proper use of traitlets. + for h, n, t in self._observers: + if t == "change" and (n is traitlets.All or "value" in as_tuple(n)): + h( + { + "name": "value", + "old": {}, + "new": {}, + "owner": None, + "type": "change", + } + ) + + # We trigger observers upon adding + trigger_observers() + def remove_entry(b): # If we are at the specified minimum, remove should be ignored if "minItems" in schema: @@ -358,6 +395,9 @@ def remove_entry(b): vbox.children = vbox.children[:index] + vbox.children[index + 1 :] elements.pop(index) + # We trigger observers upon removing + trigger_observers() + trash.on_click(remove_entry) def move(dir): @@ -455,7 +495,7 @@ def _construct_anyof(self, schema, label=None, key="anyOf"): widget = ipywidgets.VBox([selector] + elements[0].widgets) # Whenever there is a change, we switch the subschema widget - def _select(change): + def _select(_): widget.children = [selector] + elements[names.index(selector.value)].widgets selector.observe(_select)