Skip to content

Rules of creating standard widgets

Jusong Yu edited this page Aug 30, 2023 · 4 revisions

Motivation

In principle, there is no need to create any widget class to wrap sub-widgets and construct a big one. All the widgets can exist independently with observers and callbacks to interact with other widgets. Then why do we need to bundle things together? Will this only complex thing and set a hurdle for new developers to start? Before mentioning the cliche OOP philosophies, I list the final goal we are heading to:

  1. Less error-prone when using widgets in the app.
  2. Easy to develop new widgets and reduce the chance of making mistakes.
  3. Easy to write unit tests for widgets.

The first one is also what I think is the initial motivation that led to the current situation we have many composite widgets now. As I know, there was a pre-mature stage of the app where widgets were in the notebook, and observers and callbacks were defined here and there in the same notebook. It totally makes sense that developers start from the simple case and build large things from the bottom up. After that, the number of widgets grew and there was a refactoring to move widgets to modules and keep the notebook as simple as possible. At the same time, some fancy widgets were created for the common process of apps which are now some of them is in aiidalab-widgets-base, such as WizardAppWdiget.

When more parameters and logic between parameters were added to the app, especially the configuration step (that is step 2), widgets were distributed in one large step widget and we see a lot that callbacks were far from the place where the widget was initially defined. Then it brings problems since some widgets are interacting with not only one widget/function but multiple of them. Take KpointSettings as an example, it is a sub-widget of the configuration step. It is simple since it has only one value that need to set and read which is kpoint_distance. However, we encounter issues in that the value is not changed or set to the final workflow builder as expected. The reason is that the value depends on the protocol selected in the basic settings, the output is enabled/disabled based on whether we set the value different from the protocol value to trigger the override of the builder. Let alone the fact that there are more interactions, such as resetting the value or such as set the value from reading an existing calculation. The situation becomes quite convoluted with even such a simple case.

Defining a widget can therefore help to set a boundary for the widget with other objects in two aspects, a) It confines all the value changes that happen inside the widget. b) It exposes interfaces to let other widgets know how to interact with this widget. For case "a)" it usually means the interaction between the widget and the real user, the user will see the widget UI and apply changes to it. After the changes, the widget is supposed to make some responses inside it, changing a traitlets or probably triggering some validation. For case "b)", it usually means the interaction between the widget (label "w-A") and another widget (label it as "w-B"). Here another widget w-B can be the widget that wraps w-A or an independent widget of another step for instance.

Rules

When to define a new widget

It is not obvious when to just use the basic widgets from ipywidgets or create a new widget class with more flexible in interacting with other widgets. Rule-1: When a widget (e.g w-A) always appears with another (w-B), and need to interact with widgets from other widget (w-C). Then bind widgets w-A and w-B into a big one. Rule-2: If a widget value is dependent on an outside value to initialize and change, use a new widget to describe such dependency. For example, the kpoint settings although only has one value to change, its value depend on protocol value. Then making it a widget can add flexible logic to set its value based on the changes of protocol. Rule-3: If a widget value need to reset, create a class and define a dedicated reset function to not only reset the widget value, but also the traitlet values if it has.

These three rules make it looks like for all widget, there is need to create a widget class for it. Here is an example of when it is not necessary.

When to use dlink

When to define traitlets interfaces

Example(s)