dojo dragon main logo

Dojo 7 Has Arrived

Anthony Gubler May 26, 2020, 12:00 PM

We’re excited to announce the latest release of Dojo, a continually evolving, batteries-included, TypeScript web framework. Dojo’s primary goals haven’t changed, and we continue to focus on harmony with the modern web ecosystem, best in class developer ergonomics and intelligent, powerful defaults that enable users to concentrate on building features and applications.

Announcing Dojo 7

After nearly two years since the first official release of modern Dojo, version 7 provides many new features, bug fixes and general improvements spanning the entire framework. Major releases have been regular occurrences over the last two years. The latest version 7 release is the most adventurous. This release has taken a few months longer than usual but is worth the wait.

We’re especially proud of the overhaul to the official Dojo widget library, @dojo/widgets, which has been re-thought from the ground up. Dojo widgets have now caught up with the Dojo framework improvements and best practices that have evolved over the last two years. Further details are available in the Dojo widget release blog.

For @dojo/framework and friends, version 7 primarily focuses on building on the function-based widget and middleware authoring patterns from Dojo 6. This effort helps make building applications more straightforward than ever, continues improving the developer experience and ergonomics, and further refines Dojo's Web Component Custom Element support.

Listen to one of Dojo's lead engineers, Matt Gadd, talking about this release of Dojo 7 and the future of Dojo on the JS Party podcast.

Typed Widget Children

Function-based widgets have empowered Dojo to deliver features that were extremely difficult with the Class-based widget authoring pattern. Render properties were getting used as a workaround to being able to implement functionality for widgets where properties need to get passed back to the user to render output that effectively gets used as children. Although this pattern works, it comes with some significant gotchas, as the property needs to get treated like children by Dojo's rendering engine. Unfortunately, this isn't something that Dojo can do automatically, meaning that the author was left to ensure that the widget always re-renders to guarantee that the render properties output is not stale.

Now with function-based widgets, it is possible to specify the expected type(s) of the children as functions or objects. In doing so, this ensures that Dojo knows that the widget has children and all the correct rendering paths get followed.

import { create, tsx } from '@dojo/framework/core/vdom';
import { RenderResult } from '@dojo/framework/core/interfaces';
import icache from '@dojo/framework/core/middleware/icache';

interface MyWidgetChildren {
    (active: boolean): RenderResult;
}

const factory = create({ icache }).children<MyWidgetChildren>();

const MyWidget = factory(function MyWidget({ children, middleware: { icache } }) {
    const [ child ] = children();
    const active = icache.getOrSet('active', true);
    return (
        <div>{child(active)}</div>
        <button onclick={() => {
            icache.set('active', icache.getOrSet('active', true));
        }}>{`Set ${active ? 'inactive' : 'active'}`}</button>
    );
});

// Usage
<MyWidget>{(active) => <div>{`${active ? 'ACTIVE' : 'NOT ACTIVE'}`}</div>}</MyWidget>

Typed children can be even more expressive by using an object that describes children for different sections of the widget, referred to as named children. For example, a Card widget could have title, avatar, and content that can get defined by a user. Using standard children, there is no clear way for a user to define the output required for each, and using render properties still have all the original downsides of the pattern. However, with named children an object can be used to indicate the purpose of the child and whether they are mandatory or optional.

import { create, tsx } from '@dojo/framework/core/vdom';

import * as css from './Card.m.css';

interface CardChildren {
    title: RenderResult;
    avatar: RenderResult;
    content: RenderResult;
}

const factory = create().children<CardChildren>();

const Card = factory(function Card({ children }) {
    const [{ title, avatar, content }] = children();
    return (
        <div>
            <div classes={[css.title]}>
                {title}
            </div>
            <span classes={[css.avatar]}>
                {avatar}
            </span>
            <div classes={[css.content]}
                {content}
            </div>
        </div>
    );
});

