Creating widgets

Overview

In this tutorial, you will learn how to create and style custom widgets in Dojo.

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.

Creating the application widget

Create a new root node for the application.

In Your first Dojo application, the first tutorial in this series, we created an application with a single widget, which we modified to show the title of our Biz-E-Bodies view. In this tutorial, we are going to expand our application to show the names and portraits of our Biz-E-Bodies, the workers in the fictional Biz-E-Corp. Before we get to that, we have some refactoring to do. Our demo application is currently hard-wired to render our widget, which has been renamed to the more appropriate Banner in this tutorial. This can be found in main.ts:

  1. import renderer from '@dojo/framework/widget-core/vdom';
  2. import { w } from '@dojo/framework/widget-core/d';
  3. import Banner from './widgets/Banner';
  4. const r = renderer(() => w(Banner, {}));
  5. r.mount({ domNode: document.querySelector('my-app') as HTMLElement });

This line: const Projector = ProjectorMixin(Banner); tells the application to use the Banner widget as the source of the virtual DOM elements for rendering the application. To add a second widget, we are going to create a new widget called App that represents the entire application that we are building. To start that process, go into the empty App.ts file located in the src/widgets directory. First, we need to add the required dependencies to create the App widget.

Add these lines at the top of the file.

  1. import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
  2. import { v, w } from '@dojo/framework/widget-core/d';

The WidgetBase class will be used as the base class for our App widget. WidgetBase (and its descendants) work with the WidgetProperties interface to define the publicly accessible properties of the widget. Finally, the v() and w() functions are used to render virtual DOM nodes (with the v function) or widgets (via w). Both virtual DOM nodes and widgets ultimately generate DNodes, the base type of all virtual DOM nodes in Dojo.

Our next dependency to load is the Banner widget that we created in the first tutorial.

To import it, add the following statement to App.ts

  1. import Banner from './Banner';

With all of the dependencies in place, let’s create the App widget itself.

Add the following class definition.

  1. export default class App extends WidgetBase {
  2. }

Notice that the App class is extending WidgetBase, a generic class that accepts the WidgetProperties interface. This will give our class several default properties and behaviors that are expected to be present in a Dojo widget. Also, notice that we have added the export and default keywords before the class keyword. This is the ES6 standard approach for creating modules, which Dojo leverages when creating a widget - the widget should be the default export in order to make it as convenient as possible to use.

Our next step is to override WidgetBase‘s render method to generate the application’s view. The render method has the following signature protected render(): DNode| DNode[], which means that our render method has to return a DNode or an array of DNodes so that the application’s projector knows what to render. The only way to generate this DNode is by calling either the v or w functions.

To start, let's use a simple render method by adding this to the App class:

  1. protected render() {
  2. return v('div');
  3. }

This method will generate a div virtual node with no children. To render the Banner as a child of the div, we’ll use the w function that is designed to render widgets.

Update the render method to the following:

  1. protected render() {
  2. return v('div', [
  3. w(Banner, {})
  4. ]);
  5. }

Mandatory object for properties
The 2nd argument of the w() function is mandatory even you have no properties to pass in. This is to ensure the correct type guarding for all widgets in TypeScript.

Notice that the w function takes two parameters - a widget class and an object literal. That literal must implement the interface that was passed to WidgetBase via TypeScript generics. In this case, the Banner class uses WidgetProperties which has the following definition:

  1. export interface WidgetProperties {
  2. key?: string;
  3. }

key is optional, so we can pass an empty object for now. Next, we will replace the Banner class with the App as the root of our application.

Make the App widget the root of the application

Replace the Banner class with the App widget.

Our App class is now complete and ready to replace the Banner class as the root of the application. To do that, we will edit main.ts.

The first update will replace the import statement from the Banner class to the new App class:

  1. import App from './widgets/App';

The only other change we need to make is to pass the App to the w() call in the renderer:

  1. const r = renderer(() => w(App, {}));

