DEV Community

Cover image for Dojo Containers
Rene Rubalcava
Rene Rubalcava

Posted on • Originally published at learn-dojo.com

Dojo Containers

Once you start building applications that begin to compose multiple widgets and you are trying to manage state across those widgets, you might want to start looking at Dojo Containers. Containers allow you to inject values into widget properties, without having to import state directly into your widget.

To do this, Dojo provides a higher order component, similar to what you might use with React. That HOC is located in the @dojo/framework/widget-core/Container.

Let's say that you wanted to work with a streaming API and update your widget when the stream returns new data. We want to display this data in a simple list.

// src/widgets/Items.ts
export class Items extends WidgetBase<ItemsProperties> {
  protected render() {
    const { items } = this.properties;
    return v(
      "ul",
      { classes: css.root },
      items.map((x, idx) =>
        v("li", { innerHTML: x.name, key: `${x.name}-${idx}` })
      )
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This widget has an items array in the properties. You could bind this widget directly a data store and update the widget when new data comes in, but again, maybe we want that data available in the parent widget, or other widgets in use.

Let's create a parent Application container that will render this widget.

// src/containers/AppContainer.ts
class AppContainer extends WidgetBase<ItemsProperties> {
  protected render() {
    return v("div", {}, [w(Items, { items: this.properties.items })]);
  }
}
Enter fullscreen mode Exit fullscreen mode

This particular container is not doing much other than passing its properties to the child Items widget.

To use the Dojo Container, we need to create a getProperties function that defines the properties returned to the Container.

// src/containers/AppContainer.ts
function getProperties(inject: Context, properties: any) {
  const { items } = inject;
  return { items };
}
Enter fullscreen mode Exit fullscreen mode

Now we can wrap our AppContainer in the Dojo Container.

// src/containers/AppContainer.ts
export default Container(AppContainer, "state", { getProperties });
Enter fullscreen mode Exit fullscreen mode

In this case "state" is the name I'm providing for my context, which I refer to as my injector since it allows me to inject values into my widgets.

At this point, you have an option for how to manage your state. You can use Dojo stores or you can create a class that accepts an invalidator and you can use this invalidator to let the higher order component know that state has changed and it will pass it to the widget that it has wrapped.

For now, let's go with a class that takes an invalidator and call it a context for our container. We can cover Dojo stores in another post.

// src/context.ts
export default class Context {
  private _items: Item[];

  private _invalidator: () => void;

  constructor(invalidator: () => {}, items: Item[] = []) {
    this._items = items;
    this._invalidator = invalidator;
    // subscribe to updates from our stream
    stream.subscribe((a: Item) => {
      this._addItem(a);
    });
  }

  get items(): Item[] {
    return this._items;
  }

  private _addItem(item: Item) {
    this._items = [...this._items, item];
    // call the invalidator to update wrapped container
    this._invalidator();
  }
}
Enter fullscreen mode Exit fullscreen mode

It's in this Context that I am subscribing to my data stream and updating the items array when new data is streamed in.

Ok, let's tie it all together in our main.ts that kick starts the whole application.

// src/main.ts
const registry = new Registry();
// the `defineInjector` will provider the invalidator
registry.defineInjector("state", (invalidator: () => any) => {
  // create a new context and return it
  const context = new Context(invalidator);
  return () => context;
});

const Projector = ProjectorMixin(AppContainer);
const projector = new Projector();
// pass the registry to the projector
projector.setProperties({ registry });

projector.append();
Enter fullscreen mode Exit fullscreen mode

When the Registry is passed to the projector, it will make sure everything is wired up as needed.

This may seem like a few steps, but it makes state management very flexible in your widgets without having to bind widgets to a data source, which makes them incredibly reusable.

You could create containers for each individual widget in your application and manage their state independently, this would be very powerful!

You can see a sample of this application above on CodeSandbox.

Be sure to subscribe to the newsletter and stay up to date with the latest content!

Top comments (0)