// Usage
<Card>{{
    title: 'My Title',
    avatar: <img src="https://my-content.com/avatar.png"/>,
    content: <div>My Main Content!</div>
}}</Card>

Custom Elements, Improved

Dojo's support for the Custom Elements portion of Web Components provides interoperability with other frameworks and component systems, not only consuming custom elements in the framework but building Dojo widgets as custom elements. Developer ergonomics are even more important with custom elements, to ensure that the Dojo custom elements can get used, with minimal effort, in the declarative way they are designed.

Working with Dojo 6's compiled custom elements that used the render property pattern did not allow the custom elements to get used effectively. However, in Dojo 7, widgets that use functional children in place of a render property can use the custom element declaratively like standard HTML.

<dojo-my-widget>
    <div>My content for the widget</div>
</dojo-my-widget>

For widgets that take advantage of the named children pattern, the children can also be declaratively defined using the new "slots" attribute to indicate which section of the widget's children the node is intended for. Using the Card widget example above, this would look something like:

<dojo-card>
    <div slot="title">My Title</div>
    <img slot="avatar" src="https://my-content.com/avatar.png" />
    <div slot="content">My Main Content!</div>
</dojo-card>

The slot feature for Dojo custom elements provides the equivalent support that slots solve for native custom elements..

Working with Dojo custom elements that require more than simple attributes has also become easier, in Dojo 7, arrays and objects are now supported as standard attributes. These attribute need to be serialized and passed as attributes to the custom element and will automatically be deserialized with the property getting set on the custom element.

<dojo-checkbox-group options="[{value:'cat'},{value:'dog'},{value:'fish'}]">
    <label slot="label">pets</label>
</dojo-checkbox-group>

For more details visit the new Dojo custom element reference guide.

Dojo Resources, bringing widgets and data together

Dojo 7 makes widgets resource-aware with the addition of resources. This allows a user to define a single sharable resource template which can be passed used across your application without widgets needing any knowledge of how the data is fetched or where it is coming from. Dojo resources provides a single consistent approach to working with data, capable of pagination and filtering. We've implemented this pattern in the new List, Select and Typeahead widgets meaning you can get up and running with resources right away.

import { create, tsx } from '@dojo/framework/core/vdom';
import { createMemoryResourceTemplate, createResourceMiddleware } from '@dojo/framework/core/middleware/resources';
import { List } from './List';

interface MyResourceItem {
    value: string;
}

const template = createMemoryResourceTemplate<MyResourceItem>();
const resource = createResourceMiddleware();
const factory = create({ resource }).properties<{ items: MyResourceItem[] }>;

export default factory(function({ id, properties, middleware: { resource }}) {
    const { items } = properties();
    const { getOrRead, createOptions } = resource;
    const options = createOptions(id);
    const [items] = getOrRead(template, options(), { id, data: item })
    return (
        <div>
            <List resource={resource{{ template, initOptions: { id, data: item }}}} />
            {items && <ul>{items.map((item) => <li>{item.value}</li>)}</ul>}
        </div>
    );
});

For more information, please see the Dojo Resources reference guide.

Dojo test renderer

As part of the Dojo 7 release, testing in Dojo has been given an overhaul to provide support for new features such as functional children, type-safe APIs and general updates that promote testing best practices.

There are five main components to the new test renderer:

  • renderer
    • The function used to render widgets in the test environment.
  • assertion
    • An assertion builder which can be expected against the test renderer.
  • wrap
    • A function that wraps widgets and nodes to be used as a type-safe selector when interacting with assertions and the test renderer.
  • ignore
    • A utility function to exclude nodes from an assertion
  • compare
    • A custom comparator for node properties that can be used with a wrapped test node

The key concept for the new renderer is using the wrap function to create wrapped test nodes and widgets that are used within a tests assertion, in place of the real widget. This provides the assertions type information based on the wrapped widget and enables identifying the node or widget in the assertion template structure. The same is true for the test renderer itself that working with properties and children can be done in a type-safe way, using the location of the wrapped test node in the assertion template structure to call properties and resolve children.