With that change, the App widget is ready to serve as the root of our application. Let’s test everything by building and running the project.

If you are working locally, run the following command:

  1. dojo build -m dev -w -s

then open up a web browser and navigate to http://localhost:9999. You should see the Biz-E-Bodies title that we started with, but if you examine the actual DOM, you will see that the Banner’s <h1> tag has been wrapped by the App’s <div>, so everything appears to be working.

In the next section, we’ll create the Worker widget that will show the portrait and name of our Biz-E-Bodies.

Creating the Worker widget

Create the Worker widget and use it to display some information about a worker.

Now it is time to create our Worker widget. For now, this widget will only render static content. We will use its properties to allow the application to customize what is rendered. Our goal is to end up with something that looks like this:

worker_widget

The first step is to create the worker widget. We will put the implementation in Worker.ts. As with the App widget that we created earlier, we need to add some initial dependencies and the class declaration to Worker.ts.

Add this code:

  1. import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
  2. import { v } from '@dojo/framework/widget-core/d';
  3. export default class Worker extends WidgetBase {
  4. protected render() {
  5. return v('div');
  6. }
  7. }

This is nearly identical to the App widget with one exception: we are not importing the w function as the Worker widget will not contain any child widgets.

Our next step is to override the render() method to customize the widget’s appearance. To accomplish this, we are going to need two children. One <img> tag to show the worker’s portrait and a <strong> tag to display the worker’s name.

Try and implement that using the URL https://dojo.io/tutorials/resources/worker.svg and "lastName, firstName" as the worker's name.

  1. protected render() {
  2. return v('div', [
  3. v('img', { src: 'https://dojo.io/tutorials/resources/worker.svg' }),
  4. v('div', [
  5. v('strong', [ 'lastName, firstName' ])
  6. ])
  7. ]);
  8. }

Before we continue to refine this widget, let’s review our progress by adding the Worker widget to the app.

Add a Worker widget to the App.

Within App.ts, import the Worker widget and then update the App's render method to render it. The Worker will be another child of the App, so we just need to add another entry to the children array.

  1. import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
  2. import { v, w } from '@dojo/framework/widget-core/d';
  3. import Banner from './Banner';
  4. import Worker from './Worker';
  5. export default class App extends WidgetBase {
  6. protected render() {
  7. return v('div', [
  8. w(Banner, {}),
  9. w(Worker, {})
  10. ]);
  11. }
  12. }

Run the application with dojo build -m dev -w -s and navigate to http://localhost:9999.

We have succeeded in rendering the widget, but there seem to be some styling issues. We’ll come back to that in a bit. For now, let’s continue refining the Worker widget to allow the application to configure it before it is rendered. In Dojo, this is done by creating an interface to pass configuration information into the widget.

Making a configurable widget

Add properties to the Worker and use those to configure it.

Return to Worker.ts and add an interface with the custom properties that we need:

  1. export interface WorkerProperties {
  2. firstName?: string;
  3. lastName?: string;
  4. }

Change the generic parameter passed into WidgetBase with the new interface:

  1. export default class Worker extends WidgetBase<WorkerProperties>

The WorkerProperties interface adds two new optional properties that we’ll be able to use. Now that these are available, let’s use them to make the name of the worker controlled by the parent widget.

Inside of the render method, add the following code to create some local constants for the first and last names:

  1. const {
  2. firstName = 'firstName',
  3. lastName = 'lastName'
  4. } = this.properties;

This code retrieves the appropriate property and provides a reasonable default in case the widget doesn’t receive a value. This is done via a destructuring assignment. We can now update the generated virtual DOM with those values by updating the returned value from the render method with those property values.

Update the render method to look like this:

  1. protected render() {
  2. const {
  3. firstName = 'firstName',
  4. lastName = 'lastName'
  5. } = this.properties;
  6. return v('div', [
  7. v('img', { src: 'https://dojo.io/tutorials/resources/worker.svg' }),
  8. v('div', [
  9. v('strong', [ `${lastName}, ${firstName}` ])
  10. ])
  11. ]);
  12. }

