Building a simple app in Dojo

dojo app

I’ve been thinking about how I could demonstrate building a basic Dojo application beyond a hello world app or a Todo app. There are some really good samples in the dojo/examples repo. Then I came across this react application for searching for emojis and who doesn’t have to search for emojis regularly, so I knew I found my demo. It also helps that the dojo template on Code Sandbox now uses TSX/JSX as the default.

Because the dojo template app uses JSX by default, it made this sample almost a complete one to one of the react sample. I won’t go into detail of this application line by line, but I do want to cover some core concepts it shows.

Get Meta

Meta in Dojo is meta information about your widget. Pretty meta right?

When you build Dojo widgets, you never touch the output HTML of your application. There is no widget method to get a reference to the DOM. This prevents you from inadvertently changing a DOM element that is referenced by Dojos virtual DOM engine, which would be bad. So don’t get too crazy here. But there are valid reasons for wanting to access a DOM node in your application. In the case of my emoji application, I am using a small library called clipboardjs to let me copy emojis to my clipboard from my application. This library requires I pass a DOM node it will use to copy data to the clipboard.

You can get this information in Dojo is via a meta. Dojo provides some metas out of the box for you, like Dimensions, Animations, Intersection, and more. You can implement your own custom meta to access DOM nodes using `@dojo/framework/widget-core/meta/Base`.

// src/widgets/ElementMeta.ts
import { Base as MetaBase } from "@dojo/framework/widget-core/meta/Base";

class ElementMeta extends MetaBase {
  get(key: string): Element {
    const node = this.getNode(key);
    return node as Element;
  }
}

export default ElementMeta;

The meta implements a get() method that will get the DOM node via a given key and return that DOM node. Now in my application, where I use clipboardjs, I can use my meta in combination with the this.meta() method of the Widget to get a referenced DOM node.

// src/widgets/EmojiResultsRow.tsx
import { tsx } from "@dojo/framework/widget-core/tsx";
import { WidgetBase } from "@dojo/framework/widget-core/WidgetBase";

import * as css from "./styles/EmojiResultsRow.m.css";

import ElementMeta from "./ElementMeta";
import * as Clipboard from "clipboard";

export interface EmojiResultsRowProperties {
  title: string;
  symbol: string;
}

export class EmojiResultsRow extends WidgetBase<EmojiResultsRowProperties> {
  clipboard: Clipboard = null;

  onAttach() {
    // use my meta to get a DOM node
    const element = this.meta(ElementMeta).get(this.properties.title);
    this.clipboard = new Clipboard(element);
  }
  onDetach() {
    this.clipboard.destroy();
  }

  protected render() {
    const { title, symbol } = this.properties;
    const codePointHex = symbol.codePointAt(0).toString(16);
    const src = `//cdn.jsdelivr.net/emojione/assets/png/${codePointHex}.png`;
    // provide a `key` property to my widget element to
    // reference with my meta
    return (
      <div
        key={title}
        classes={[css.root, "copy-to-clipboard"]}
        data-clipboard-text={symbol}
      >
        <img alt={title} src={src} />
        <span classes={[css.title]}>{title}</span>
        <span classes={[css.info]}>Click to copy emoji</span>
      </div>
    );
  }
}

export default EmojiResultsRow;

Now I am able to use my custom meta to get a DOM node created by my widget. This makes access to output DOM nodes flexible, but also protects me from shooting myself in the foot unintentionally. If I break my DOM, it is totally my fault now.

Core Widgets

Dojo provides a suite of widgets you can use for your own applications. This includes items like TimePicker, Select and layout widgets. For my application, I’m interested in having an input that I can use for search. Every time I update the input element, I want to filter the list of emojis shown in my application. So I’m going to wrap a TextInput widget so I can manage some local state and pass the value of the input to a filter method.

// src/widgets/SearchInput.tsx
...
export class SearchInput extends WidgetBase<SearchInputProperties> {
  @watch() private searchValue = "";

  private onChange(value) {
    if (!value) {
      return;
    }
    this.searchValue = value;
    const { handleChange } = this.properties;
    handleChange(value);
  }

  protected render() {
    return (
      <div classes={[css.root]}>
        <div>
          <TextInput
            placeholder="Search for emoji"
            value={this.searchValue}
            onInput={this.onChange}
          />
        </div>
      </div>
    );
  }
}

Yes, I could have used a regular <input type="text" /> here, but the TextInput is very convenient as it already has an onInput method I can use that passes the value of the input directly, and not an event I would need to do event.target.value which, because I am lazy, I can really appreciate. Then I would need to use a keyup event, and maybe do some handling for different keys to on whether I want to get my value and why hassle with all that when Dojo provides a nice way to do it already.

I am also taking advantage of the @watch decorator to manage local state in my widget. I talked about this method in more detail here. This makes it very simple to manage the value of my input at all times.

You can see the full application in action here.

You can see that building applications in Dojo provides some safety and flexibility for you to piece together everything you need to build solid, and awesome applications. Dojo isn’t just a toolkit anymore, it’s a full blown framework and has a lot to offer!

Intro to the Dojo Router

We took a quick look at the Dojo router when we reviewed the template application from the dojo cli. The template application provides almost everything you need to know about the Dojo router. But let’s take a little deeper look at routing.

Defining Routes

