Using MVVM events model to create a dynamic toolbar

Some time ago I wrote an article on a method for handling events in model classes when applying the MVVM pattern.

If you haven't already done so, I suggest you read it before going any further.

Today I'd like to show you how you can use this method to inject one view component into another, thus creating a dynamic toolbar.

This is the principle behind ribbons as implemented in Word or Autocad. The contents of the ribbon automatically adapt to the context of use. Some generic tools are always available, while others are added or removed depending on the context.

Demo app

Let's see how to implement this principle in the toolbar of a small demo web application. The main toolbar contains a menu of 3 pages, Home, Document, Charts. When you display the Document page justification commands are injected into the toolbar, which you can use to format the text. When you display the Charts page the commands injected into the toolbar offer different types of graph to render the datas.

To keep the focus on the essentials, I haven't implemented a model tree. So don't be surprised if it's the view components that initialize their own model in their constructors. This would obviously not be coded like this in a "real" application.

MainToolbar is the main toolbar. It contains the application menu and NestedToolbar, the component responsible for displaying contextual tools. Each page requiring a specific toolbar has its own dedicated toolbar component. DocumentToolbar for the Document page, ChartsToolbar for the Charts page. NestedToolbar is a simple switcher that displays the component corresponding to the current model type.

<!-- NestedToolbar -->
@switch (model.type) {
    @case (NestedToolbarType.document) {
        <app-document-toolbar [model]="model"/>
    }
    @case (NestedToolbarType.charts) {
        <app-charts-toolbar [model]="model"/>
    }
}

Then it's all about events.

Component injection

The instantiation of a page's view model triggers an AppEventsEnum.nestedToolbarAvailable event, requesting the injection of specific tools into the NestedToolbar. The view model attaches an instance of the dedicated toolbar view model class to the event. The home page, which does not require a specific tool, passes an undefined view model, which will empty the NestedToolbar.

// DocumentVM ctor
constructor() {
	...
    this.emitEventAsync(AppEventsEnum.nestedToolbarAvailable, new DocumentToolbarVM());
    ...
}

On the other hand, the MainToolbar listens to this same AppEventsEnum.nestedToolbarAvailable event and retrieves the instance of the dedicated toolbar view model, triggering its display.

// MainToolbarVM ctor
constructor() {
    ...
    this.addEventListener(AppEventsEnum.nestedToolbarAvailable, (nestedToolbar: NestedToolbarBase) => {
        this.nestedToolbar = nestedToolbar;
    });
}

Action binding

When instantiated, the view model of a dedicated toolbar emits events related to the use of its commands and sends the necessary arguments, in this case the text justification value.

onOptionChange(e: SelectButtonChangeEvent): void {
    this.emitEventAsync(AppEventsEnum.documentToolbarOptionChange, e.value.justify);
}

On the other hand, the view model of the current page listens to these events and triggers the relevant actions.

// DocumentVM ctor
constructor() {
	...
    this.addEventListener(AppEventsEnum.documentToolbarOptionChange, (justify: string) => {
        this.justify = justify;
    });
}

And voilà!

This architecture, although very simple, is already fully functional. It can be reworked to handle more complex cases, and nested nested toolbars are even possible.

Thanks for checking by!