Rendering widgets

Dojo is a reactive framework, handling responsibilities of data change propagation and associated rendering updates behind the scenes. Dojo leverages a virtual DOM (VDOM) concept to represent elements intended for output, with nodes in the VDOM being simple JavaScript objects that are designed to be more efficient for developers to work with than actual DOM elements.

Applications only need to concern themselves with declaring their intended output structure as a hierarchy of virtual DOM nodes, typically done as the return values from their widgets’ render functions. The framework’s Renderer component then synchronizes the intended output with concrete elements in the DOM. Virtual DOM nodes also serve to configure and provide state to widgets and elements by passing in properties.

Dojo supports subtree rendering, meaning that when a change in state occurs, the framework is able to determine specific subsets of VDOM nodes affected by the change. Only the required corresponding subtrees within the DOM tree are then updated to reflect the change, increasing rendering performance and improving user interactivity and experience.

Note: Returning virtual nodes from widget render functions is the only concern applications have around rendering. Attempting to use any other practice is considered an anti-pattern in Dojo application development, and should be avoided.

TSX support

Dojo supports use of the jsx syntax extension known as tsx in TypeScript. This syntax allows for a more convenient representation of a widget’s VDOM output that is closer to the resulting HTML within a built application.

TSX-enabled applications

TSX-enabled projects can easily get scaffolded via the dojo create app --tsx CLI command.

For Dojo projects that were not scaffolded in this way, TSX can be enabled with the following additions to the project’s TypeScript config:

./tsconfig.json

  1. {
  2. "compilerOptions": {
  3. "jsx": "react",
  4. "jsxFactory": "tsx"
  5. },
  6. "include": ["./src/**/*.ts", "./src/**/*.tsx", "./tests/**/*.ts", "./tests/**/*.tsx"]
  7. }

TSX widget example

Widgets with a .tsx file extension can output TSX from their render function by simply importing the tsx function from the @dojo/framework/core/vdom module:

src/widgets/MyTsxWidget.tsx

Function-based variant:

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. const factory = create();
  3. export default factory(function MyTsxWidget() {
  4. return <div>Hello from a TSX widget!</div>;
  5. });

Class-based variant:

  1. import WidgetBase from '@dojo/framework/core/WidgetBase';
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. export default class MyTsxWidget extends WidgetBase {
  4. protected render() {
  5. return <div>Hello from a TSX widget!</div>;
  6. }
  7. }

Widgets that need to return multiple top-level TSX nodes can wrap them in a <virtual> container element. This is a clearer option than returning an array of nodes as it allows for more natural automated code formatting within TSX blocks. For example:

src/widgets/MyTsxWidget.tsx

Function-based variant:

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. const factory = create();
  3. export default factory(function MyTsxWidget() {
  4. return (
  5. <virtual>
  6. <div>First top-level widget element</div>
  7. <div>Second top-level widget element</div>
  8. </virtual>
  9. );
  10. });

Working with the VDOM

VDOM node types

Dojo recognizes two types of nodes within its VDOM:

  • VNodes, or Virtual Nodes, which are virtual representations of concrete DOM elements, and serve as the lowest-level rendering output for all Dojo applications.
  • WNodes, or Widget Nodes, which tie Dojo widgets into the VDOM hierarchy.

Both VNodes and WNodes are considered subtypes of DNodes within Dojo’s virtual DOM, but applications don’t typically deal with DNodes in their abstract sense. Using TSX syntax is also preferred as it allows applications to render both virtual node types with uniform syntax.

Instantiating VDOM nodes

If TSX output is not desired, widgets can import one or both of the v() and w() primitives provided by the @dojo/framework/core/vdom module. These create VNodes and WNodes, respectively, and can be used as part of the return value from a widget’s render function. Their signatures, in abstract terms, are:

  • v(tagName | VNode, properties?, children?):
  • w(Widget | constructor, properties, children?)
ArgumentOptionalDescription
tagName | VNodeNoTypically, components will pass in tagName as a string, which identifies the corresponding DOM element tag that will be rendered for the VNode. If another VNode is passed instead, the newly created VNode will act as a copy of the original. If a properties argument is given, the copy will receive a set of merged properties with any duplicates in properties overriding those from the original VNode. If a children argument is passed, it will completely override the original VNode‘s children in the new copy.
Widget | constructorNoTypically, components will pass in Widget as a generic type reference to an imported widget. Several types of constructors can also be passed, allowing Dojo to instantiate widgets in a variety of different ways. These allow for advanced features such as deferred or lazy loading.
propertiesv: Yes, w: NoThe set of properties used to configure the newly created VDOM node. These also allow the framework to determine whether the node has been updated and should therefore be re-rendered.
childrenYesAn array of nodes to render as children of the newly created node. This can also include any text node children as literal strings, if required. Widgets typically encapsulate their own children, so this argument is more likely to be used with v() than w().

Virtual nodes example

The following sample widget includes a more typical render function that returns a VNode. It has an intended structural representation of a simple div DOM element which includes a text child node:

src/widgets/MyWidget.ts

Function-based variant:

  1. import { create, v } from '@dojo/framework/core/vdom';
  2. const factory = create();
  3. export default factory(function MyWidget() {
  4. return v('div', ['Hello, Dojo!']);
  5. });

Class-based variant:

  1. import WidgetBase from '@dojo/framework/core/WidgetBase';
  2. import { v } from '@dojo/framework/core/vdom';
  3. export default class MyWidget extends WidgetBase {
  4. protected render() {
  5. return v('div', ['Hello, Dojo!']);
  6. }
  7. }

Composition example

Similarly, widgets can compose one another using the w() method, and also output several nodes of both types to form a more complex structural hierarchy:

