Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Widget Concepts

Kitson Kelly edited this page Sep 2, 2017 · 30 revisions

What is a widget?

At the conceptual level, a widget is a user interface component which renders a virtual DOM structure. They take properties and children and interpret those to render a structure.

The other duty of a widget is to encapsulate user interactions with its virtual DOM and expose them externally as higher order event listeners which can be set via the widget properties.

Overview of Widget

graph_003 @startuml

participant Application as A; participant Widget as W; participant "Virtual DOM" as V; participant "Browser" as B; actor User;

A -> W: properties; A -> W: children; W -> V: .render(); V -> B: ; B -> User; User -> B: click; B -> W: onclick(); W -> A: onClick();

@enduml graph_003

The @dojo/widget-core provides a framework that manages widgets, the virtual DOM, and other interactions with the DOM. Widget classes should all extend originally from WidgetBase. From a widget author's perspective, the main focus is on overridding the WidgetBase.render() method. The main purpose of this method is to take the .children and .properties that have been set on the widget at runtime and translating into a virtual DOM structure, which becomes the return of the .render() method. Widget renders are constructed of two main types of objects, virtual DOM nodes and other widgets. Conceptually, even if a widget is constructed from other widgets, it should be designed in a way that fully encapsulates those widgets.

For a widget author, when a user interacts with a widget, there are two types of interaction, and therefore two ways this should be conceptually dealt with:

  • The user action is transitional or ephemeral and does not affect the external state of the widget. For example, typing of keys up until a user indicates that they are ready to proceed. These transitional states should be managed fully within the widget instance and not leak outside.
  • The user action is something that is an external concern. For example, clicking a submit type of button. In this case, the widget should expose a higher order listener (e.g. .onSubmit) but take no further action. It should expect that some external actor (the application) will adjust the properties on the widget (e.g. { invalid: true }) which will cause the widget to be invalidated and when the next render is calculated, the .render() function would render an invalid state. The widget should fire and forget.

This sort of reactive type of programming is designed to reduce the interdependency between different parts of the application. This means it becomes far easier to make a change without having an unexpected regression in another part of the application. It also increases testability, by knowing that for any given input, there is a definitive expected output.

Well designed widgets do not modify their own properties. They expect another party to decide what changes to the properties are appropriate. Well designed widgets also keep internal state to a bare minimum, just enough to ensure that transitional interactions with the user are able to be accomplished.

How do Dojo 2 Widgets compare to Dojo 1 Dijits?

They are fundamentally different. For various reasons, Dijits had an involved lifecycle. That was one of the challenges that was felt needed to be addressed with the Dojo 2 widgeting system. The lifecycle was focused on the creation of the underlying DOM structure. In Dojo 2 widgets, there is a very minimalistic lifecycle, in which as a widget developer, when and how a widget is instantiated (created) is not really a consideration. Widgets come in and out of existence as needed, which is managed lazily by the widgeting system. A widget should focus on interpreting its properties and children.

Dijits were very tightly coupled to their DOM nodes and had an optional templating system that stitched together properties on the Dijit instance and the DOM. Widgets on the other hand fully express their virtual DOM structure on every render, which is then translated into a set of changes which are applied to the real DOM. Because of this, widgets shouldn't concern themselves with their real DOM nodes and they know everything about their structure, because their .render() function fully expresses it.

Dijits evolved over a number of years as JavaScript and browsers evolved. This meant there were many patterns that evolved to set properties, manage state and even do basic DOM manipulation. There were also significant amounts of runtime checking to try to prevent developers from mis-using a Dijit. This was often confusing to a Dijit author. With Dojo 2 widgets, a significantly smaller API surface, embracing ES6+ standards and type checking with TypeScript make widgets far easier to develop with far more predictable and performant outcomes.

Dijits were instantiated (created) either programatically (e.g. var button = new Button()) or declaratively (e.g. using the dojo/parser). If they were created programatically, they would also need to be placed somewhere in the DOM structure. There was a centralized registry of all Dijits which you could reference by ID, which may or may not have matched the DOM element's ID. Dojo 2 widgets on the other hand are instantiated lazily by the widgeting system. A widget developer does not have to worry about creating widgets, just describing their properties and children. Also, placement is part of the overall declaration of the widget node placement in the virtual DOM. Widgets are just part of the overall virtual DOM structure, and are only relative and known to their parent. It is intentional that there are few ways to gain a reference to a widget instances, as this would likely lead to anti-patterns that would cause issues with the overall widget system. (Note: at this point, we are still investigating debugging where being able to have a development tool that allows a developer to more easily debug rendered widgets is a consideration.)