The current harness exists in the @dojo/framework/testing/harness directory. It will be supported through at least version 9 of Dojo, giving time for applications to update their existing tests to use the new test renderer. The harness imports will automatically be updated when upgrading your application using the cli-upgrade-app Dojo CLI command.

For more details about the new test renderer, visit the testing reference guide.

Simplified routing and outlets

The concept of an Outlet in the Dojo routing has always been synonymous with each unique route, essentially the id of the route itself. An Outlet always reflected a single Route. As such, the Outlet widget is now known as Route. With the routing configuration requiring a unique id for every route, this will usually be the same value previously used for the Outlet name.

Making this change in routing enables the existing Outlets concept to change from being tied to a specific route to represent instead a section of the application that the routing system can render, varying the content based on the matched route. This leads to less duplication in application code for each route and allows outlets in the routing configuration to more accurately describe the application layout.

To assist with the migration from Outlet to Route and updating the routing configurations to include an id for each route the @dojo/cli-upgrade-app command is available.

For more details, visit the routing reference guide.

Custom widget keys

For widgets that contain more business logic, for example fetching remote data based on a specific property value, catering for all the scenarios that data needs to get cleared and re-fetched based on the property value changing are complicated and can lead to some subtle bugs in application behavior.

In previous versions of Dojo, users can use the property value as the widget key to instruct Dojo to re-create the widget when the value changes, but this was left to the user to manage and there was no way to enforce this pattern.

In Dojo 7, authors can select a widget's property that Dojo will use in addition to the existing key property to indicate that a widget needs to get re-created. This is powerful because authors can control and guarantee the behavior from Dojo and do not need to deal with the complicated scenarios to "reset" a widget when the property value changes.

import { create, tsx } from '@dojo/framework/core/vdom';
import icache from '@dojo/framework/core/middleware/icache';

interface MyWidgetProperties {
    id: string;
}

// specifying a key will instruct Dojo to include that property along with `key`
// in determining if the widget needs to be recreated.
const factory = create()
    .properties<MyWidgetProperties>()
    .key('id');

const MyWidget = factory(function MyWidget({ properties, middleware: { icache } }) {
    const data = icache.getOrSet('data', async () => {
        const { id } = properties();
        const response = await fetch(`https://my-remote-service/v1/company/${id}/people'`);
        const data = await response.json();
        return data;
    });
    return (
        <div>
            <h2>Company Employees</h2>
            {data ? (
                <ul>
                    {data.map((item) => (
                        <li>{item}</li>
                    ))}
                </ul>
            ) : (
                <div>Loading...</div>
            )}
        </div>
    );
});

Smarter i18n

Dojo 7 includes considerable improvements to the in-built i18n support with all CLDR data now automatically included based on the locales defined in the application's .dojorc and the i18n usage within the application. The CLDR is also split into targeted bundles that are loaded on-demand when the locale changes. This massively reduces the up-front bundle cost that a user pays for a feature that they may never require or use.

There should be zero to very minimal changes required for applications to take advantage of these improvements. Simply ensure that all the supported locales are defined in the application's .dojorc (as they should already be) and enjoy smaller, smarter i18n.

{
    "build-app": {
        "locale": "en",
        "supportedLocales": ["fr", "de"]
    }
}

Runtime theme variants

The themes provided by Dojo have always been built using CSS variables that define aspects such as color and sizing, however, it has not been easily possible to create a variant of a theme by providing a separate set of CSS variables. This is due to the variable being defined at the :root level on the document body.

Dojo 7 introduces some changes designed to solve this issue. Variables are no longer required to get imported into each individual CSS file, instead the theme itself has been updated to contain both the theme CSS for each widget and a set of variants that specify all the required CSS variables. These variables are no longer defined at the :root: but instead under a class named .root. When setting a theme for a widget or application, the specific variant can get set beside it and is made available by the theme middleware or Themed mixin using theme.variant() and this.variant() respectively. This "variant" class is required to get set on the outer node of a widget to ensure that the widget uses the correct CSS variables.

