Components of a Dojo application
Overview
In this tutorial, you will learn about the structure of a simple Dojo application and the purpose of each part of the application. This will not be a comprehensive discussion about all of the possible parts of a Dojo application. We will focus on the minimum application that we started with in Your first Dojo application.
Prerequisites
You can open the tutorial on codesandbox.io or download the demo project and run npm install
to get started.
The @dojo/cli
command line tool should be installed globally. Refer to the Dojo local installation article for more information.
You also need to be familiar with TypeScript as Dojo uses it extensively.
The main HTML document
HTML pages are the foundation for every web application and Dojo applications are no different. In the sample application, the index.html
file serves this role. Notice that the <body>
tag contains a single element: <my-app>
. While there is nothing special about this element, the application is using this node to determine where to place the Dojo application on the page. Everything that the application does should be contained within this single element. There are several benefits to this approach:
- A Dojo application can easily coexist on a page with other content.
- That content can consist of static assets, a legacy application or even another Dojo application.
Mounting Your Dojo Application
In Your first Dojo application, we reviewed Dojo’s use of a virtual DOM to provide an abstraction between the application and the rendered page. To actually render your dojo application, we use the renderer
function from the vdom
module. The renderer
accepts a function that returns virtual DOM (generated by either the w()
or v()
pragma, more on these later!).
Review these lines in main.ts
:
const r = renderer(() =>
v('h1', { title: 'I am a title!' }, [ 'Biz-E-Bodies' ])
);
These lines are the key to allowing the projector to coordinate between the virtual DOM and the rendered HTML that the user sees. The final step is to call mount
on the value returned from the renderer
call. By default the renderer
will mount the application on the HTML documents body, but this can be overridden by providing a domNode
option to the mount
function.
The mount
operation in main.ts
:
r.mount({ domNode: document.querySelector('my-app') as HTMLElement });
Widgets
Until now, we have returned a virtual DOM that represents HTML elements directly from the renderer
, however, that would not scale as an application grows.
This is where widgets are beneficial, as widgets are Dojo’s basic building blocks for user interfaces. They combine both the visual and behavioral aspects of a component into a single element. These aspects are encapsulated within the widget’s implementation. Interaction with the widget is possible from other components through the properties and methods that the widget exposes. Consider the following diagrams:
The first diagram shows a traditional HTML + JavaScript architecture. Since the visual (HTML) and behavioral (JavaScript) aspects of the application are publicly accessible, the application’s components can be manipulated directly which can lead to the HTML and JavaScript getting out of sync with each other. Extensive test suites are often needed to make sure that this does not happen.
The second image shows how widgets ensure that components only interact according to their design intent. The widget encapsulates its visual and behavioral aspects. It then exposes properties and methods that allow other components to interact with it. By providing a controlled interface, it is much easier to keep the visual and behavioral aspects of the widget synchronized.
In our demo application, we do not currently have any widgets, but let’s abstract the virtual DOM from main.ts
into a dedicated widget. You will notice that to render a widget, we use a different pragma, w()
, than we used before to render nodes that directly represent the DOM nodes.
Create a widget HelloWorld.ts
in src/widgets
import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
import { v } from '@dojo/framework/widget-core/d';
export default class HelloWorld extends WidgetBase {
protected render() {
return v('h1', { title: 'I am a title!' }, [ 'Biz-E-Bodies' ]);
}
}
Use the HelloWorld
widget in the renderer
in src/main.ts
const r = renderer(() => w(HelloWorld, {}));
This is a very simple widget, containing the virtual DOM that we previously were returning straight from the vdom renderer
, but it demonstrates some important concepts. Notice the render
method, which provides the virtual nodes (also known as DNodes) for the renderer
to determine what to add to the HTML document. In this example, the widget is simple enough that the function always returns the same result. We could make this widget more sophisticated by allowing it to take additional properties that can be used to alter how the DNodes are generated. We can provide sensible default values for these properties to add more complex behavior without needing to change how the rest of the application interacts with the widget. This approach encourages the development of loosely coupled components that are easier to develop and maintain over time.
Tests
The final aspect that our basic application contains is its test suite. Dojo is designed to ensure that errors are either not possible or easily detected, but tests are still required to verify business logic and ensure the application’s components work together as expected. Dojo leverages the Intern testing framework to provide its testing capabilities. Intern supports several testing strategies including unit, functional, performance benchmark, accessibility and visual regression testing. The tests also use the Dojo test-extras library from @dojo/framework
to verify the output of the widget’s render function. The test-extras library is designed to facilitate testing the functionality of Dojo widgets. For more information, refer to the Testing Dojo Widgets article.
Our demo application includes some tests to verify that it is working as expected. The tests are found in tests/unit/widgets/HelloWorld.ts
. Let’s examine this part of the test code:
it('should render', () => {
const h = harness(() => w(Banner, {}));
h.expect(() => v('h1', { title: 'I am a title!' }, [ 'Biz-E-Bodies' ]));
});
This test is ensuring that the rendering function is returning the correct tag and that the tag has the correct content. We will return to the topic of testing in a later tutorial, but for now you can use them to check your work as you progress through this series by running the following terminal commands:
dojo test
Running dojo test
compiles the application on demand (just in time compilation) by default for Node.js. However the tests can also be run against built bundles using the —config
config with dojo test
, but first we need to make sure we’ve built the test bundles. These bundles can be built using a watch command, meaning that the full application does not need to be rebuilt to re-run the tests after every change. There is a separate mode for unit and functional test bundles, —mode unit
and —mode functional
respectively.
In one terminal run:
dojo build --mode unit --watch
While this is command is running, in another terminal run:
dojo test --config local
Summary
This tutorial introduced the components that make up the core of every Dojo application. While there are many other components that are optional, the main HTML document, renderer, widgets and, hopefully, tests are present in all of them.
If you would like, you can open the completed demo application on codesandbox.io or alternatively download the project.
In Creating widgets, we will take a deeper look at Dojo widgets. We will go beyond the static widgets that we have worked with so far and learn how to create widgets that encapsulate state and behavior.