Configuring a widget

Pass properties to the Worker widget to configure it.

Remember
You should already see the new values. However, if you shut down the build command, you can start it up again by running dojo build -m dev -w -s and navigating to http://localhost:9999.

To use the functionality of the new Worker widget we will update the render method in the App class to pass in some properties. In a full Dojo application, these values could possibly be retrieved from an external state store or fetched from an external resource, but for now, we’ll just use static properties. To learn more about working with stores in Dojo, take a look at the dojo/stores tutorial in the advanced section.

In App.ts, update the line that is rendering the Worker to contain values for the firstName and lastName properties:

  1. w(Worker, { firstName: 'Tim', lastName: 'Jones' })

At this point, we have a good start to our widget, but it still doesn’t look very good. In the next section we’ll address that by learning how to use CSS to style our widgets.

Styling widgets with CSS modules

Use Cascading Style Sheets to change a widget's appearance.

We can use CSS files to establish the look and feel of a widget or application.

Dojo leverages CSS Modules to provide all of the flexibility of CSS, but with the additional benefit of localized styling rules to help prevent inadvertent rule collisions. Dojo also makes use of typed CSS modules, so that we can provide CSS typing files, enabling you to target CSS files in your import statements.

To allow our Worker widget to be styled, we need to modify the class. First, apply a decorator to the class to modify the widget’s constructor and prepare its instances to work with CSS modules. Also, we will apply a theme mixin to the Worker widget. A mixin is not intended to be used on its own, but instead works with a class to add useful functionality.

Add the following import to the top of Worker.ts:

  1. import { theme, ThemedMixin } from '@dojo/framework/widget-core/mixins/Themed';

We also need to import our CSS:

  1. import * as css from '../styles/worker.m.css';

worker.m.css contains CSS selectors and rules to be consumed by our widget and its components.

With the imports in place, add the @theme decorator and apply the mixin to the Worker class in Worker.ts:

  1. @theme(css)
  2. export default class Worker extends ThemedMixin(WidgetBase)<WorkerProperties> {

Add the CSS rules in src/styles/worker.m.css which will allow us to style the Worker widget:

  1. .worker {
  2. flex: 1 1 calc(33% - 20px);
  3. margin: 0 10px 40px;
  4. max-width: 350px;
  5. min-width: 250px;
  6. position: relative;
  7. border: 1px solid #333;
  8. border-radius: 4px;
  9. box-sizing: border-box;
  10. padding: 30px;
  11. text-align: center;
  12. z-index: 2;
  13. }
  14. .image {
  15. margin: 0 auto 20px;
  16. max-width: 250px;
  17. width: 100%;
  18. }

dojo build -m dev -w -s will detect these new rules and generate the type declaration files automatically, allowing us to apply them to the Worker widget.

Return to Worker.ts and update the render method to add the classes:

  1. protected render() {
  2. const {
  3. firstName = 'firstName',
  4. lastName = 'lastName'
  5. } = this.properties;
  6. return v('div', {
  7. classes: this.theme(css.worker)
  8. }, [
  9. v('img', {
  10. classes: this.theme(css.image),
  11. src: 'https://dojo.io/tutorials/resources/worker.svg' }),
  12. v('div', [
  13. v('strong', [ `${lastName}, ${firstName}` ])
  14. ])
  15. ]
  16. );
  17. }

You may notice that we are calling this.theme with the worker and image classes as arguments. theme is a method provided by the ThemedMixin which is used to return the overriding class if the widget has been configured with a theme. To learn more about theming, review the Theming an Application tutorial.

If you return to the browser, you’ll see that the widget now has the classes applied and looks a little better.

worker_page

While you are there, open up the developer tools and look at the CSS classes that have been applied to the widget’s components. Notice that we don’t have class names such as .worker or .image like we used in the CSS file, rather we have something like .workerimage3aIJl. The dojo build command uses CSS Modules to obfuscate class names when it compiles the project to ensure that CSS selectors are localized to a given widget. There are also ways to provide global styling rules (called “themes”). To learn more about those, take a look at the Theming an Application tutorial.

We’ve now updated our application to display a single employee, but our goal is to display a collection of employees. We could certainly add additional Worker widgets to our application, but they would all be siblings of the Banner widget and could be difficult to style properly. In the next section, we’ll create a simple container widget that will manage the layout of the Worker widgets.

Moving the Worker into a container

Create a container to handle the layout of Worker widgets.

The WorkerContainer manages the layout of our Worker widgets and makes it easier to style these widgets. The WorkerContainer has many of the same responsibilities as the App widget. It will be responsible for generating both virtual DOM nodes directly as well as rendering widgets. Similar to the Worker widget, we will apply some styling to it.

Putting this all together, add the following to WorkerContainer.ts:

  1. import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
  2. import { w, v } from '@dojo/framework/widget-core/d';
  3. import Worker from './Worker';
  4. import { theme, ThemedMixin } from '@dojo/framework/widget-core/mixins/Themed';
  5. import * as css from '../styles/workerContainer.m.css';
  6. const WorkerContainerBase = ThemedMixin(WidgetBase);
  7. @theme(css)
  8. export default class WorkerContainer extends WorkerContainerBase {
  9. protected render() {
  10. return v('div', {
  11. classes: this.theme(css.container)
  12. });
  13. }
  14. }

Now update the render method to include some workers. Add the following to the top of the render method:

  1. const workers = [
  2. w(Worker, {
  3. key: '1',
  4. firstName: 'Tim',
  5. lastName: 'Jones'
  6. }),
  7. w(Worker, {
  8. key: '2',
  9. firstName: 'Alicia',
  10. lastName: 'Fitzgerald'
  11. }),
  12. w(Worker, {
  13. key: '3',
  14. firstName: 'Hans',
  15. lastName: 'Mueller'
  16. })
  17. ];

Notice that we have added a key property to each child. This is needed so that we can differentiate between the children. If you add multiple children that have the same tag name, e.g. div or widget name, e.g. Worker, then you will need to add a property that makes each child unique.In the code example shown above, we have added a key property and set the value to be unique for each child widget.

We can now pass these workers as children to the container.

Add this array as the third argument to the v function.

  1. return v('div', {
  2. classes: this.theme(css.container)
  3. }, workers);

Now it is time to add styling rules for the WorkerContainer. Inside of styles/workerContainer.m.css.

Add the following rule.

  1. .container {
  2. display: flex;
  3. flex-flow: row wrap;
  4. justify-content: center;
  5. align-items: stretch;
  6. margin: 0 auto;
  7. width: 100%;
  8. }

Finally, update the App class to replace the Worker widget with the new WorkerContainer.

  1. import WorkerContainer from './WorkerContainer';
  1. protected render() {
  2. return v('div', [
  3. w(Banner, {}),
  4. w(WorkerContainer, {})
  5. ]);
  6. }

The application now renders three workers in the WorkerContainer widget, allowing us to control how they are laid out without impacting the overall application.

Summary

In this tutorial, we have created and styled widgets within Dojo. Widgets are classes that derive from WidgetBase. This base class provides the basic functionality for generating visual components in a Dojo application. By overriding the render method, a widget can generate the virtual DOM nodes that control how it is rendered.

Additionally, we learned how to style widgets by using CSS modules. These modules provide all of the flexibility of CSS with the additional advantages of providing strongly typed and localized class names that allow a widget to be styled without the risk of affecting other aspects of the application.

If you would like, you can open the completed demo application on codesandbox.io or alternatively download the project.

In Responding to events, we will explore the how to add event handlers to allow our application to respond to user interactions.