import * as MyWidget from './MyWidget.m.css';

import * as light from './variants/light.m.css';
import * as dark from './variants/dark.m.css';

export default {
    theme: {
        'my-project/MyWidget'
    },
    variants: {
        default: light,
        light,
        dark
    }
}

And using the theme.variant() function to apply the current variant to the root of the widget:

const MyWidget = factory(function MyWidget({ middleware: { theme } }) {
    const themedCss = theme.classes(css);
    return (
        <div classes={[theme.variant(), themedCss.root]}>
    );
});

With widgets setup to use the variant's class name, switching the variant at runtime gets done by using the theme middleware:

import myTheme from './my-theme';

const App = factory(function App({ middleware: { theme } }) {
    return (
        <button
            onclick={() => {
                theme.set(myTheme, 'dark');
            }}
        >
            dark mode
        </button>
    );
});

Dojo CLI Improvements

Two big improvements to using the Dojo CLI are included in version 7, the first is validation for .dojorc config values. If there are unsupported or incorrect config values detected, an error gets output to the console. The second is support for composing .dojorc configuration to reduce duplication and maintenance using the extends key pointing to the config from which to extend.

Better BTR developer experience

Dojo 7 introduces some significant improvements to the developer experience when working with build time rendering. The first is a new option, that is enabled by default to automatically discover pages to build, using the build-time-render options in the .dojorc, discoverPaths. The second is an on demand build time rendering mode that is turned on when working with the watch and serve cli-build-app flag. After the initial build, pages will only be built when they are visited in the browser, significantly speeding up the development experience.

Dojo Parade, show off your widgets

Dojo Parade is a brand new package for building widget documentation and examples from within your application or widget library. @dojo/widgets is using Dojo Parade for its documentation, that can be seen at https://widgets.dojo.io, it's early days for Dojo Parade and we have lots of ideas on how we can improve on what we have now, but we wanted to get this released early so we can hear feedback and ideas from our community and most importantly everyone can benefit from thorough documentation for their widgets.

Creating widget libraries

To complement the cli-create-app command, the existing cli-create-widget command has been converted to fulfill a higher destiny. The new cli-create-widget command in Dojo 7 will scaffold a skeleton widget library with all the tooling and documentation that we use in @dojo/widgets. We really hope this helps kickstart community-led widget projects!

dojo create widget --name my-dojo-widget-lib

Update to TypeScript Support

Dojo 7 has been tested and verified up to the latest released version, currently TypeScript 3.8, which includes enhancements such as optional chaining that landed in TypeScript 3.7. Dojo continues to support TypeScript 3.4.5 and greater.

Seamless Vercel Support

Over the last couple of months we have been working with the Vercel (previously ZEIT) team to get Dojo setup for seamless, zero configuration Vercel deployment, it's now even easier to deploy your next Dojo application.

What's next?

After eight busy months, it’s a pleasure to announce the release, but rest assured the work is not done, and over the next few weeks, we’ll be planning the goals for the next release and updating the Dojo roadmap.

Migration

As usual with modern Dojo releases, all breaking changes introduced in Dojo releases are carefully considered, so that we truly believe the benefits outweigh the upgrade effort. To assist with the transition, we have updated the CLI upgrade command, which will automatically upgrade your Dojo dependencies, upgrade your application code where possible, and highlight areas in the application that require manual intervention. For more information on what has changed in Dojo 7, please see the version 6 to 7 migration guide.

Support

See the Dojo version 7 release notes for more details on version 7.0.0 of Dojo! Love what we’re doing or having a problem? We ❤️ our community. Reach out to the Dojo team on Discord, check out the Dojo roadmap and see where we are heading, and try out Dojo 7.0.0 on CodeSandbox. We look forward to your feedback!