Skip to content

Proposal for functional Widgets and Render Middleware #349

@matt-gadd

Description

@matt-gadd

Proposal for functional Widgets and Render Middleware

Introduction

As part of 6.0 we want to offer a simpler way of constructing widgets. This would be moving away from Classes over to a factory and render function. This allows us to really make the user focus on writing simple reactive Widgets, and not using things like Class constructors or WidgetBase lifecycles in exotic ways. Having a render function imposes some limits on the existing extensibility patterns we currently use:

Existing Widget extensibility patterns

Since launching modern dojo, we've had a number of patterns that provide extensibility and funtionality to widgets that include the following:

  • Decorators - we have a small amount of decorators in widget-core for attaching to some lifecycle events. We have never comprehensively committed to decorators due to limitations on how they can affect types and that they can only be used on classes.

  • Mixins - the main reason we leverage mixins for some behaviour is the ability for them to augment a users widget properties. They also have access to the same lifecycle as the widget would have and can provide utility functions off of the class. In terms of authoring they are not particularly nice to type and you cannot compose mixins inside other mixins.

  • Meta - meta is currently a specific pattern used for making imperative dom api's reactive. The meta's are both flexible in terms of the api they can provide, and type well. They're used inside a render function making them logical and not dependent on other lifecycles. Meta's are currently not composable and have a very limited dom lifecycle api available.

Render Middleware

When supporting widgets without classes, both Decorators and Mixins are no longer applicable. Which leaves us with the Meta pattern. The Meta pattern can be expanded to be more generic beyond dom api's, and with later TypeScript versions it's also possible to influence the Widget Properties like the Mixin Pattern. This means we should be able to convert the existing Mixins to a Meta like implementation.

Given the new power of the Meta like pattern, we are proposing a more generic name of "Render Middleware" (this is open to suggestions though). As well as now being able to influence Widget Properties, this new pattern will be composable ie being able to use middlewares inside of middlewares, something the current Meta pattern does not support.

On top of this the Middleware aligns nicely/symmetrically with functional Widgets, in that they both use the same creation factory, and both receive the same arguments to the user implemented function. The only difference being that functional Widgets return a RenderResult (DNodes etc), and Middleware returns a user defined API. This alignment on patterns across the board should make it simple for us to explain generally.

Usage

The new functional Widget and Render Middleware API would look like so:

Example Widget (Button):

import cache from './middleware/cache';

const render = create({ cache }).properties<ButtonProperties>();
const Button = render(({ properties, middleware }) => {
    const { label, onClick } = properties;
    const { cache } = middleware;
    cache.set('anything', 'value');
    return (
        <div>
            <button onclick={ onClick }>{ label }</button>
        </div>
    );
});

Example Render Middleware (Cache):

import other from './middleware/other';

const middleware = create({ other });
const cache = middleware(({ properties, middleware }) => {
    const cacheMap = new Map<string, any>();
    const { other } = middleware;
    return {
        get<T = any>(key: string): T | null {
            other.whatever();
            return cacheMap.get(key);
        },
        set<T = any>(key: string, value: T): void {
            cacheMap.set(key, value);
        }
    };
});

Notice both patterns use the same factory, and both receive the same arguments to the function body. They both declare what middleware they need in the create() factory, and are injected into the function body as middleware.

Summary

This new pattern hopefully consolidates a number of existing patterns and makes writing both widgets and supporting extensions more ergonomic. This is a change that is all additive (non breaking), but the new middleware pattern will only be supported in functional widgets.

Changes needed

  • dojo/framework support for TypeScript 3.4
  • Core changes for new authoring pattern
  • Core middleware primitives (invalidator, destroy, getRegistry, defer, diffProperty, node)
  • harness testing support
  • webpack-contrib changes to support code splitting for functional widgets
  • webpack-contrib changes to support custom elements for functional widgets
  • Middleware for all existing Mixins and Meta's in widget-core
  • Documentation/Reference guide updates
  • Update dojo/examples to use new authoring pattern
  • Update dojo/widgets to use new authoring pattern
  • cli-create-app and cli-create-widget to use new authoring pattern
  • codesandbox template

Metadata

Metadata

Assignees

No one assigned

    Labels

    Epicarea: coreCoreenhancementNew feature or requestnextIssue/Pull Request for the next major version

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions