Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal for functional Widgets and Render Middleware #349

Closed
matt-gadd opened this issue May 21, 2019 · 6 comments

Comments

@matt-gadd
Copy link
Contributor

commented May 21, 2019

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
@devpaul

This comment has been minimized.

Copy link
Member

commented May 23, 2019

Can you provide some examples of how we can use middleware and functional widgets? In particular I'm interested in seeing examples for

  • Applying a theme to widgets
  • Internationalization
  • How will middleware DOM access work?
  • How would someone store local state? How would @watch be implemented?
  • How would @diffproperty be implemented to generate local state based on a property change?
  • Would Container and StoreProvider work the same or would middleware warrant a different/better approach?

Also had a few questions

  • If I have 3 middlewares and a renderer that all use the cache middleware, will only one instance of cache be available throughout the render instance or does each cache consumer get its own instance of cache?
  • Will the middleware have access to the Registry?
  • Will we be able to redefine middleware used by a renderer?
  • Can middleware be named with a symbol?
  • Since middlewares can use one another are we concerned with circular dependencies?
  • Will we write a migration (eventually) to migrate class-based renders to functional?

I really like the functional approach to creating widgets. The composition of middleware makes a lot more sense that trying to define a class that is then attached to the framework with decorators. Thanks in advance for answering my questions!

@agubler

This comment has been minimized.

Copy link
Member

commented May 24, 2019

@devpaul Thanks for all the questions, I'll do my best to answer them.

The TLDR is that we intend to provide a middleware equivalent for all the existing metas and mixins so that there is at least parity between classes and functional widgets.

Applying a theme to widgets

There will be a theme middleware that can be used to get theme classes, something similar to:

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

const render = create({ theme });
const MyWidget = render(({ middleware }) => {
    const { theme } = middleware;
    const classes = theme(css);
    return <div classes={[classes.root]} />
});

Internationalization

There will be a i18n middleware that is equivelant to the I18nMixin, with a similar pattern to the theme middleware above.

How will middleware DOM access work?

A low level dom core middleware will be provided by dojo/framework. This will then be used to build higher level user-land middleware such as dimensions, intersection, resize etc.

How would someone store local state? How would @watch be implemented?

Initially there will be a low level middleware, cache that provides a mechanism to store values across renders. On top of this it would then be possible to build a higher level state middleware.

How would @diffproperty be implemented to generate local state based on a property change?

This is advanced widget'ing 😄 There will be a core middleware that will deal with registering a function to run at the point of properties being set, this would allow a higher level primitive such as diffProperty to be built.

Would Container and StoreProvider work the same or would middleware warrant a different/better approach?

The StoreProvider and Container will still be available when using classes, however we are investigating the possibility of a "store" middleware that would be the recommended pattern with functional components.

If I have 3 middlewares and a renderer that all use the cache middleware, will only one instance of cache be available throughout the render instance or does each cache consumer get its own instance of cache?

That very much depends on the implementation of cache or any other middlware. The factory function is called per usage though.

Will the middleware have access to the Registry?

It won't have direct access to the registry but there will be a registry middleware that can be used to provide access to the registry.

Will we be able to redefine middleware used by a renderer?

I'm not 100% sure what this means.

Can middleware be named with a symbol?

I don't think so, and it's not something we'd recommend anyway.

Since middlewares can use one another are we concerned with circular dependencies?

Since middleware is composed by registering them when creating the factory, I don't think it's possible to create a circular (or not easily possible at least).

Will we write a migration (eventually) to migrate class-based renders to functional?

That would be ideal but alas I think it would be very difficult to do so reliably, if possible at all.

I hope that helps with some/all of your question, certainly a lot of these details will become clearer as the middleware is built.

@devpaul

This comment has been minimized.

Copy link
Member

commented Jun 1, 2019

Thanks so much for providing all these details. I'm looking forward to getting my hands on these features :).

I had a moment to ask @agubler some questions outside of this issue about how the middleware system connects to larger Dojo framework; e.g. how does middleware gain access to the registry. The answer is there is a set of base factories that provide this access: https://github.com/dojo/labs/blob/master/packages/framework/src/widget-core/middleware/base.ts#L6.

@agubler regarding this question:

Will we be able to redefine middleware used by a renderer?

What I was getting at here is the case where I want to create a standard implementation of a middleware but later replace it with a different internal implementation. This would likely be rare in a production sense. Dojo will mostly be used for creating applications, so environments will likely be fully known and middleware could be written to those environments. But library and widget builders may want to allow middleware to be replaced. For instance, if I wrote a middleware to access the camera and a renderer that used it I might write a WebRTC implementation, but might want to allow a different middleware for other environments like Cordova or UWP that have different APIs but could conform to the same middleware interface.

Replacing middleware would also be useful for testing. For classical widgets mocking Metas is relatively straight-forward. How would mocking middleware and testing renderers work?

@agubler agubler added Epic and removed discussion labels Jun 5, 2019

@agubler

This comment has been minimized.

Copy link
Member

commented Jul 3, 2019

@devpaul Sorry about the delays in answering

What I was getting at here is the case where I want to create a standard implementation of a middleware but later replace it with a different internal implementation. This would likely be rare in a production sense. Dojo will mostly be used for creating applications, so environments will likely be fully known and middleware could be written to those environments. But library and widget builders may want to allow middleware to be replaced. For instance, if I wrote a middleware to access the camera and a renderer that used it I might write a WebRTC implementation, but might want to allow a different middleware for other environments like Cordova or UWP that have different APIs but could conform to the same middleware interface.

I think that the author would need to provide multiple implementations of the widget using the right middleware for each targeted implementation. Or write a middleware that can conditionally handle the supported targets. The middleware is injected by the rendering engine, so it cannot be swapped out once it has been defined in the widgets creating factory, create({ myMiddleware });

Replacing middleware would also be useful for testing. For classical widgets mocking Metas is relatively straight-forward. How would mocking middleware and testing renderers work?

The Dojo testing harness has been enhanced to support passing middleware mocks, there are examples in the stock middleware mock test here. When creating the harness an array "real" middleware against "mock" middleware tuples can be passed, mock middlewares are created using the same factory as real middleware.

const h = harness(() => <MyWidget>), { 
    middleware: [ [ store, mockStore ], [ otherMiddlware, mockOtherMiddleware ] ] 
});

Hope this helps!

@devpaul

This comment has been minimized.

Copy link
Member

commented Jul 16, 2019

I like how the testing harness support middleware mocks. Compartmentalizing off the render from logic will make it easier to test. Looking forward to this!

Thanks for answering my long list of questions @agubler ! This helps a lot.

@agubler

This comment has been minimized.

Copy link
Member

commented Aug 28, 2019

Delivered in Dojo 6 🎉

@agubler agubler closed this Aug 28, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants
You can’t perform that action at this time.