[TODO: We need to refresh our Dijit wrapper to give developers a level of comfort that they won't have to throw out all their custom Dijits]

How do widgets get created and destroyed?

Is it best to think of the virtual DOM as a tree of descriptor nodes, which provide information to the engine to determine what the DOM should look like. The virtual DOM engine determines efficiently the changes it needs to make by walking down from the root of the virtual DOM Projector. The virtual DOM system tries to be efficient as possible, which means that it only instantiates (creates) widgets when they are actually part of the DOM. This is sometimes called lazy instantiation. This means as a widget developer, you are not sure when and how your widgets will get created. Also, the widget's .render() function only gets called when the widget system knows there might be a change that could affect the output of .render(). The main focus of the widget developer is to respond properly when requested. This means that when and how instances of your widget get created aren't important.

Using the w() function actually just describes the widget versus actually instantiating the widget. The widgeting system takes a look at the widget class, the properties and the children and determines what it needs to do if that widget is going to be rendered. If it doesn't have a specific instance it will create one. It will then inform the existing instance, or the new one, of its properties and children. Part of WidgetBase will efficiently determine if the .properties or .children have changed in a way that is likely to invalidate the last render of the widget. If that is the case, the widget will invalidate itself and subsequently the .render() method on the widget will be called in the correct order as part of the virtual DOM. All of this behavior is automatic to the widgeting system and not something the widget author has to concern themselves with.

Just as widgets get instantiated lazily, they also get destroyed or disposed of lazily. The widget system provides a higher order garbage collection of disconnected widgets that are no longer part of the virtual DOM. The only thing that a widget developer needs to worry about is leveraging the Dojo 2 Destroyable functionality and having the instance .own() any destruction handles it might need for cleanup.

Virtual DOM

The concept of the virtual DOM is one where a developer doesn't directly interact with the DOM, but an abstraction layer converts representations of the DOM into actual DOM elements. This concept provides several benefits:

  • Provides an easier to learn and consistent API for interacting with the DOM.
  • Allows changes to the DOM to be batched up and reduced to only the necessary changes, providing a better end user experience.

The way the virtual DOM manages the DOM is by introducing the concept of a projector. You can think of this as an overhead projector that is put on a wall with the virtual DOM engine going along and tracing out the DOM on the wall that is being projected. Every time the projector renders, the engine looks for differences from the last render and then applies those to the DOM.

In order to be efficient, the virtual DOM has some intentional constraints. First, it only pays attention to changes from its previous render. That means if someone were to manipulate the DOM directly, the virtual DOM wouldn't see any of those changes.

Another constraint is that if you have two nodes that are peers of each other and you make changes to them, if they happen to be the same tag, and cannot be uniquely identified, the virtual DOM may not be able to differentiate between them. For example, if you had <div>foo</div><div>bar</div> and the next render you express just <div>qat</div> were you trying to remove both nodes and replace it with a new node? Were you deleting the second node and changing the text on the first node? Or maybe it was the opposite and you were deleting the first node and changing the text on the second one? In many cases, it doesn't matter, but if it does there are ways to deal with this described below.

The last constraint listeners/event handlers cannot be added or changed between renders. This would mean that on every render, the virtual DOM engine would have to remove and re-add the listener, because it would be a different reference to the listener. Usually this is unintentional by the developer, caused by generating the listener within the virtual DOM node, instead of passing in a reference that is created outside of the node.

While the Dojo 2 widgeting system uses a virtual DOM engine named Maquette the system abstracts the developer from the implementation details. There were two main reasons why this was felt to be a good idea:

  • It isolated the widgeting system from the virtual DOM implementation, meaning if required in the future, the engine could be changed without impacting the widgeting system.
  • Give the end developer an abstraction that closer aligned to the way the widgeting system worked.

Virtual DOM Creation Functions

There are two core virtual DOM creation functions, v() and w().

v() is the function to express normal HTML elements in the DOM. It returns an object called an HNode (HTML node). There are up to three arguments that can be passed to v():

  • selector - This is similar to a CSS selector, a string which describes the HTML node. Typically, the element's tag name is specified (e.g. div would generate a <div></div> node and span would generate a <span></span>). Also the . and # are recognised. . is like CSS, and sets a class on the node (e.g. div.foo.bar would generate a <div class="foo bar"></div>). # is also like CSS, and sets the ID of the node (e.g. div#foo would generate a <div id="foo"></div>). Using . and # have some disadvantages, as changes to this string are not applied to the node after it is created. Therefore it is recommended that setting these in properties are better.
  • properties - This is an object which specifies properties for the node. Most of these properties are passed through to the DOM node, though there are some that have special meaning, the most notable is key. Because the virtual DOM is fluid and by design the engine cannot tell the difference between a sibling that has the same virtual DOM selector, it cannot always determine which node is being changed. The key property allows labelling of peers. It can also be used in other situations, like testing, to try to identify parts of the virtual DOM.
  • children - This is an array of other virtual DOM, which can add v() children, w() children, text for the node as a string or an empty placeholder of null.

w() is very similar to v() except instead of describing a HTML element, it describes a widget. It returns what is called a WNode (a widget node). It also takes up to three arguments:

  • widgetClass - This is either a widget constructor/class, string, or a symbol. When using a string or symbol, the constructor/class is looked up in the widget registry that the enclosing widget/projector is aware of.
  • properties - These are properties that will be set on the widget once instantiated. It is up to the implementation of the widget to interpret these properties in order to affect its render or behavior.
  • children - These are children that will be set on the widget once instantiated. How the children are represented in the render is up to the implementation of the widget, though typically, there will be part of the rendered virtual DOM that will contain these as an array of children.

w() does not directly instantiate/create the widget. A widget is lazily instantiated, when a projector needs to project it onto the DOM.

Important properties

There are several properties are important when dealing with widgets and virtual DOM nodes.

The key property is used for two purposes by the widgeting system. The first is that a key is used when rendering the virtual DOM to disambiguate siblings that appear to be the same type. For example if you have two <div> elements as siblings of each other and on the first render, one has a class of foo and the other has the class of bar, but on the second render, there is only one <div> with the class of baz, the rendering engine can't tell which node was removed and which node was changed. This is where key provides that information to the rendering engine to determine which node is which and make the most efficient mutations to the DOM.

The other purpose is that when referencing parts of the virtual DOM, several parts of the widgeting system use the key as an identifier (e.g. the Meta system described below). In all these cases though, the key is essentially scoped to the widget instance, so it only needs to be unique within the widget class.

Lifecycle

Because widgets are reactive and instantiated lazily, there is no over arching lifecycle. The basic concept boils down to properties and children in, render out. There are some subtleties and flexibility to allow advanced use cases.

In order to create the widgeting system, there are some public methods on widget core that are used to create this system, but are not points where a widget author should ever override or overload functionality. The widgeting system provides a set of decorators that can be used to further customize the behavior of a widget without overriding methods.

Setting Properties

As the main function of a widget is rendering its properties, setting properties is a core part of the widget's lifecycle. The concept is reactive. A widget should care about what a property's value is or take action if a property has changed.

Outside of setting the value of .properties, the main intent of the lifecycle is to track and calculate differences in properties so that implementors can take particular actions when certain properties change. The overall process of setting properties is diagrammed below:

Setting Properties Lifecycle

graph_002 digraph G {
".setProperties()" [shape=box, style=filled];
"@diffProperties" [shape=box, style=dotted];
".invalidate()" [shape=box];
".setProperties()" -> "@diffProperties";
"@diffProperties" -> ".setProperties()";
".setProperties()" -> ".properties";
".setProperties()" -> ".invalidate()";
".invalidate()" -> ".setProperties()"

}

graph_002

The typical way properties are set on a widget is through the w() function and as part of the render process. Enclosing widgets, during render, propagate the properties the child widget needs to know about to respond properly. At the very top level, the Projector has a public method named .setProperties() which is designed to provide a projection the appropriate properties to propagate down into the rest of the projection.

There is also the concept of state injection [TBC in other section]

By default, the incoming properties and the current properties are diffed using a strategy that ignores functions, does a shallow compare of objects, and a reference compare to all other values. Diff strategies can be customized at the property level by using the @diffProperty decorator. You may have to customize, or provide your own, diffing strategy to deal with complex property types.

Normally, the widget does not receive feedback when it's properties change, the widget simply re-renders. By applying the @diffProperty decorator on an instance method, the widget can be notified when a property changes.

Setting Children

Outside of a widget's properties, its children is the other main data that is unique to the instance which a widget has to respond to. Usually widgets will have their children as part of the render, and so setting the children is part of the lifecycle. Like properties, a widget shouldn't be concerned about what its children are but should be concerned about rendering them.

Unlike properties, there is little complexity to setting children, which is almost exclusively done via the w() functions. The Projector does have a public .setChildren() method which allows the top level children to be set. Setting children also implicitly calls .invalidate() just like setting properties, which will cause the render cycle to occur.

Invalidation and Rendering

The .invalidate() method on a widget does two things. It lets the widget know it is in a dirty state and that it should, when asked, recalculate its render. The widgeting system also bubbles this up to the enclosing widget, so that it is also marked as dirty. This continues until it reaches the enclosing Projector. When it reaches the Projector, it will request a render from each of its children. The widgeting system provides an automatic cache system so only those widgets which are dirty will actually have their .render() function called.

Invalidation and Rendering Lifecycle

graph_001 digraph G {
size ="4,4";
".invalidate()" [shape=box];
"@beforeRender" [shape=box, style=dotted];
".render()" [shape=box];
"@afterRender" [shape=box, style=dotted];
".invalidate()" -> "@beforeRender";
"@beforeRender" -> ".invalidate()"
".invalidate()" -> ".render()"
".render()" -> ".invalidate()"
".invalidate()" -> "@afterRender"
"@afterRender" -> ".invalidate()"

}

graph_001

The .invalidate() method is called automatically when when the widget's properties or children have changed.

The main focus for a widget developer is on the .render() method. For advanced use cases there are the @beforeRender and @afterRender decorators which can be used to further augment the render cycle.

.render()

.render() is the main method that makes a class of widget unique. It is the method that widget developers should override. The method takes no arguments and must return a single DNode. A DNode is a virtual DOM abstraction of a DOM node. There are four valid types of DNodes:

  • string - A string of text which is the text content of the enclosing DOM node.
  • null - A placeholder for a virtual DOM node. Intended to make it easier to deal with children slots when there is no child to currently include in the render.
  • HNode - A node that represents an HTML element. These are typically generated by the v() function.
  • WNode - A node that represents a widget. These are typically generated by the w() function.

A widget developer would look to express the widgets DOM by taking the .properties and .children of the widget and generating a virtual DOM structure from them.

Meta

Some operations can only be performed with an actual DOM node, as opposed to a v node, such as getting the on-page dimensions of an element, or using intersection observers. The widget system provides access to DOM node and DOM node-dependent properties using widget meta. A few meta are provided out of the box:

  • Dimensions - Provide dimension information about a v node. The node's size, position, and scroll properties are returned.
  • Intersection - Set up an Intersection Observer for a node and have the widget invalidated when the intersection values change.

Note that meta values are handled synchronously, so it is possible that you will receive a default value if the node you are requesting Dimensions for does not exist. Synchronous meta calls help keep your render() methods clean of unwanted async side-effects.

If the built-in meta does not provide what you need, it is easy to create your own meta implementations.

Destruction

Widgets that are no longer referenced through another widget's render method, and are thus detached from the virtual node system, are automatically destroyed. During the destruction process, any handles added with .own will be destroyed.

Event Handling

In Dojo 1, Dijits would emit events using DOM events. In Dojo 2, events are not treated specially, and instead are simply reported using callback properties. Instead of listening for a click event on a button, you would add an onClick: this.buttonClicked property while creating the w node for the button. The buttonClicked method in your widget would then fire when the button was clicked.

Classes

During the development of the Dojo 2 widgeting system, it was felt that it was a requirement to have an easy way for a widget author to ensure that CSS that was required for a widget to operate properly was coupled with the widget. It was also a desire to take advantage of the intellisense capabilities that can be provided by TypeScript to ensure that developers were authoring their widgets correctly.

Other frameworks have embraced CSS in JavaScript. It is perceived this is an advantage because the developer doesn't have to worry about ensuring the CSS they depend on is included in a final build. It is felt by the Dojo 2 team that this causes an unacceptable sacrifice of user experience in lieu of developer ergonomics. Having JavaScript inject inline styles at runtime is always less performant than CSS available as classes in the current environment. It also makes things like progressive enhancement near impossible, because you have to have your full JavaScript application loaded before you can provide a user with a usable interface.

Dojo 2 widgets embraces CSS Modules. The team feels this provides the best solution to keep widget code connected with its CSS, ensure that widget CSS does not conflict with other classes or styles, and there is a mechanism to ensure that required CSS can be optimally built. In addition, using a little bit of tooling, TypeScript definition files can be generated for the CSS modules so that a widget developer simply imports the CSS they require and get intellisense and code completion in their editor. This whole system helps ensure that widgets express their CSS dependencies as they would other JavaScript modules, while at the same time, authoring their CSS as they would normally.

The tooling for Dojo 2 incorporates CSS Next as well. This means that widget authors focus on writing modern, modular CSS, that will work on their target browsers and included optimally in their build without sacrificing performance or end user experience.

Theming

[TBC]

Animations

[TBC]