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
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "tsx"
},
"include": ["./src/**/*.ts", "./src/**/*.tsx", "./tests/**/*.ts", "./tests/**/*.tsx"]
}
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:
import { create, tsx } from '@dojo/framework/core/vdom';
const factory = create();
export default factory(function MyTsxWidget() {
return <div>Hello from a TSX widget!</div>;
});
Class-based variant:
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
export default class MyTsxWidget extends WidgetBase {
protected render() {
return <div>Hello from a TSX widget!</div>;
}
}
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:
import { create, tsx } from '@dojo/framework/core/vdom';
const factory = create();
export default factory(function MyTsxWidget() {
return (
<virtual>
<div>First top-level widget element</div>
<div>Second top-level widget element</div>
</virtual>
);
});
Working with the VDOM
VDOM node types
Dojo recognizes two types of nodes within its VDOM:
VNode
s, or Virtual Nodes, which are virtual representations of concrete DOM elements, and serve as the lowest-level rendering output for all Dojo applications.WNode
s, or Widget Nodes, which tie Dojo widgets into the VDOM hierarchy.
Both VNode
s and WNode
s are considered subtypes of DNode
s within Dojo's virtual DOM, but applications don't typically deal with DNode
s 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 VNode
s and WNode
s, 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?)
Argument | Optional | Description |
---|---|---|
tagName | VNode |
No | Typically, 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 | constructor |
No | Typically, components will pass in Widget as a generic type reference to an imported widget. Several types of constructor s 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. |
properties |
v : Yes, w : No |
The 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. |
children |
Yes | An 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:
import { create, v } from '@dojo/framework/core/vdom';
const factory = create();
export default factory(function MyWidget() {
return v('div', ['Hello, Dojo!']);
});
Class-based variant:
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v } from '@dojo/framework/core/vdom';
export default class MyWidget extends WidgetBase {
protected render() {
return v('div', ['Hello, Dojo!']);
}
}
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:
import { create, v, w } from '@dojo/framework/core/vdom';
const factory = create();
import MyWidget from './MyWidget';
export default factory(function MyComposingWidget() {
return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
});
Class-based variant:
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v, w } from '@dojo/framework/core/vdom';
import MyWidget from './MyWidget';
export default class MyComposingWidget extends WidgetBase {
protected render() {
return v('div', ['This widget outputs several virtual nodes in a hierarchy', w(MyWidget, {})]);
}
}
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
import renderer, { tsx } from '@dojo/framework/core/vdom';
import MyComposingWidget from './widgets/MyComposingWidget';
const r = renderer(() => <MyComposingWidget />);
r.mount();
MountOptions
properties
The Renderer.mount()
method accepts an optional MountOptions
argument that configures how the mount operation gets performed.
Property | Type | Optional | Description |
---|---|---|---|
sync |
boolean |
Yes | Default: 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. |
domNode |
HTMLElement |
Yes | A reference to a specific DOM element that the VDOM should be rendered within. Defaults to document.body if not specified. |
registry |
Registry |
Yes | An 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
<!DOCTYPE html>
<html lang="en-us">
<body>
<div>This div is outside the mounted Dojo application.</div>
<div id="my-dojo-app">This div contains the mounted Dojo application.</div>
</body>
</html>
src/main.tsx
import renderer, { tsx } from '@dojo/framework/core/vdom';
import MyComposingWidget from './widgets/MyComposingWidget';
const dojoAppRootElement = document.getElementById('my-dojo-app') || undefined;
const r = renderer(() => <MyComposingWidget />);
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.
const r = renderer(() => <App />);
r.mount();
// To unmount the dojo application
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 })
Argument | Optional | Description |
---|---|---|
node |
No | The external DOM node that should be added to Dojo's VDOM |
attrs |
Yes | The HTML attributes that should be applied to the external DOM node |
props |
Yes | The properties that should be attached to the DOM node |
on |
Yes | The set of events to apply to the external DOM node |
diffType |
Yes | Default: none . The change detection strategy to use when determining if the external DOM node requires updating from within the Dojo application |
onAttach |
Yes | An 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:
diffType |
Description |
---|---|
none |
This 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. |
dom |
This 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. |
vdom |
This 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. |