Skip to content

Commit

Permalink
New VDOM Rendering Engine (#58)
Browse files Browse the repository at this point in the history
* Initial vdom rewrite

* refactor

* vdom

* Additional vdom changes

* More changes to vdom to support merges correctly

* Refactors

* More refactors

* Fix dom vnode logic and refactor property/attribute/event updates

* more refactors

* Use original properties passed to the widget when creating the instruction for an invalidation

* Do not try to merge for custom elements

* Move dom application to after the invalidation queue has been processed

* Run deferred properties in WidgetBase

* README

* Clean up and tests for ProjectorMixin

* Move invalidate back to instanceData

* Uncomment tests

* Move from class to function for vdom

* vdom reorg and tweaking

* README

* Add back remaining vdom unit tests

* refactor and fixes to ensure the latest wrapper is used

* Run filtering before clearing the node handler to allow meta usage in deferred props

* Move registry resolution to WidgetBase from vdom

* Create attach application for operations on a widget after it has been appended

* Fix insert before calculation to ensure results of widgets added to the correct position

* Falsy dnodes are now filtered by widgetbase, so deal with this in asserting render

* more tidying
  • Loading branch information
agubler authored Aug 16, 2018
1 parent bf03a16 commit b8a3dd8
Show file tree
Hide file tree
Showing 30 changed files with 2,880 additions and 3,372 deletions.
30 changes: 15 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions src/testing/support/assertRender.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DNode, WNode, VNode, DefaultWidgetBaseInterface, Constructor } from '../../widget-core/interfaces';
import { isWNode } from '../../widget-core/d';
import { isWNode, isVNode } from '../../widget-core/d';
import * as diff from 'diff';
import WeakMap from '../../shim/WeakMap';
import Set from '../../shim/Set';
Expand Down Expand Up @@ -30,21 +30,28 @@ export function formatDNodes(nodes: DNode | DNode[], depth: number = 0): string
for (let i = 0; i < depth; i++) {
tabs = `${tabs}\t`;
}
let requiresCarriageReturn = false;
let formattedNode = nodes.reduce((result: string, node, index) => {
if (node === null || node === undefined) {
if (!node) {
return result;
}
if (index > 0) {
if (requiresCarriageReturn) {
result = `${result}\n`;
} else {
requiresCarriageReturn = true;
}
result = `${result}${tabs}`;

if (typeof node === 'string') {
return `${result}"${node}"`;
}

if (isVNode(node) && node.text) {
return `${result}"${node.text}"`;
}

result = `${result}${formatNode(node, tabs)}`;
if (node.children && node.children.length > 0) {
if (node.children && node.children.some((child) => !!child)) {
result = `${result}, [\n${formatDNodes(node.children, depth + 1)}\n${tabs}]`;
}
return `${result})`;
Expand Down
67 changes: 41 additions & 26 deletions src/widget-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,30 @@ class HelloDojo extends WidgetBase {

#### Rendering a Widget in the DOM

To display your new component in the view you will need to decorate it with some functionality needed to "project" the widget into the browser. This is done using the `ProjectorMixin` from `@dojo/framework/widget-core/mixins/Projector`.
To display your new component in the view you will to use the `renderer` from the `@dojo/framework/widget-core/vdom` module. The `renderer` function accepts function that returns your component using the `w()` pragma and calling `.mount()` on the returned API.

```ts
const Projector = ProjectorMixin(HelloDojo);
const projector = new Projector();
import renderer from '@dojo/framework/widget-core/vdom';
import { w } from '@dojo/framework/widget-core/d';

projector.append();
const r = renderer(() => w(HelloDojo, {}));
r.mount();
```

By default, the projector will attach the widget to the `document.body` in the DOM, but this can be overridden by passing a reference to the preferred parent DOM Element.
`renderer#mount` accepts an optional argument of `MountOptions` that controls configuration of the mount operation.

```ts
interface MountOptions {
sync: boolean; // (default `false`)

merge: boolean; // (default `true`)

domNode: HTMLElement; // (default `document.body)

transition: TransitionStrategy; // (default `cssTransitions`)
}

The renderer by default mounts to the `document.body` in the DOM, but this can be overridden by passing the preferred target dom node to the `.mount()` function.

Consider the following in your HTML file:

Expand All @@ -81,11 +95,12 @@ Consider the following in your HTML file:
You can target this Element:

```ts
const root = document.getElementById('my-app');
const Projector = ProjectorMixin(HelloDojo);
const projector = new Projector();
import renderer from '@dojo/framework/widget-core/vdom';
import { w } from '@dojo/framework/widget-core/d';
projector.append(root);
const root = document.getElementById('my-app');
const r = renderer(() => w(HelloDojo, {}));
r.mount({ domNode: root });
```

#### Widgets and Properties
Expand Down Expand Up @@ -130,17 +145,13 @@ class App extends WidgetBase {
}
```

We can now use `App` with the `ProjectorMixin` to render the `Hello` widgets.
We can now use `App` with the `renderer` to display the `Hello` widgets.

```ts
const Projector = ProjectorMixin(App);
const projector = new Projector();

projector.append();
const r = renderer(() => w(App, {}));
r.mount({ domNode: root });
```

**Note:** Widgets must return a single top-level `DNode` from the `render` method, which is why the `Hello` widgets were wrapped in a `div` element.

#### Decomposing Widgets

Splitting widgets into multiple smaller widgets is easy and helps to add extended functionality and promotes reuse.
Expand Down Expand Up @@ -197,7 +208,7 @@ interface ListItemProperties {
id: string;
content: string;
highlighted: boolean;
onItemClick: (id: string) => void;
onItemClick(id: string) => void;
}

class ListItem extends WidgetBase<ListItemProperties> {
Expand Down Expand Up @@ -226,7 +237,7 @@ interface ListProperties {
content: string;
highlighted: boolean;
};
onItemClick: (id: string) => void;
onItemClick(id: string) => void;
}

class List extends WidgetBase<ListProperties> {
Expand Down Expand Up @@ -596,7 +607,7 @@ These are some of the **important** principles to keep in mind when creating and
1. The widget's *`__render__`*, *`__setProperties__`*, *`__setChildren__`* functions should **never** be called or overridden.
- These are the internal methods of the widget APIs and their behavior can change in the future, causing regressions in your application.
2. Except for projectors, you should **never** need to deal directly with widget instances
2. You should **never** need to deal directly with widget instances
- The Dojo widget system manages all instances required including caching and destruction, trying to create and manage other widgets will cause issues and will not work as expected.
3. **Never** update `properties` within a widget instance, they should be considered pure.
- Properties are considered read-only and should not be updated within a widget instance, updating properties could cause unexpected behavior and introduce bugs in your application.
Expand Down Expand Up @@ -798,23 +809,27 @@ class MyWidget extends WidgetBase {
The `Registry` provides a mechanism to define widgets and injectors (see the [`Containers & Injectors`](#containers--injectors) section), that can be dynamically/lazily loaded on request. Once the registry widget is loaded all widgets that need the newly loaded widget will be invalidated and re-rendered automatically.
A main registry can be provided to the `projector`, which will be automatically passed to all widgets within the tree (referred to as `baseRegistry`). Each widget also gets access to a private `Registry` instance that can be used to define registry items that are scoped to the widget. The locally defined registry items are considered a higher precedence than an item registered in the `baseRegistry`.
A main registry can be provided to the `renderer`, which will be automatically passed to all widgets within the tree (referred to as `baseRegistry`). Each widget also gets access to a private `Registry` instance that can be used to define registry items that are scoped to the widget. The locally defined registry items are considered a higher precedence than an item registered in the `baseRegistry`.
```ts
import { Registry } from '@dojo/framework/widget-core/Registry';
import { w } from '@dojo/framework/widget-core/d';

import { MyWidget } from './MyWidget';
import { MyAppContext } from './MyAppContext';
import MyWidget from './MyWidget';
import MyAppContext from './MyAppContext';
import App from './App';

const registry = new Registry();

registry.define('my-widget', MyWidget);

registry.defineInjector('my-injector', (invalidator) => {
const appContext = new MyAppContext(invalidator);
return () => appContext;
});
// ... Mixin and create Projector ...

projector.setProperties({ registry });
const r = renderer(() => w(App, {}));
r.registry = registry;
```
In some scenarios, it might be desirable to allow the `baseRegistry` to override an item defined in the local `registry`. Use true as the second argument of the registry.get function to override the local item.
Expand Down Expand Up @@ -970,7 +985,7 @@ class MyClass extends WidgetBase {
### Containers & Injectors
There is built-in support for side-loading/injecting values into sections of the widget tree and mapping them to a widget's properties. This is achieved by registering an injector factory with a `registry` and setting the registry as a property on the application's `projector` to ensure the registry instance is available to your application.
There is built-in support for side-loading/injecting values into sections of the widget tree and mapping them to a widget's properties. This is achieved by registering an injector factory with a `registry` and setting the registry on the application's `renderer` to ensure the registry instance is available to your application.
Create a factory function for a function that returns the required `payload`.
Expand Down Expand Up @@ -1570,7 +1585,7 @@ Your widget will be registered with the browser using the provided tag name. The
##### Initialization
Custom logic can be performed after properties/attributes have been defined but before the projector is created. This
Custom logic can be performed after properties/attributes have been defined but before the custom element is rendered. This
allows you full control over your widget, allowing you to add custom properties, event handlers, work with child nodes, etc.
The initialization function is run from the context of the HTML element.
Expand Down
Loading

0 comments on commit b8a3dd8

Please sign in to comment.