The template application does a great job of providing a clear way to configure your routes.

export default [
  {
    path: "home",
    outlet: "home",
    defaultRoute: true
  },
  {
    path: "about",
    outlet: "about"
  },
  {
    path: "profile/{username}",
    outlet: "profile"
  }
];

The routes are defined as an array of objects. each Route object has a RouteConfig interface with properties you can define. In the snippet above I have made one change. I have set the path for the profile route as profile/{username}. This means I will need to define a parameter to that route, which we’ll get to in a moment, but first let’s look at the options for a route config.

// dojo/framework/src/routing/interfaces.d.ts
export interface RouteConfig {
  path: string;
  outlet: string;
  children?: RouteConfig[];
  defaultParams?: Params;
  defaultRoute?: boolean;
}

That’s the beauty of working with TypeScript and Dojo, you can look at the types and interfaces of the code and use them as a guide as to how you should use the tools. The only required properties are path and outlet. One of the other properties we see defined in our configuration is the defaultRoute, which as you may have guessed is the default route of your application. Who says naming things is hard?!

The children property would be used if you had nested routes. You could also define some default parameters, which is really useful if you have a route the depends on parameters, and your route needs them to behave correctly.

Outlet

The first part of routing we need to look at is the Outlet. The Outlet is a higher order component that you use to wrap up widgets that are part of a designated route.

// src/App.ts
...
export default class App extends WidgetBase {
  protected render() {
    return v("div", { classes: [css.root] }, [
      w(Menu, {}),
      v("div", [
        // Outlet is where routes go
        // the decide which widgets
        // match each route
        w(Outlet, {
          key: "home",
          id: "home",
          renderer: () => w(Home, {})
        }),
        w(Outlet, {
          key: "about",
          id: "about",
          renderer: () => w(About, {})
        }),
        w(Outlet, {
          key: "profile",
          id: "profile",
          renderer: () => w(Profile, { username: "Dojo User" })
        })
      ])
    ]);
  }
}

Looking at the outlet, you can see that we define the id of the Outlet to match the Route configuration we defined. The actual widget rendered in the Route doesn’t have to match the id, but as you can see, it’s pretty good practice to do so. Keep the code readable please.

Outlets are pretty straightforward. Since they render the widget for a Route, they can also handle passing any URL parameters as properties to the widget.

Link and Parameters

Before we dive in to URL parameters, first we need to talk about how you can create a link to a route that is expecting parameters. We can define those parameters with a specific component in Dojo for working with routes, the Link component.

// src/widgets/Menu.ts
w(
  Link,
  {
    to: 'profile',
    key: 'profile',
    classes: [css.link],
    activeClasses: [css.selected],
    params: {
      username: 'odoe'
    }
  },
  ['Profile']
)

The Link component is designed specifically for creating links to routes and static paths in your application. They provide some sugar to regular anchor tags you can take advantage of in your apps. In this case, I am providing a value to the username parameter we defined for our route. This means that is will pass the object { username: ‘odoe’ } to my Outlet that I can then use to pass to my child widget.

// src/App.ts
w(Outlet, {
  key: 'profile',
  id: 'profile',
  renderer: ({ params }: MatchDetails) => {
    return w(Profile, { username: params.username });
  }
})

When you pass parameters to a URL in the Dojo router, your render method is passed the parameters for you to use in your application as needed. Now, although this method works fine, you can be more explicit in how you use your route parameters.

You can define query parameters in your routes and use them for more advanced usage. Let’s update the route configuration.

// src/routes.ts
export default [
  ...
  {
    path: "profile/{param}?{value}",
    outlet: "profile"
  }
];

Maybe we have different ways of searching for users in our backend API. We can search by name or id,.

// src/widgets/Menu.ts
w(
  Link,
  {
    to: 'profile',
    key: 'profile',
    classes: [css.link],
    activeClasses: [css.selected],
    params: {
      param: 'name',
      value: 'odoe'
    }
  },
  ['Profile']
)

Now we can update our Outlet to pass the correct information to the child widget.

// src/App.ts
w(Outlet, {
  key: 'profile',
  id: 'profile',
  renderer: ({ params, queryParams }: MatchDetails) => {
    const user = users.find((user: User) => {
      return user[params.param] == queryParams.value;
    }) as User;
    return w(Profile, { username: `${user.name} ${user.lastName}` });
  }
})

Now we have built a fairly generic way of passing parameters and values to our Outlet and being able to search for the correct username to use in our widget. We can search by the name value or an id value.

Default Parameters

So far we have been defining parameters in our Link, but maybe we want to define some default parameters directly in our route instead.

// src/routes.ts
export default [
  ...
  {
    path: 'profile/{param}?{value}',
    outlet: 'profile',
    defaultParams: {
      param: 'id',
      value: '2'
    }
  }
];

For out default route, we can decide to search by id with a value of 2. When you start dealing with URL parameters, everything is a string, so if you wanted to use real numbers, you would need to do some additional sanitization in your application, but I think we’ve dived pretty deep into setting up the Dojo router for starter use. Big thanks to Anthony Gubler for helping me out with some of my router questions, it was a big help.

Summary

As you can see, the Dojo router is very flexible in how you want to define your routes and parameters. Depending on how your backend APIs are defined, you could create some very powerful and scalable applications!