Rendering widgets

    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 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.

    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 projects can easily get scaffolded via the .

    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

    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:

    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. });

    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.

    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 . Their signatures, in abstract terms, are:

    • v(tagName | VNode, properties?, children?):
    • w(Widget | constructor, properties, children?)

    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:

    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, {})]);

    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. }

    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:

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

    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 });

    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();

    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 })

    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.

    Available dom() change detection strategies: