Data-driven widgets

Overview

The Dojo widget system provides a functional API that strives to strictly enforce a unidirectional data flow. The only way to interact with a widget is through the properties it exposes, and dealing directly with widget instances is both uncommon and considered an anti-pattern. It may initially be challenging to understand how to build data-driven widgets in such a reactive framework, especially when widgets in past frameworks such as Dojo 1 were so tightly coupled to data store implementations.

In this tutorial we will create a filterable data-driven list widget, demonstrating how Dojo widgets should be decoupled from data providers.

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 a filterable List widget

Create a widget that uses a Dojo TextInput.

Before digging into the specifics of wiring a widget to a data source, a basic list widget with a filter input must first be created. Initially, the widget will simply render a Dojo TextInput widget, with additional functionality to be added later within this tutorial. List.ts will contain our implementation, and similarly to widgets created during previous Dojo tutorials, initial dependencies and a class declaration are needed to get started.

Add the following skeleton to List.ts:

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

This code lays the base foundation for a standard Dojo widget: it extends the WidgetBase class and it defines a render method that returns a virtual DOM. The next step is to import a Dojo TextInput and use it inside the List.

Import TextInput into List.ts:

  1. import TextInput from '@dojo/widgets/text-input';

Update the ListProperties interface so that a value and an onInput callback can be passed into the List:

  1. export interface ListProperties {
  2. onInput: (value: string) => void;
  3. value: string;
  4. }

The next step in creating an initial filterable list widget is to update its render method to define a TextInput using the w module.

Update the List to render a TextInput using the value and onInput properties.

  1. protected onInput(value: string) {
  2. this.properties.onInput(value);
  3. }
  4. protected render() {
  5. return v('div', [
  6. w(TextInput, {
  7. value: this.properties.value,
  8. onInput: this.onInput,
  9. placeholder: 'Filter workers...'
  10. })
  11. ]);
  12. }

Before continuing with the List implementation, let’s review and verify the progress so far by adding the current widget to the Biz-E-Bodies application.

Adding a List widget to the application

Add the List widget to the application.

We will add the List widget to the existing Banner widget so users can filter a list of workers by name when first visiting the application. The end result should look something like this:

list_widget

Import the List and the w module into Banner.ts:

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

Update Banner to render an array of virtual DOM nodes including the existing <h1> element as well as a List widget.

  1. protected render() {
  2. return [
  3. v('h1', { title: 'I am a title!' }, [ 'Welcome To Biz-E-Bodies' ]),
  4. v('label', ['Find a worker:']),
  5. w(List, {
  6. onInput: (value: string) => null,
  7. value: ''
  8. })
  9. ];
  10. }

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

A solid foundation for a filterable list widget using a Dojo TextInput widget has been created and added to the existing Banner. Thus far, the widget is not connected to an external data source.

Connecting the List to data

Connect the List to data using its properties.

Traditional widget frameworks like Dojo 1 required developers to tightly couple widget instances to data store instances. For example, it was common for Dojo 1 widget code to expect a store property to exist on a widget instance, and to further expect that store to expose a Dojo-specific data store API. While effective, this method of explicitly connecting widgets to data stores is brittle and forces widgets to call specific methods on stores.

Data-driven widgets in Dojo do not call methods on a store directly (and are not connected directly to a data store at all). Instead widgets request data from their parent widget using callback properties, and the parent passes properties containing relevant data down to its children. The parent can in turn request data from its parent in the same manner, or it can fetch data directly, such as by dispatching an action to an app store like Redux or making an XHR request directly. Dojo widgets can use this parent-driven data approach to enable compatibility with virtually any data provider by strictly decoupling widgets from the stores that power them.

The first step to connecting the List to worker data is to update its properties interface so it can accept a data array.

Update the ListProperties to support passing a data array property into the List:

  1. import { WorkerProperties } from './Worker';
  2. export interface ListProperties {
  3. data?: WorkerProperties[];
  4. onInput: (value: string) => void;
  5. value: string;
  6. }

Playing it safe
The WorkerProperties interface is imported from the Worker widget so that the TypeScript typings for the data array can be as strict as possible.

The ListProperties interface now defines an optional data property that can be passed into a List widget. This property could have been called by any name other than data; the important takeaway is that data items can now be passed into the List widget using properties. The next step is to update the List to render a list of items based on its new data property.

Update the List so it also renders a <div> for each item in its data array property.

  1. protected renderItems() {
  2. const { data = [] } = this.properties;
  3. return data.map((item: any) => v('div', [ `${item.firstName} ${item.lastName}` ]));
  4. }
  5. protected render() {
  6. return v('div', [
  7. w(TextInput, {
  8. value: this.properties.value,
  9. onInput: this.onInput,
  10. placeholder: 'Filter workers...'
  11. }),
  12. v('div', this.renderItems())
  13. ]);
  14. }

Injecting state
The State management tutorial details how to use Dojo Containers and Injectors to inject external state as properties of widgets.

The List widget now renders both a TextInput to accept user input and a list of result items based on its data property. Item rendering is offloaded into a helper method (renderItems) so widgets extending List that need to modify the rendering of items only need to override a small helper method instead of the main render method.

The next step updates the Banner widget to pass the correct data into the List

Pass worker data down through the Banner to the List

Currently, the application keeps all worker data as a private variable within the App widget. In order to use this data to also power the List, the Banner widget must also be updated to accept a data property so it can in turn pass it down to the List it renders.

Export a BannerProperties interface from the Banner widget so a data array property can be passed into Banner:

  1. import { WorkerProperties } from './Worker';
  2. export interface BannerProperties {
  3. data?: WorkerProperties[];
  4. }

Update the generic parameter passed into WidgetBase to use the new interface:

  1. export default class Banner extends WidgetBase<BannerProperties> {

Pass worker data to the routing outlet for the Banner widget in App.ts:

  1. w(Outlet, { id: 'home', renderer: () => {
  2. return w(Banner, {
  3. data: this._workerData
  4. });
  5. }})

Now that the Banner widget has worker data, it can pass a subset of this data into the List widget based on the current List input value.

Pass a subset of worker data into the List based on its value in Banner.ts:

  1. private _data: any[];
  2. protected filterData(value: string) {
  3. const { data = [] } = this.properties;
  4. this._data = data.filter((item: WorkerProperties) => {
  5. const name = `${item.firstName} ${item.lastName}`;
  6. const match = name.toLowerCase().match(new RegExp(`^${value.toLowerCase()}`));
  7. return Boolean(match && match.length > 0);
  8. });
  9. this.invalidate();
  10. }
  11. protected render() {
  12. return [
  13. v('h1', { title: 'I am a title!' }, [ 'Welcome To Biz-E-Bodies' ]),
  14. v('label', ['Find a worker:']),
  15. w(List, {
  16. onInput: this.filterData,
  17. value: '',
  18. data: this._data || this.properties.data
  19. })
  20. ];
  21. }

A helper method was added that filters worker data items by name based on a query value. These filtered items are passed to the List as its data, causing the List to render result items if any items exist based on the user-specified filter.

Summary

A filterable list widget was created throughout this tutorial to demonstrate how Dojo widgets have no coupling to a specific data store implementation. Despite the simplicity of this example widget, it demonstrates a key departure from Dojo 1 and other widget frameworks that lack reactivity: data-driven widgets request data from their parent and parents pass data back down to their children.

It’s also important to note that in this tutorial, hardcoded worker data is passed down from the App widget to the Banner widget and ultimately to the List widget, but the data could be provided from any data source, including a remote server. It’s entirely possible for Banner to directly initiate an XHR request for data, and it’s also possible for Banner to dispatch an action to an application store that in turn requests data. The flexibility provided by the widget system makes it so the List is not concerned with the data origin. Instead, data-driven widgets in Dojo both request and receive data using their properties, and the parent is responsible for either relaying or initiating the data request itself.

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