src/widgets/MyComposingWidget.ts

Function-based variant:

  1. import { create, v, w } from '@dojo/framework/core/vdom';
  2. const factory = create();
  3. import MyWidget from './MyWidget';
  4. export default factory(function MyComposingWidget() {
  5. return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
  6. });

Class-based variant:

  1. import WidgetBase from '@dojo/framework/core/WidgetBase';
  2. import { v, w } from '@dojo/framework/core/vdom';
  3. import MyWidget from './MyWidget';
  4. export default class MyComposingWidget extends WidgetBase {
  5. protected render() {
  6. return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
  7. }
  8. }

Rendering to the DOM

Applications provide a render factory function to Dojo’s renderer() primitive, available as the default export from the @dojo/framework/core/vdom module. The provided factory defines the root of an application’s intended VDOM structural output.

Applications typically call renderer() in their main entry point (main.tsx/main.ts), then mount the returned Renderer object to a specific DOM element within the application’s HTML container. If no element is specified when mounting an application, document.body is used by default.

For example:

src/main.tsx

  1. import renderer, { tsx } from '@dojo/framework/core/vdom';
  2. import MyComposingWidget from './widgets/MyComposingWidget';
  3. const r = renderer(() => <MyComposingWidget />);
  4. r.mount();

MountOptions properties

The Renderer.mount() method accepts an optional MountOptions argument that configures how the mount operation gets performed.

PropertyTypeOptionalDescription
syncbooleanYesDefault: false. If true, relevant render lifecycle callbacks (specifically, after and deferred render callbacks) are run synchronously. If false, the callbacks are instead scheduled to run asynchronously before the next repaint via window.requestAnimationFrame(). Synchronous render callbacks can be beneficial in rare scenarios where specific nodes need to exist in the DOM, but this pattern is not recommended for most applications.
domNodeHTMLElementYesA reference to a specific DOM element that the VDOM should be rendered within. Defaults to document.body if not specified.
registryRegistryYesAn optional Registry instance to use across the mounted VDOM.

For example, to mount a Dojo application within a specific DOM element other than document.body:

src/index.html

  1. <!DOCTYPE html>
  2. <html lang="en-us">
  3. <body>
  4. <div>This div is outside the mounted Dojo application.</div>
  5. <div id="my-dojo-app">This div contains the mounted Dojo application.</div>
  6. </body>
  7. </html>

src/main.tsx

  1. import renderer, { tsx } from '@dojo/framework/core/vdom';
  2. import MyComposingWidget from './widgets/MyComposingWidget';
  3. const dojoAppRootElement = document.getElementById('my-dojo-app') || undefined;
  4. const r = renderer(() => <MyComposingWidget />);
  5. r.mount({ domNode: dojoAppRootElement });

Unmounting an application

To fully unmount a Dojo application the renderer provides an unmount API which will remove the DOM nodes and perform any registered destroy operations for all widgets that are current rendered.

  1. const r = renderer(() => <App />);
  2. r.mount();
  3. // To unmount the dojo application
  4. r.unmount();

Adding external DOM nodes into the VDOM

Dojo can wrap external DOM elements, effectively bringing them into the application’s VDOM and using them as part of the render output. This is accomplished with the dom() utility method from the @dojo/framework/core/vdom module. It works similarly to v(), but takes an existing DOM node rather than an element tag string as its primary argument. It returns a VNode which references the DOM node passed into it, rather than a newly created element when using v().

The Dojo application effectively takes ownership of the wrapped DOM node once the VNode returned by dom() has been added to the application’s VDOM. Note that this process only works for nodes external to the Dojo application - either siblings of the element containing the mounted application, or newly-created nodes that are disconnected from the main webpage’s DOM. Wrapping a node that is an ancestor or descendant of the application mount target element will not work.

dom() API

  • dom({ node, attrs = {}, props = {}, on = {}, diffType = 'none', onAttach })
ArgumentOptionalDescription
nodeNoThe external DOM node that should be added to Dojo’s VDOM
attrsYesThe HTML attributes that should be applied to the external DOM node
propsYesThe properties that should be attached to the DOM node
onYesThe set of events to apply to the external DOM node
diffTypeYesDefault: none. The change detection strategy to use when determining if the external DOM node requires updating from within the Dojo application
onAttachYesAn optional callback that is executed after the node has been appended to the DOM

External DOM node change detection

External nodes added through dom() are a step removed from regular virtual DOM nodes as it is possible for them get managed outside of the Dojo application. This means Dojo cannot use the VNode‘s properties as the master state for the element, but instead must rely on the underlying JavaScript properties and HTML attributes on the DOM node itself.

dom() accepts a diffType property that allows users to specify a property change detection strategy for the wrapped node. A particular strategy given the wrapped node’s intended usage can help Dojo to determine if a property or attribute has changed, and therefore needs to be applied to the wrapped DOM node. The default strategy is none, meaning Dojo will simply add the wrapped DOM element as-is within the application’s output on every render cycle.

Note: All strategies use the events from the previous VNode to ensure that they are correctly removed and applied upon each render.

Available dom() change detection strategies:

diffTypeDescription
noneThis mode passes an empty object as the previous attributes and properties within the wrapping VNode, meaning the props and attrs passed to dom() will always be reapplied to the wrapped node in every render cycle.
domThis mode uses the attributes and properties from the DOM node as the base to calculate if there is a difference from the props and attrs passed to dom() that then need to get applied.
vdomThis mode will use the previous VNode for the diff, which is effectively Dojo’s default VDOM diff strategy. Any changes made directly to the wrapped node will get ignored in terms of change detection and rendering updates.