diff --git a/package-lock.json b/package-lock.json index a4ae27986..7e9370741 100644 --- a/package-lock.json +++ b/package-lock.json @@ -224,7 +224,7 @@ "dev": true, "requires": { "@types/connect": "3.4.32", - "@types/node": "9.6.28" + "@types/node": "9.6.27" } }, "@types/chai": { @@ -239,7 +239,7 @@ "integrity": "sha512-F9OalGhk60p/DnACfa1SWtmVTMni0+w9t/qfb5Bu7CsurkEjZFN7Z+ii/VGmYpaViPz7o3tBahRQae9O7skFlQ==", "dev": true, "requires": { - "@types/node": "9.6.28" + "@types/node": "9.6.27" } }, "@types/cldrjs": { @@ -253,7 +253,7 @@ "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", "dev": true, "requires": { - "@types/node": "9.6.28" + "@types/node": "9.6.27" } }, "@types/diff": { @@ -286,7 +286,7 @@ "dev": true, "requires": { "@types/events": "1.2.0", - "@types/node": "9.6.28", + "@types/node": "9.6.27", "@types/range-parser": "1.2.2" } }, @@ -357,9 +357,9 @@ } }, "@types/jquery": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.6.tgz", - "integrity": "sha512-403D4wN95Mtzt2EoQHARf5oe/jEPhzBOBNrunk+ydQGW8WmkQ/E8rViRAEB1qEt/vssfGfNVD6ujP4FVeegrLg==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.5.tgz", + "integrity": "sha512-18OnkBZ+9pOx8grC2w4i256VS+9j/Ya/N0DcWkZRgPrg7V2oolgk8n7790goBlnChL6nIXAXy1lBTrz/r4lJTg==", "dev": true }, "@types/jsdom": { @@ -368,8 +368,8 @@ "integrity": "sha512-xaHlMIzlReyciMIWGJBnkEdHngCOEpik2ojt9tJFe7rD+QiObCIcmr9/tAqxn7l1jflQ3wEIkh7+gt4ls5n1Dw==", "dev": true, "requires": { - "@types/jquery": "3.3.6", - "@types/node": "9.6.28" + "@types/jquery": "3.3.5", + "@types/node": "9.6.27" } }, "@types/jszip": { @@ -378,7 +378,7 @@ "integrity": "sha512-UaVbz4buRlBEolZYrxqkrGDOypugYlbqGNrUFB4qBaexrLypTH0jyvaF5jolNy5D+5C4kKV1WJ3Yx9cn/JH8oA==", "dev": true, "requires": { - "@types/node": "9.6.28" + "@types/node": "9.6.27" } }, "@types/lodash": { @@ -409,9 +409,9 @@ } }, "@types/node": { - "version": "9.6.28", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.28.tgz", - "integrity": "sha512-LMSOxMKNJ8tGqUVs8lSIT8RGo1XGWYada/ZU2QZcbcD6AW9futXDE99tfQA0K6DK60GXcwplsGGK5KABRmI5GA==", + "version": "9.6.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-9.6.27.tgz", + "integrity": "sha512-fGWGG9Wypv6JZLIrnq9jXFX/FhQzgNccTlojez9hBbQ9UiBdxtc0ONMMe4/vnB2nDgOMDpPR/7HhenUB+Bw5yQ==", "dev": true }, "@types/platform": { @@ -438,7 +438,7 @@ "integrity": "sha512-GPewdjkb0Q76o459qgp6pBLzJj/bD3oveS2kfLhIkZ9U3t3AFKtl5DlFB6lGTw0iZmcmxoGC8lpLW3NNJKrN9A==", "dev": true, "requires": { - "@types/node": "9.6.28" + "@types/node": "9.6.27" } }, "@types/selenium-webdriver": { @@ -487,7 +487,7 @@ "dev": true, "requires": { "@types/events": "1.2.0", - "@types/node": "9.6.28" + "@types/node": "9.6.27" } }, "@types/yargs": { diff --git a/src/testing/support/assertRender.ts b/src/testing/support/assertRender.ts index 1a34c41da..1865ea50c 100644 --- a/src/testing/support/assertRender.ts +++ b/src/testing/support/assertRender.ts @@ -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'; @@ -30,12 +30,15 @@ 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}`; @@ -43,8 +46,12 @@ export function formatDNodes(nodes: DNode | DNode[], depth: number = 0): 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})`; diff --git a/src/widget-core/README.md b/src/widget-core/README.md index 811910332..a525ce852 100644 --- a/src/widget-core/README.md +++ b/src/widget-core/README.md @@ -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: @@ -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 @@ -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. @@ -197,7 +208,7 @@ interface ListItemProperties { id: string; content: string; highlighted: boolean; - onItemClick: (id: string) => void; + onItemClick(id: string) => void; } class ListItem extends WidgetBase { @@ -226,7 +237,7 @@ interface ListProperties { content: string; highlighted: boolean; }; - onItemClick: (id: string) => void; + onItemClick(id: string) => void; } class List extends WidgetBase { @@ -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. @@ -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. @@ -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`. @@ -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. diff --git a/src/widget-core/WidgetBase.ts b/src/widget-core/WidgetBase.ts index 48c0cc94c..0f670250f 100644 --- a/src/widget-core/WidgetBase.ts +++ b/src/widget-core/WidgetBase.ts @@ -1,20 +1,21 @@ import Map from '../shim/Map'; import WeakMap from '../shim/WeakMap'; import Symbol from '../shim/Symbol'; -import { v } from './d'; +import { v, VNODE, isVNode, isWNode } from './d'; import { auto } from './diff'; import { AfterRender, BeforeProperties, BeforeRender, - CoreProperties, DiffPropertyReaction, DNode, Render, WidgetMetaBase, WidgetMetaConstructor, WidgetBaseInterface, - WidgetProperties + WidgetProperties, + WNode, + VNode } from './interfaces'; import RegistryHandler from './RegistryHandler'; import NodeHandler from './NodeHandler'; @@ -34,6 +35,16 @@ const boundAuto = auto.bind(null); export const noBind = Symbol.for('dojoNoBind'); +function toTextVNode(data: any): VNode { + return { + tag: '', + properties: {}, + children: undefined, + text: `${data}`, + type: VNODE + }; +} + /** * Main widget base for all widgets to extend */ @@ -105,14 +116,18 @@ export class WidgetBase

implement this.destroy(); }, nodeHandler: this._nodeHandler, - registry: () => { - return this.registry; - }, - coreProperties: {} as CoreProperties, rendering: false, inputProperties: {} }); + this.own({ + destroy: () => { + widgetInstanceMap.delete(this); + this._nodeHandler.clear(); + this._nodeHandler.destroy(); + } + }); + this._runAfterConstructors(); } @@ -150,25 +165,11 @@ export class WidgetBase

implement return [...this._changedPropertyKeys]; } - public __setCoreProperties__(coreProperties: CoreProperties): void { - const { baseRegistry } = coreProperties; - const instanceData = widgetInstanceMap.get(this)!; - - if (instanceData.coreProperties.baseRegistry !== baseRegistry) { - if (this._registry === undefined) { - this._registry = new RegistryHandler(); - this.own(this._registry); - this.own(this._registry.on('invalidate', this._boundInvalidate)); - } - this._registry.base = baseRegistry; - this.invalidate(); + public __setProperties__(originalProperties: this['properties'], bind?: WidgetBaseInterface): void { + const instanceData = widgetInstanceMap.get(this); + if (instanceData) { + instanceData.inputProperties = originalProperties; } - instanceData.coreProperties = coreProperties; - } - - public __setProperties__(originalProperties: this['properties']): void { - const instanceData = widgetInstanceMap.get(this)!; - instanceData.inputProperties = originalProperties; const properties = this._runBeforeProperties(originalProperties); const registeredDiffPropertyNames = this.getDecorator('registeredDiffProperty'); const changedPropertyKeys: string[] = []; @@ -187,10 +188,7 @@ export class WidgetBase

implement } checkedProperties.push(propertyName); const previousProperty = this._properties[propertyName]; - const newProperty = this._bindFunctionProperty( - properties[propertyName], - instanceData.coreProperties.bind - ); + const newProperty = this._bindFunctionProperty(properties[propertyName], bind); if (registeredDiffPropertyNames.indexOf(propertyName) !== -1) { runReactions = true; const diffFunctions = this.getDecorator(`diffProperty:${propertyName}`); @@ -233,10 +231,7 @@ export class WidgetBase

implement for (let i = 0; i < propertyNames.length; i++) { const propertyName = propertyNames[i]; if (typeof properties[propertyName] === 'function') { - properties[propertyName] = this._bindFunctionProperty( - properties[propertyName], - instanceData.coreProperties.bind - ); + properties[propertyName] = this._bindFunctionProperty(properties[propertyName], bind); } else { changedPropertyKeys.push(propertyName); } @@ -261,19 +256,56 @@ export class WidgetBase

implement } } - public __render__(): DNode | DNode[] { - const instanceData = widgetInstanceMap.get(this)!; - instanceData.dirty = false; + private _filterAndConvert(nodes: DNode[]): (WNode | VNode)[]; + private _filterAndConvert(nodes: DNode): WNode | VNode; + private _filterAndConvert(nodes: DNode | DNode[]): (WNode | VNode) | (WNode | VNode)[]; + private _filterAndConvert(nodes: DNode | DNode[]): (WNode | VNode) | (WNode | VNode)[] { + const isArray = Array.isArray(nodes); + const filteredNodes = Array.isArray(nodes) ? nodes : [nodes]; + const convertedNodes: (WNode | VNode)[] = []; + for (let i = 0; i < filteredNodes.length; i++) { + const node = filteredNodes[i]; + if (!node) { + continue; + } + if (typeof node === 'string') { + convertedNodes.push(toTextVNode(node)); + continue; + } + if (isVNode(node) && node.deferredPropertiesCallback) { + const properties = node.deferredPropertiesCallback(false); + node.originalProperties = node.properties; + node.properties = { ...properties, ...node.properties }; + } + if (isWNode(node) && !isWidgetBaseConstructor(node.widgetConstructor)) { + node.widgetConstructor = + this.registry.get(node.widgetConstructor) || node.widgetConstructor; + } + if (!node.bind) { + node.bind = this; + } + convertedNodes.push(node); + if (node.children && node.children.length) { + node.children = this._filterAndConvert(node.children); + } + } + return isArray ? convertedNodes : convertedNodes[0]; + } + + public __render__(): (WNode | VNode) | (WNode | VNode)[] { + const instanceData = widgetInstanceMap.get(this); + if (instanceData) { + instanceData.dirty = false; + } const render = this._runBeforeRenders(); - let dNode = render(); - dNode = this.runAfterRenders(dNode); + const dNode = this._filterAndConvert(this.runAfterRenders(render())); this._nodeHandler.clear(); return dNode; } public invalidate(): void { - const instanceData = widgetInstanceMap.get(this)!; - if (instanceData.invalidate) { + const instanceData = widgetInstanceMap.get(this); + if (instanceData && instanceData.invalidate) { instanceData.invalidate(); } } diff --git a/src/widget-core/d.ts b/src/widget-core/d.ts index e934f4858..ec46515d5 100644 --- a/src/widget-core/d.ts +++ b/src/widget-core/d.ts @@ -9,9 +9,10 @@ import { VNodeProperties, WidgetBaseInterface, WNode, - DomOptions + DomOptions, + RenderResult, + DomVNode } from './interfaces'; -import { InternalVNode, RenderResult } from './vdom'; /** * The symbol identifier for a WNode type @@ -47,7 +48,7 @@ export function isVNode(child: DNode): child is VNode { /** * Helper function that returns true if the `DNode` is a `VNode` created with `dom()` using the `type` property */ -export function isDomVNode(child: DNode): child is VNode { +export function isDomVNode(child: DNode): child is DomVNode { return Boolean(child && typeof child !== 'string' && child.type === DOMVNODE); } @@ -208,6 +209,7 @@ export function v( return { tag, deferredPropertiesCallback, + originalProperties: {}, children, properties, type: VNODE @@ -220,7 +222,7 @@ export function v( export function dom( { node, attrs = {}, props = {}, on = {}, diffType = 'none' }: DomOptions, children?: DNode[] -): VNode { +): DomVNode { return { tag: isElementNode(node) ? node.tagName.toLowerCase() : '', properties: props, @@ -231,5 +233,5 @@ export function dom( domNode: node, text: isElementNode(node) ? undefined : node.data, diffType - } as InternalVNode; + }; } diff --git a/src/widget-core/interfaces.d.ts b/src/widget-core/interfaces.d.ts index 74cfa3c62..3ae005da2 100644 --- a/src/widget-core/interfaces.d.ts +++ b/src/widget-core/interfaces.d.ts @@ -2,6 +2,7 @@ import { Destroyable } from '../core/Destroyable'; import { Evented, EventType, EventObject } from '../core/Evented'; import Map from '../shim/Map'; import WeakMap from '../shim/WeakMap'; +import { RegistryHandler } from './RegistryHandler'; /** * Generic constructor type @@ -100,6 +101,12 @@ export interface DomOptions { diffType?: DiffType; } +export interface VDomOptions { + props?: VNodeProperties; + attrs?: { [index: string]: string | undefined }; + on?: On; +} + export interface VNodeProperties { /** * The animation to perform when this node is added to an already existing parent. @@ -282,21 +289,6 @@ export interface KeyedWidgetProperties extends WidgetProperties { key: string | number; } -/** - * - */ -interface CoreProperties { - /** - * The default registry for the projection - */ - baseRegistry: any; - - /** - * The scope used to bind functions - */ - bind: any; -} - /** * Wrapper for v */ @@ -311,10 +303,15 @@ export interface VNode { */ properties: VNodeProperties; + /** + * VNode properties + */ + originalProperties?: VNodeProperties; + /** * VNode attributes */ - attributes?: { [index: string]: string }; + attributes?: { [index: string]: string | undefined }; /** * VNode events @@ -345,6 +342,15 @@ export interface VNode { * Indicates the type of diff for the VNode */ diffType?: DiffType; + + /** + * instance the created the vnode + */ + bind?: WidgetBaseInterface; +} + +export interface DomVNode extends VNode { + domNode: Text | Element; } /** @@ -370,6 +376,8 @@ export interface WNode | DNode[]; + export interface Render { (): DNode | DNode[]; } diff --git a/src/widget-core/mixins/Projector.ts b/src/widget-core/mixins/Projector.ts index 952bb10a0..86dc7dc61 100644 --- a/src/widget-core/mixins/Projector.ts +++ b/src/widget-core/mixins/Projector.ts @@ -1,152 +1,54 @@ -import { assign } from '../../shim/object'; -import cssTransitions from '../animations/cssTransitions'; -import { Constructor, DNode, Projection, ProjectionOptions } from './../interfaces'; -import { WidgetBase } from './../WidgetBase'; -import { afterRender } from './../decorators/afterRender'; -import { v } from './../d'; -import { Registry } from './../Registry'; -import { dom } from './../vdom'; +import { Constructor, DNode } from '../interfaces'; +import WidgetBase from '../WidgetBase'; import { Handle } from '../../core/Destroyable'; +import Registry from '../Registry'; +import { renderer } from './../vdom'; +import { w } from '../d'; -/** - * Represents the attach state of the projector - */ export enum ProjectorAttachState { Attached = 1, Detached } -/** - * Attach type for the projector - */ -export enum AttachType { - Append = 1, - Merge = 2 -} - -export interface AttachOptions { - /** - * If `'append'` it will appended to the root. If `'merge'` it will merged with the root. If `'replace'` it will - * replace the root. - */ - type: AttachType; - - /** - * Element to attach the projector. - */ - root?: Element; -} - export interface ProjectorProperties { registry?: Registry; } -export interface ProjectorMixin

{ - readonly properties: Readonly

& Readonly; - - /** - * Append the projector to the root. - */ +export interface ProjectorMixin { append(root?: Element): Handle; - - /** - * Merge the projector onto the root. - * - * The `root` and any of its `children` will be re-used. Any excess DOM nodes will be ignored and any missing DOM nodes - * will be created. - * @param root The root element that the root virtual DOM node will be merged with. Defaults to `document.body`. - */ merge(root?: Element): Handle; - - /** - * Attach the project to a _sandboxed_ document fragment that is not part of the DOM. - * - * When sandboxed, the `Projector` will run in a sync manner, where renders are completed within the same turn. - * The `Projector` creates a `DocumentFragment` which replaces any other `root` that has been set. - * @param doc The `Document` to use, which defaults to the global `document`. - */ sandbox(doc?: Document): void; - - /** - * Sets the properties for the widget. Responsible for calling the diffing functions for the properties against the - * previous properties. Runs though any registered specific property diff functions collecting the results and then - * runs the remainder through the catch all diff function. The aggregate of the two sets of the results is then - * set as the widget's properties - * - * @param properties The new widget properties - */ - setProperties(properties: this['properties']): void; - - /** - * Sets the widget's children - */ + setProperties(properties: T['properties'] & ProjectorProperties): void; setChildren(children: DNode[]): void; - - /** - * Return a `string` that represents the HTML of the current projection. The projector needs to be attached. - */ toHtml(): string; - - /** - * Indicates if the projectors is in async mode, configured to `true` by defaults. - */ async: boolean; - - /** - * Root element to attach the projector - */ root: Element; - - /** - * The status of the projector - */ - readonly projectorState: ProjectorAttachState; - - /** - * Runs registered destroy handles - */ destroy(): void; + readonly projectorState: ProjectorAttachState; } -export function ProjectorMixin>>(Base: T): T & Constructor> { - abstract class Projector extends Base { +export function ProjectorMixin>(Base: Constructor): Constructor> { + class Projector { public projectorState: ProjectorAttachState; - private _root: Element = document.body; private _async = true; - private _attachHandle: Handle | undefined; - private _projectionOptions: Partial; - private _projection: Projection | undefined; - private _projectorProperties: this['properties'] = {} as this['properties']; - public abstract properties: Readonly

& Readonly; - - constructor(...args: any[]) { - super(...args); + private _children: DNode[]; + private _properties: P & ProjectorProperties = {} as P; + private _widget: Constructor = Base; - this._projectionOptions = { - transitions: cssTransitions - }; - - this.root = document.body; - this.projectorState = ProjectorAttachState.Detached; - } - - public append(root?: Element): Handle { - const options = { - type: AttachType.Append, - root + public append(root: Element = this._root): Handle { + const { registry, ...props } = this._properties as any; + this._root = root; + const r = renderer(() => w(this._widget, props, this._children)); + r.mount({ domNode: root as HTMLElement, registry, sync: !this.async }); + this.projectorState = ProjectorAttachState.Attached; + return { + destroy() {} }; - - return this._attach(options); } - public merge(root?: Element): Handle { - const options = { - type: AttachType.Merge, - root - }; - - return this._attach(options); + public merge(root: Element = document.body): Handle { + return this.append((root.parentNode as Element) || undefined); } public set root(root: Element) { @@ -176,98 +78,25 @@ export function ProjectorMixin>>(Base: T) throw new Error('Projector already attached, cannot create sandbox'); } this._async = false; - const previousRoot = this.root; - - /* free up the document fragment for GC */ - this.own({ - destroy: () => { - this._root = previousRoot; - } - }); - - this._attach({ - /* DocumentFragment is not assignable to Element, but provides everything needed to work */ - root: doc.createDocumentFragment() as any, - type: AttachType.Append - }); + this.append(doc.createDocumentFragment() as any); } public setChildren(children: DNode[]): void { - this.__setChildren__(children); + this._children = children; } - public setProperties(properties: this['properties']): void { - this.__setProperties__(properties); - } - - public __setProperties__(properties: this['properties']): void { - if (this._projectorProperties && this._projectorProperties.registry !== properties.registry) { - if (this._projectorProperties.registry) { - this._projectorProperties.registry.destroy(); - } - } - this._projectorProperties = assign({}, properties); - super.__setCoreProperties__({ bind: this, baseRegistry: properties.registry }); - super.__setProperties__(properties); + public setProperties(properties: P): void { + this._properties = properties; } public toHtml(): string { - if (this.projectorState !== ProjectorAttachState.Attached || !this._projection) { + if (this.projectorState !== ProjectorAttachState.Attached) { throw new Error('Projector is not attached, cannot return an HTML string of projection.'); } - return (this._projection.domNode.childNodes[0] as Element).outerHTML; - } - - @afterRender() - public afterRender(result: DNode) { - let node = result; - if (typeof result === 'string' || result === null || result === undefined) { - node = v('span', {}, [result]); - } - - return node; - } - - public destroy() { - super.destroy(); + return (this._root.childNodes[0] as Element).outerHTML; } - private _attach({ type, root }: AttachOptions): Handle { - if (root) { - this.root = root; - } - - if (this._attachHandle) { - return this._attachHandle; - } - - this.projectorState = ProjectorAttachState.Attached; - - const handle = { - destroy: () => { - if (this.projectorState === ProjectorAttachState.Attached) { - this._projection = undefined; - this.projectorState = ProjectorAttachState.Detached; - } - } - }; - - this.own(handle); - this._attachHandle = handle; - - this._projectionOptions = { ...this._projectionOptions, ...{ sync: !this._async } }; - - switch (type) { - case AttachType.Append: - this._projection = dom.append(this.root, this, this._projectionOptions); - break; - case AttachType.Merge: - this._projection = dom.merge(this.root, this, this._projectionOptions); - break; - } - - return this._attachHandle; - } + public destroy() {} } return Projector; diff --git a/src/widget-core/registerCustomElement.ts b/src/widget-core/registerCustomElement.ts index 90075e954..315f11f6c 100644 --- a/src/widget-core/registerCustomElement.ts +++ b/src/widget-core/registerCustomElement.ts @@ -1,5 +1,5 @@ import { WidgetBase, noBind } from './WidgetBase'; -import { ProjectorMixin } from './mixins/Projector'; +import { renderer } from './vdom'; import { from } from '../shim/array'; import { w, dom } from './d'; import global from '../shim/global'; @@ -48,7 +48,7 @@ export function create(descriptor: any, WidgetConstructor: any): any { }); return class extends HTMLElement { - private _projector: any; + private _renderer: any; private _properties: any = {}; private _children: any[] = []; private _eventProperties: any = {}; @@ -134,10 +134,9 @@ export function create(descriptor: any, WidgetConstructor: any): any { const registry = registryFactory(); const themeContext = registerThemeInjector(this._getTheme(), registry); global.addEventListener('dojo-theme-set', () => themeContext.set(this._getTheme())); - const Projector = ProjectorMixin(Wrapper); - this._projector = new Projector(); - this._projector.setProperties({ registry }); - this._projector.append(this); + const r = renderer(() => w(Wrapper, {})); + this._renderer = r; + r.mount({ domNode: this, merge: false, registry }); this._initialised = true; this.dispatchEvent( @@ -167,8 +166,8 @@ export function create(descriptor: any, WidgetConstructor: any): any { } private _render() { - if (this._projector) { - this._projector.invalidate(); + if (this._renderer) { + this._renderer.invalidate(); this.dispatchEvent( new CustomEvent('dojo-ce-render', { bubbles: false, diff --git a/src/widget-core/vdom.ts b/src/widget-core/vdom.ts index ce5e9c9a1..0e14a5a82 100644 --- a/src/widget-core/vdom.ts +++ b/src/widget-core/vdom.ts @@ -1,235 +1,213 @@ import global from '../shim/global'; +import { WeakMap } from '../shim/WeakMap'; import { - CoreProperties, - DefaultWidgetBaseInterface, - DNode, - VNode, WNode, - ProjectionOptions, - Projection, + VNode, + DNode, + VNodeProperties, SupportedClassName, + WidgetBaseConstructor, TransitionStrategy, - VNodeProperties + WidgetProperties, + DefaultWidgetBaseInterface } from './interfaces'; -import { from as arrayFrom } from '../shim/array'; -import { isWNode, isVNode, isDomVNode, VNODE, WNODE } from './d'; -import { isWidgetBaseConstructor } from './Registry'; -import WeakMap from '../shim/WeakMap'; +import transitionStrategy from './animations/cssTransitions'; +import { isVNode, isWNode, WNODE, v, isDomVNode } from './d'; +import { Registry, isWidgetBaseConstructor } from './Registry'; +import { WidgetBase } from './WidgetBase'; import NodeHandler from './NodeHandler'; -import RegistryHandler from './RegistryHandler'; -const NAMESPACE_W3 = 'http://www.w3.org/'; -const NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; -const NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; +export interface WidgetData { + onDetach: () => void; + onAttach: () => void; + dirty: boolean; + nodeHandler: NodeHandler; + invalidate?: Function; + rendering: boolean; + inputProperties: any; +} -const emptyArray: (InternalWNode | InternalVNode)[] = []; +export interface BaseNodeWrapper { + node: WNode | VNode; + domNode?: Node; + childrenWrappers?: DNodeWrapper[]; + depth: number; + requiresInsertBefore?: boolean; + hasPreviousSiblings?: boolean; + hasParentWNode?: boolean; + namespace?: string; + hasAnimations?: boolean; +} -const nodeOperations = ['focus', 'blur', 'scrollIntoView', 'click']; +export interface WNodeWrapper extends BaseNodeWrapper { + node: WNode; + instance?: WidgetBase; + mergeNodes?: Node[]; + nodeHandlerCalled?: boolean; +} + +export interface VNodeWrapper extends BaseNodeWrapper { + node: VNode; + merged?: boolean; + decoratedDeferredProperties?: VNodeProperties; + inserted?: boolean; +} -export type RenderResult = DNode | DNode[]; +export type DNodeWrapper = VNodeWrapper | WNodeWrapper; -interface InstanceMapData { - parentVNode: InternalVNode; - dnode: InternalWNode; +export interface MountOptions { + sync: boolean; + merge: boolean; + transition: TransitionStrategy; + domNode: HTMLElement; + registry: Registry | null; } -export interface InternalWNode extends WNode { - /** - * The instance of the widget - */ - instance: DefaultWidgetBaseInterface; - - /** - * The rendered DNodes from the instance - */ - rendered: InternalDNode[]; - - /** - * Core properties that are used by the widget core system - */ - coreProperties: CoreProperties; - - /** - * Children for the WNode - */ - children: InternalDNode[]; +export interface Renderer { + invalidate(): void; + mount(mountOptions?: Partial): void; } -export interface InternalVNode extends VNode { - /** - * Children for the VNode - */ - children?: InternalDNode[]; +interface ProcessItem { + current?: (WNodeWrapper | VNodeWrapper)[]; + next?: (WNodeWrapper | VNodeWrapper)[]; + meta: ProcessMeta; +} - inserted?: boolean; +interface ProcessResult { + item?: ProcessItem; + widget?: AttachApplication; + dom?: ApplicationInstruction; +} - /** - * Bag used to still decorate properties on a deferred properties callback - */ - decoratedDeferredProperties?: VNodeProperties; +interface ProcessMeta { + mergeNodes?: Node[]; + oldIndex?: number; + newIndex?: number; +} - /** - * DOM element - */ - domNode?: Element | Text; +interface InvalidationQueueItem { + instance: WidgetBase; + depth: number; } -export type InternalDNode = InternalVNode | InternalWNode; +interface Instruction { + current: undefined | DNodeWrapper; + next: undefined | DNodeWrapper; +} -export interface RenderQueue { - instance: DefaultWidgetBaseInterface; - depth: number; +interface CreateWidgetInstruction { + next: WNodeWrapper; } -export interface WidgetData { - onDetach: () => void; - onAttach: () => void; - dirty: boolean; - registry: () => RegistryHandler; - nodeHandler: NodeHandler; - coreProperties: CoreProperties; - invalidate?: Function; - rendering: boolean; - inputProperties: any; +interface UpdateWidgetInstruction { + current: WNodeWrapper; + next: WNodeWrapper; } -interface ProjectorState { - deferredRenderCallbacks: Function[]; - afterRenderCallbacks: Function[]; - nodeMap: WeakMap>; - renderScheduled?: number; - renderQueue: RenderQueue[]; - merge: boolean; - mergeElement?: Node; +interface RemoveWidgetInstruction { + current: WNodeWrapper; } -export const widgetInstanceMap = new WeakMap(); +interface CreateDomInstruction { + next: VNodeWrapper; +} -const instanceMap = new WeakMap(); -const nextSiblingMap = new WeakMap(); -const projectorStateMap = new WeakMap(); +interface UpdateDomInstruction { + current: VNodeWrapper; + next: VNodeWrapper; +} -function same(dnode1: InternalDNode, dnode2: InternalDNode) { - if (isVNode(dnode1) && isVNode(dnode2)) { - if (isDomVNode(dnode1) || isDomVNode(dnode2)) { - if (dnode1.domNode !== dnode2.domNode) { - return false; - } - } - if (dnode1.tag !== dnode2.tag) { - return false; - } - if (dnode1.properties.key !== dnode2.properties.key) { - return false; - } - return true; - } else if (isWNode(dnode1) && isWNode(dnode2)) { - if (dnode1.instance === undefined && typeof dnode2.widgetConstructor === 'string') { - return false; - } - if (dnode1.widgetConstructor !== dnode2.widgetConstructor) { - return false; - } - if (dnode1.properties.key !== dnode2.properties.key) { - return false; - } - return true; - } - return false; +interface RemoveDomInstruction { + current: VNodeWrapper; } -const missingTransition = function() { - throw new Error('Provide a transitions object to the projectionOptions to do animations'); -}; - -function getProjectionOptions( - projectorOptions: Partial, - projectorInstance: DefaultWidgetBaseInterface -): ProjectionOptions { - const defaults: Partial = { - namespace: undefined, - styleApplyer: function(domNode: HTMLElement, styleName: string, value: string) { - (domNode.style as any)[styleName] = value; - }, - transitions: { - enter: missingTransition, - exit: missingTransition - }, - depth: 0, - merge: false, - sync: false, - projectorInstance - }; - return { ...defaults, ...projectorOptions } as ProjectionOptions; +interface AttachApplication { + type: 'attach'; + instance: WidgetBase; + attached: boolean; } -function checkStyleValue(styleValue: Object) { - if (typeof styleValue !== 'string') { - throw new Error('Style values must be strings'); - } +interface CreateDomApplication { + type: 'create'; + current?: VNodeWrapper; + next: VNodeWrapper; + parentDomNode: Node; } -function updateEvent( - domNode: Node, - eventName: string, - currentValue: Function, - projectionOptions: ProjectionOptions, - bind: any, - previousValue?: Function -) { - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - const eventMap = projectorState.nodeMap.get(domNode) || new WeakMap(); +interface DeleteDomApplication { + type: 'delete'; + current: VNodeWrapper; +} - if (previousValue) { - const previousEvent = eventMap.get(previousValue); - domNode.removeEventListener(eventName, previousEvent); - } +interface UpdateDomApplication { + type: 'update'; + current: VNodeWrapper; + next: VNodeWrapper; +} - let callback = currentValue.bind(bind); +type ApplicationInstruction = CreateDomApplication | UpdateDomApplication | DeleteDomApplication | AttachApplication; - if (eventName === 'input') { - callback = function(this: any, evt: Event) { - currentValue.call(this, evt); - (evt.target as any)['oninput-value'] = (evt.target as HTMLInputElement).value; - }.bind(bind); - } +export const widgetInstanceMap = new WeakMap< + WidgetBase>, + WidgetData +>(); - domNode.addEventListener(eventName, callback); - eventMap.set(currentValue, callback); - projectorState.nodeMap.set(domNode, eventMap); +const EMPTY_ARRAY: DNodeWrapper[] = []; +const nodeOperations = ['focus', 'blur', 'scrollIntoView', 'click']; +const NAMESPACE_W3 = 'http://www.w3.org/'; +const NAMESPACE_SVG = NAMESPACE_W3 + '2000/svg'; +const NAMESPACE_XLINK = NAMESPACE_W3 + '1999/xlink'; + +function isWNodeWrapper(child: DNodeWrapper): child is WNodeWrapper { + return child && isWNode(child.node); } -function addClasses(domNode: Element, classes: SupportedClassName) { - if (classes) { - const classNames = classes.split(' '); - for (let i = 0; i < classNames.length; i++) { - domNode.classList.add(classNames[i]); - } - } +function isVNodeWrapper(child?: DNodeWrapper | null): child is VNodeWrapper { + return !!child && isVNode(child.node); } -function removeClasses(domNode: Element, classes: SupportedClassName) { - if (classes) { - const classNames = classes.split(' '); - for (let i = 0; i < classNames.length; i++) { - domNode.classList.remove(classNames[i]); +function isAttachApplication(value: any): value is AttachApplication { + return !!value.type; +} + +function updateAttributes( + domNode: Element, + previousAttributes: { [index: string]: string | undefined }, + attributes: { [index: string]: string | undefined }, + namespace?: string +) { + const attrNames = Object.keys(attributes); + const attrCount = attrNames.length; + for (let i = 0; i < attrCount; i++) { + const attrName = attrNames[i]; + const attrValue = attributes[attrName]; + const previousAttrValue = previousAttributes[attrName]; + if (attrValue !== previousAttrValue) { + updateAttribute(domNode, attrName, attrValue, namespace); } } } -function buildPreviousProperties(domNode: any, previous: InternalVNode, current: InternalVNode) { - const { diffType, properties, attributes } = current; +function buildPreviousProperties(domNode: any, current: VNodeWrapper, next: VNodeWrapper) { + const { + node: { diffType, properties, attributes } + } = current; if (!diffType || diffType === 'vdom') { - return { properties: previous.properties, attributes: previous.attributes, events: previous.events }; + return { + properties: current.node.properties, + attributes: current.node.attributes, + events: current.node.events + }; } else if (diffType === 'none') { - return { properties: {}, attributes: previous.attributes ? {} : undefined, events: previous.events }; + return { properties: {}, attributes: current.node.attributes ? {} : undefined, events: current.node.events }; } let newProperties: any = { properties: {} }; if (attributes) { newProperties.attributes = {}; - newProperties.events = previous.events; + newProperties.events = current.node.events; Object.keys(properties).forEach((propName) => { newProperties.properties[propName] = domNode[propName]; }); @@ -248,52 +226,55 @@ function buildPreviousProperties(domNode: any, previous: InternalVNode, current: return newProperties; } -function nodeOperation( - propName: string, - propValue: any, - previousValue: any, - domNode: Element, - projectionOptions: ProjectionOptions -): void { - let result; - if (typeof propValue === 'function') { - result = propValue(); - } else { - result = propValue && !previousValue; +function same(dnode1: DNodeWrapper, dnode2: DNodeWrapper): boolean { + if (isVNodeWrapper(dnode1) && isVNodeWrapper(dnode2)) { + if (isDomVNode(dnode1.node) && isDomVNode(dnode2.node)) { + if (dnode1.node.domNode !== dnode2.node.domNode) { + return false; + } + } + if (dnode1.node.tag !== dnode2.node.tag) { + return false; + } + if (dnode1.node.properties.key !== dnode2.node.properties.key) { + return false; + } + return true; + } else if (isWNodeWrapper(dnode1) && isWNodeWrapper(dnode2)) { + if (dnode1.instance === undefined && typeof dnode2.node.widgetConstructor === 'string') { + return false; + } + if (dnode1.node.widgetConstructor !== dnode2.node.widgetConstructor) { + return false; + } + if (dnode1.node.properties.key !== dnode2.node.properties.key) { + return false; + } + return true; } - if (result === true) { - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - projectorState.deferredRenderCallbacks.push(() => { - (domNode as any)[propName](); - }); + return false; +} + +function findIndexOfChild(children: DNodeWrapper[], sameAs: DNodeWrapper, start: number) { + for (let i = start; i < children.length; i++) { + if (same(children[i], sameAs)) { + return i; + } } + return -1; } -function removeOrphanedEvents( - domNode: Element, - previousProperties: VNodeProperties, - properties: VNodeProperties, - projectionOptions: ProjectionOptions, - onlyEvents: boolean = false -) { - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - const eventMap = projectorState.nodeMap.get(domNode); - if (eventMap) { - Object.keys(previousProperties).forEach((propName) => { - const isEvent = propName.substr(0, 2) === 'on' || onlyEvents; - const eventName = onlyEvents ? propName : propName.substr(2); - if (isEvent && !properties[propName]) { - const eventCallback = eventMap.get(previousProperties[propName]); - if (eventCallback) { - domNode.removeEventListener(eventName, eventCallback); - } - } - }); +function applyClasses(domNode: any, classes: SupportedClassName, op: string) { + if (classes) { + const classNames = classes.split(' '); + for (let i = 0; i < classNames.length; i++) { + domNode.classList[op](classNames[i]); + } } } -function updateAttribute(domNode: Element, attrName: string, attrValue: string, projectionOptions: ProjectionOptions) { - if (projectionOptions.namespace === NAMESPACE_SVG && attrName === 'href') { +function updateAttribute(domNode: Element, attrName: string, attrValue: string | undefined, namespace?: string) { + if (namespace === NAMESPACE_SVG && attrName === 'href' && attrValue) { domNode.setAttributeNS(NAMESPACE_XLINK, attrName, attrValue); } else if ((attrName === 'role' && attrValue === '') || attrValue === undefined) { domNode.removeAttribute(attrName); @@ -302,887 +283,876 @@ function updateAttribute(domNode: Element, attrName: string, attrValue: string, } } -function updateAttributes( - domNode: Element, - previousAttributes: { [index: string]: string }, - attributes: { [index: string]: string }, - projectionOptions: ProjectionOptions -) { - const attrNames = Object.keys(attributes); - const attrCount = attrNames.length; - for (let i = 0; i < attrCount; i++) { - const attrName = attrNames[i]; - const attrValue = attributes[attrName]; - const previousAttrValue = previousAttributes[attrName]; - if (attrValue !== previousAttrValue) { - updateAttribute(domNode, attrName, attrValue, projectionOptions); +function runEnterAnimation(next: VNodeWrapper, transitions: TransitionStrategy) { + const { + domNode, + node: { properties }, + node: { + properties: { enterAnimation } } + } = next; + if (enterAnimation) { + if (typeof enterAnimation === 'function') { + return enterAnimation(domNode as Element, properties); + } + transitions.enter(domNode as Element, properties, enterAnimation); } } -function updateProperties( - domNode: Element, - previousProperties: VNodeProperties, - properties: VNodeProperties, - projectionOptions: ProjectionOptions, - includesEventsAndAttributes = true -) { - let propertiesUpdated = false; - const propNames = Object.keys(properties); - const propCount = propNames.length; - if (propNames.indexOf('classes') === -1 && previousProperties.classes) { - if (Array.isArray(previousProperties.classes)) { - for (let i = 0; i < previousProperties.classes.length; i++) { - removeClasses(domNode, previousProperties.classes[i]); - } - } else { - removeClasses(domNode, previousProperties.classes); +function runExitAnimation(current: VNodeWrapper, transitions: TransitionStrategy) { + const { + domNode, + node: { properties }, + node: { + properties: { exitAnimation } } + } = current; + const removeDomNode = () => { + domNode && domNode.parentNode && domNode.parentNode.removeChild(domNode); + current.domNode = undefined; + }; + if (typeof exitAnimation === 'function') { + return exitAnimation(domNode as Element, removeDomNode, properties); } + transitions.exit(domNode as Element, properties, exitAnimation as string, removeDomNode); +} - includesEventsAndAttributes && removeOrphanedEvents(domNode, previousProperties, properties, projectionOptions); - - for (let i = 0; i < propCount; i++) { - const propName = propNames[i]; - let propValue = properties[propName]; - const previousValue = previousProperties![propName]; - if (propName === 'classes') { - const previousClasses = Array.isArray(previousValue) ? previousValue : [previousValue]; - const currentClasses = Array.isArray(propValue) ? propValue : [propValue]; - if (previousClasses && previousClasses.length > 0) { - if (!propValue || propValue.length === 0) { - for (let i = 0; i < previousClasses.length; i++) { - removeClasses(domNode, previousClasses[i]); - } - } else { - const newClasses: (null | undefined | string)[] = [...currentClasses]; - for (let i = 0; i < previousClasses.length; i++) { - const previousClassName = previousClasses[i]; - if (previousClassName) { - const classIndex = newClasses.indexOf(previousClassName); - if (classIndex === -1) { - removeClasses(domNode, previousClassName); - } else { - newClasses.splice(classIndex, 1); - } - } - } - for (let i = 0; i < newClasses.length; i++) { - addClasses(domNode, newClasses[i]); - } - } - } else { - for (let i = 0; i < currentClasses.length; i++) { - addClasses(domNode, currentClasses[i]); - } - } - } else if (nodeOperations.indexOf(propName) !== -1) { - nodeOperation(propName, propValue, previousValue, domNode, projectionOptions); - } else if (propName === 'styles') { - const styleNames = Object.keys(propValue); - const styleCount = styleNames.length; - for (let j = 0; j < styleCount; j++) { - const styleName = styleNames[j]; - const newStyleValue = propValue[styleName]; - const oldStyleValue = previousValue && previousValue[styleName]; - if (newStyleValue === oldStyleValue) { - continue; - } - propertiesUpdated = true; - if (newStyleValue) { - checkStyleValue(newStyleValue); - projectionOptions.styleApplyer!(domNode as HTMLElement, styleName, newStyleValue); - } else { - projectionOptions.styleApplyer!(domNode as HTMLElement, styleName, ''); - } - } - } else { - if (!propValue && typeof previousValue === 'string') { - propValue = ''; - } - if (propName === 'value') { - const domValue = (domNode as any)[propName]; - if ( - domValue !== propValue && - ((domNode as any)['oninput-value'] - ? domValue === (domNode as any)['oninput-value'] - : propValue !== previousValue) - ) { - (domNode as any)[propName] = propValue; - (domNode as any)['oninput-value'] = undefined; - } - if (propValue !== previousValue) { - propertiesUpdated = true; - } - } else if (propName !== 'key' && propValue !== previousValue) { - const type = typeof propValue; - if (type === 'function' && propName.lastIndexOf('on', 0) === 0 && includesEventsAndAttributes) { - updateEvent( - domNode, - propName.substr(2), - propValue, - projectionOptions, - properties.bind, - previousValue - ); - } else if (type === 'string' && propName !== 'innerHTML' && includesEventsAndAttributes) { - updateAttribute(domNode, propName, propValue, projectionOptions); - } else if (propName === 'scrollLeft' || propName === 'scrollTop') { - if ((domNode as any)[propName] !== propValue) { - (domNode as any)[propName] = propValue; - } - } else { - (domNode as any)[propName] = propValue; - } - propertiesUpdated = true; - } - } - } - return propertiesUpdated; +function arrayFrom(arr: any) { + return Array.prototype.slice.call(arr); } -function findIndexOfChild(children: InternalDNode[], sameAs: InternalDNode, start: number) { - for (let i = start; i < children.length; i++) { - if (same(children[i], sameAs)) { - return i; +export function renderer(renderer: () => WNode): Renderer { + let _mountOptions: MountOptions = { + sync: false, + merge: true, + transition: transitionStrategy, + domNode: global.document.body, + registry: null + }; + let _invalidationQueue: InvalidationQueueItem[] = []; + let _processQueue: (ProcessItem | AttachApplication)[] = []; + let _applicationQueue: ApplicationInstruction[] = []; + let _eventMap = new WeakMap(); + let _instanceToWrapperMap = new WeakMap(); + let _parentWrapperMap = new WeakMap(); + let _wrapperSiblingMap = new WeakMap(); + let _renderScheduled: number | undefined; + let _afterRenderCallbacks: Function[] = []; + let _deferredRenderCallbacks: Function[] = []; + let parentInvalidate: () => void; + + function nodeOperation( + propName: string, + propValue: (() => boolean) | boolean, + previousValue: boolean, + domNode: HTMLElement & { [index: string]: any } + ): void { + let result = propValue && !previousValue; + if (typeof propValue === 'function') { + result = propValue(); + } + if (result === true) { + _afterRenderCallbacks.push(() => { + domNode[propName](); + }); } } - return -1; -} -export function toParentVNode(domNode: Element): InternalVNode { - return { - tag: '', - properties: {}, - children: undefined, - domNode, - type: VNODE - }; -} + function updateEvent( + domNode: Node, + eventName: string, + currentValue: Function, + bind: any, + previousValue?: Function + ) { + if (previousValue) { + const previousEvent = _eventMap.get(previousValue); + domNode.removeEventListener(eventName, previousEvent); + } -export function toTextVNode(data: any): InternalVNode { - return { - tag: '', - properties: {}, - children: undefined, - text: `${data}`, - domNode: undefined, - type: VNODE - }; -} + let callback = currentValue.bind(bind); -function toInternalWNode(instance: DefaultWidgetBaseInterface, instanceData: WidgetData): InternalWNode { - return { - instance, - rendered: [], - coreProperties: instanceData.coreProperties, - children: instance.children as any, - widgetConstructor: instance.constructor as any, - properties: instanceData.inputProperties, - type: WNODE - }; -} + if (eventName === 'input') { + callback = function(this: any, evt: Event) { + currentValue.call(this, evt); + (evt.target as any)['oninput-value'] = (evt.target as HTMLInputElement).value; + }.bind(bind); + } -export function filterAndDecorateChildren( - children: undefined | DNode | DNode[], - instance: DefaultWidgetBaseInterface -): InternalDNode[] { - if (children === undefined) { - return emptyArray; + domNode.addEventListener(eventName, callback); + _eventMap.set(currentValue, callback); } - children = Array.isArray(children) ? children : [children]; - - for (let i = 0; i < children.length; ) { - const child = children[i] as InternalDNode; - if (child === undefined || child === null) { - children.splice(i, 1); - continue; - } else if (typeof child === 'string') { - children[i] = toTextVNode(child); - } else { - if (isVNode(child)) { - if (child.properties.bind === undefined) { - (child.properties as any).bind = instance; - if (child.children && child.children.length > 0) { - filterAndDecorateChildren(child.children, instance); - } - } - } else { - if (!child.coreProperties) { - const instanceData = widgetInstanceMap.get(instance)!; - child.coreProperties = { - bind: instance, - baseRegistry: instanceData.coreProperties.baseRegistry - }; - } - if (child.children && child.children.length > 0) { - filterAndDecorateChildren(child.children, instance); + + function removeOrphanedEvents( + domNode: Element, + previousProperties: VNodeProperties, + properties: VNodeProperties, + onlyEvents: boolean = false + ) { + Object.keys(previousProperties).forEach((propName) => { + const isEvent = propName.substr(0, 2) === 'on' || onlyEvents; + const eventName = onlyEvents ? propName : propName.substr(2); + if (isEvent && !properties[propName]) { + const eventCallback = _eventMap.get(previousProperties[propName]); + if (eventCallback) { + domNode.removeEventListener(eventName, eventCallback); } } - } - i++; + }); } - return children as InternalDNode[]; -} -function nodeAdded(dnode: InternalDNode, transitions: TransitionStrategy) { - if (isVNode(dnode) && dnode.properties) { - const enterAnimation = dnode.properties.enterAnimation; - if (enterAnimation) { - if (typeof enterAnimation === 'function') { - enterAnimation(dnode.domNode as Element, dnode.properties); - } else { - transitions.enter(dnode.domNode as Element, dnode.properties, enterAnimation as string); + function renderedToWrapper( + rendered: DNode[], + parent: DNodeWrapper, + currentParent: DNodeWrapper | null + ): DNodeWrapper[] { + const wrappedRendered: DNodeWrapper[] = []; + const hasParentWNode = isWNodeWrapper(parent); + const currentParentLength = isVNodeWrapper(currentParent) && (currentParent.childrenWrappers || []).length > 1; + const requiresInsertBefore = (parent.hasPreviousSiblings !== false && hasParentWNode) || currentParentLength; + let previousItem: DNodeWrapper | undefined; + for (let i = 0; i < rendered.length; i++) { + const renderedItem = rendered[i]; + const wrapper = { + node: renderedItem, + depth: parent.depth + 1, + requiresInsertBefore, + hasParentWNode, + namespace: parent.namespace + } as DNodeWrapper; + if (isVNode(renderedItem) && renderedItem.properties.exitAnimation) { + parent.hasAnimations = true; + let nextParent = _parentWrapperMap.get(parent); + while (nextParent) { + if (nextParent.hasAnimations) { + break; + } + nextParent.hasAnimations = true; + nextParent = _parentWrapperMap.get(nextParent); + } } + _parentWrapperMap.set(wrapper, parent); + if (previousItem) { + _wrapperSiblingMap.set(previousItem, wrapper); + } + wrappedRendered.push(wrapper); + previousItem = wrapper; } + return wrappedRendered; } -} -function nodeToRemove(dnode: InternalDNode, transitions: TransitionStrategy, projectionOptions: ProjectionOptions) { - if (isWNode(dnode)) { - const item = instanceMap.get(dnode.instance); - const rendered = (item ? item.dnode.rendered : dnode.rendered) || emptyArray; - if (dnode.instance) { - const instanceData = widgetInstanceMap.get(dnode.instance)!; - instanceData.onDetach(); - instanceMap.delete(dnode.instance); - } - for (let i = 0; i < rendered.length; i++) { - nodeToRemove(rendered[i], transitions, projectionOptions); - } - } else { - const domNode = dnode.domNode; - const properties = dnode.properties; - if (dnode.children && dnode.children.length > 0) { - for (let i = 0; i < dnode.children.length; i++) { - nodeToRemove(dnode.children[i], transitions, projectionOptions); + function findParentWNodeWrapper(currentNode: DNodeWrapper): WNodeWrapper | undefined { + let parentWNodeWrapper: WNodeWrapper | undefined; + let parentWrapper = _parentWrapperMap.get(currentNode); + + while (!parentWNodeWrapper && parentWrapper) { + if (!parentWNodeWrapper && isWNodeWrapper(parentWrapper)) { + parentWNodeWrapper = parentWrapper; } + parentWrapper = _parentWrapperMap.get(parentWrapper); } - const exitAnimation = properties.exitAnimation; - if (properties && exitAnimation) { - (domNode as HTMLElement).style.pointerEvents = 'none'; - const removeDomNode = function() { - domNode && domNode.parentNode && domNode.parentNode.removeChild(domNode); - dnode.domNode = undefined; - }; - if (typeof exitAnimation === 'function') { - exitAnimation(domNode as Element, removeDomNode, properties); - return; - } else { - transitions.exit(dnode.domNode as Element, properties, exitAnimation as string, removeDomNode); - return; + return parentWNodeWrapper; + } + + function findParentDomNode(currentNode: DNodeWrapper): Node | undefined { + let parentDomNode: Node | undefined; + let parentWrapper = _parentWrapperMap.get(currentNode); + + while (!parentDomNode && parentWrapper) { + if (!parentDomNode && isVNodeWrapper(parentWrapper) && parentWrapper.domNode) { + parentDomNode = parentWrapper.domNode; } + parentWrapper = _parentWrapperMap.get(parentWrapper); } - domNode && domNode.parentNode && domNode.parentNode.removeChild(domNode); - dnode.domNode = undefined; + return parentDomNode; } -} -function checkDistinguishable( - childNodes: InternalDNode[], - indexToCheck: number, - parentInstance: DefaultWidgetBaseInterface -) { - const childNode = childNodes[indexToCheck]; - if (isVNode(childNode) && !childNode.tag) { - return; // Text nodes need not be distinguishable + function runDeferredProperties(next: VNodeWrapper) { + if (next.node.deferredPropertiesCallback) { + const properties = next.node.properties; + next.node.properties = { ...next.node.deferredPropertiesCallback(true), ...next.node.originalProperties }; + _afterRenderCallbacks.push(() => { + processProperties(next, properties); + }); + } } - const { key } = childNode.properties; - - if (key === undefined || key === null) { - for (let i = 0; i < childNodes.length; i++) { - if (i !== indexToCheck) { - const node = childNodes[i]; - if (same(node, childNode)) { - let nodeIdentifier: string; - const parentName = (parentInstance as any).constructor.name || 'unknown'; - if (isWNode(childNode)) { - nodeIdentifier = (childNode.widgetConstructor as any).name || 'unknown'; - } else { - nodeIdentifier = childNode.tag; - } - console.warn( - `A widget (${parentName}) has had a child addded or removed, but they were not able to uniquely identified. It is recommended to provide a unique 'key' property when using the same widget or element (${nodeIdentifier}) multiple times as siblings` - ); + function findInsertBefore(next: DNodeWrapper) { + let insertBefore: Node | null = null; + let searchNode: DNodeWrapper | undefined = next; + while (!insertBefore) { + const nextSibling = _wrapperSiblingMap.get(searchNode); + if (nextSibling) { + if (isVNodeWrapper(nextSibling)) { + if (nextSibling.domNode && nextSibling.domNode.parentNode) { + insertBefore = nextSibling.domNode; + break; + } + searchNode = nextSibling; + continue; + } + if (nextSibling.domNode && nextSibling.domNode.parentNode) { + insertBefore = nextSibling.domNode; break; } + searchNode = nextSibling; + continue; + } + searchNode = _parentWrapperMap.get(searchNode); + if (!searchNode || isVNodeWrapper(searchNode)) { + break; } } + return insertBefore; } -} -function updateChildren( - parentVNode: InternalVNode, - siblings: InternalDNode[], - oldChildren: InternalDNode[], - newChildren: InternalDNode[], - parentInstance: DefaultWidgetBaseInterface, - projectionOptions: ProjectionOptions -) { - oldChildren = oldChildren || emptyArray; - newChildren = newChildren; - const oldChildrenLength = oldChildren.length; - const newChildrenLength = newChildren.length; - const transitions = projectionOptions.transitions!; - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - projectionOptions = { ...projectionOptions, depth: projectionOptions.depth + 1 }; - let oldIndex = 0; - let newIndex = 0; - let i: number; - let textUpdated = false; - while (newIndex < newChildrenLength) { - let oldChild = oldIndex < oldChildrenLength ? oldChildren[oldIndex] : undefined; - const newChild = newChildren[newIndex]; - if (isVNode(newChild) && typeof newChild.deferredPropertiesCallback === 'function') { - newChild.inserted = isVNode(oldChild) && oldChild.inserted; - addDeferredProperties(newChild, projectionOptions); - } - if (oldChild !== undefined && same(oldChild, newChild)) { - oldIndex++; - newIndex++; - textUpdated = - updateDom( - oldChild, - newChild, - projectionOptions, - parentVNode, - parentInstance, - oldChildren.slice(oldIndex), - newChildren.slice(newIndex) - ) || textUpdated; - continue; + function setProperties( + domNode: HTMLElement, + currentProperties: VNodeProperties = {}, + nextWrapper: VNodeWrapper, + includesEventsAndAttributes = true + ): void { + const propNames = Object.keys(nextWrapper.node.properties); + const propCount = propNames.length; + if (propNames.indexOf('classes') === -1 && currentProperties.classes) { + const classes = Array.isArray(currentProperties.classes) + ? currentProperties.classes + : [currentProperties.classes]; + for (let i = 0; i < classes.length; i++) { + applyClasses(domNode, classes[i], 'remove'); + } } - const findOldIndex = findIndexOfChild(oldChildren, newChild, oldIndex + 1); - const addChild = () => { - let insertBeforeDomNode: Node | undefined = undefined; - let childrenArray = oldChildren; - let nextIndex = oldIndex + 1; - let child: InternalDNode = oldChildren[oldIndex]; - if (!child) { - child = siblings[0]; - nextIndex = 1; - childrenArray = siblings; - } - if (child) { - let insertBeforeChildren = [child]; - while (insertBeforeChildren.length) { - const insertBefore = insertBeforeChildren.shift()!; - if (isWNode(insertBefore)) { - const item = instanceMap.get(insertBefore.instance); - if (item && item.dnode.rendered) { - insertBeforeChildren.push(...item.dnode.rendered); + includesEventsAndAttributes && removeOrphanedEvents(domNode, currentProperties, nextWrapper.node.properties); + + for (let i = 0; i < propCount; i++) { + const propName = propNames[i]; + let propValue = nextWrapper.node.properties[propName]; + const previousValue = currentProperties[propName]; + if (propName === 'classes') { + const previousClasses = Array.isArray(previousValue) + ? previousValue + : previousValue + ? [previousValue] + : []; + const currentClasses = Array.isArray(propValue) ? propValue : [propValue]; + const prevClassesLength = previousClasses.length; + if (previousClasses && prevClassesLength > 0) { + if (!propValue || propValue.length === 0) { + for (let i = 0; i < prevClassesLength; i++) { + applyClasses(domNode, previousClasses[i], 'remove'); } } else { - if (insertBefore.domNode) { - if (insertBefore.domNode.parentElement !== parentVNode.domNode) { - break; + const newClasses: (null | undefined | string)[] = [...currentClasses]; + for (let i = 0; i < prevClassesLength; i++) { + const previousClassName = previousClasses[i]; + if (previousClassName) { + const classIndex = newClasses.indexOf(previousClassName); + if (classIndex === -1) { + applyClasses(domNode, previousClassName, 'remove'); + continue; + } + newClasses.splice(classIndex, 1); } - insertBeforeDomNode = insertBefore.domNode; - break; } + for (let i = 0; i < newClasses.length; i++) { + applyClasses(domNode, newClasses[i], 'add'); + } + } + } else { + if (nextWrapper.merged) { + for (let i = 0; i < currentClasses.length; i++) { + applyClasses(domNode, currentClasses[i], 'add'); + } + } else { + domNode.className = currentClasses.join(' ').trim(); + } + } + } else if (nodeOperations.indexOf(propName) !== -1) { + nodeOperation(propName, propValue, previousValue, domNode); + } else if (propName === 'styles') { + const styleNames = Object.keys(propValue); + const styleCount = styleNames.length; + for (let j = 0; j < styleCount; j++) { + const styleName = styleNames[j]; + const newStyleValue = propValue[styleName]; + const oldStyleValue = previousValue && previousValue[styleName]; + if (newStyleValue === oldStyleValue) { + continue; + } + (domNode.style as any)[styleName] = newStyleValue || ''; + } + } else { + if (!propValue && typeof previousValue === 'string') { + propValue = ''; + } + if (propName === 'value') { + const domValue = (domNode as any)[propName]; + if ( + domValue !== propValue && + ((domNode as any)['oninput-value'] + ? domValue === (domNode as any)['oninput-value'] + : propValue !== previousValue) + ) { + (domNode as any)[propName] = propValue; + (domNode as any)['oninput-value'] = undefined; } - if (insertBeforeChildren.length === 0 && childrenArray[nextIndex]) { - insertBeforeChildren.push(childrenArray[nextIndex]); - nextIndex++; + } else if (propName !== 'key' && propValue !== previousValue) { + const type = typeof propValue; + if (type === 'function' && propName.lastIndexOf('on', 0) === 0 && includesEventsAndAttributes) { + updateEvent(domNode, propName.substr(2), propValue, nextWrapper.node.bind, previousValue); + } else if (type === 'string' && propName !== 'innerHTML' && includesEventsAndAttributes) { + updateAttribute(domNode, propName, propValue, nextWrapper.namespace); + } else if (propName === 'scrollLeft' || propName === 'scrollTop') { + if ((domNode as any)[propName] !== propValue) { + (domNode as any)[propName] = propValue; + } + } else { + (domNode as any)[propName] = propValue; } } } + } + } - createDom( - newChild, - parentVNode, - newChildren.slice(newIndex + 1), - insertBeforeDomNode, - projectionOptions, - parentInstance - ); - nodeAdded(newChild, transitions); - const indexToCheck = newIndex; - projectorState.afterRenderCallbacks.push(() => { - checkDistinguishable(newChildren, indexToCheck, parentInstance); - }); - }; - - if (!oldChild || findOldIndex === -1) { - addChild(); - newIndex++; - continue; + function runDeferredRenderCallbacks() { + const { sync } = _mountOptions; + const callbacks = _deferredRenderCallbacks; + _deferredRenderCallbacks = []; + if (callbacks.length) { + const run = () => { + let callback: Function | undefined; + while ((callback = callbacks.shift())) { + callback(); + } + }; + if (sync) { + run(); + } else { + global.requestAnimationFrame(run); + } } + } - const removeChild = () => { - const indexToCheck = oldIndex; - projectorState.afterRenderCallbacks.push(() => { - checkDistinguishable(oldChildren, indexToCheck, parentInstance); - }); - if (isWNode(oldChild)) { - const item = instanceMap.get(oldChild.instance); - if (item) { - oldChild = item.dnode; + function runAfterRenderCallbacks() { + const { sync } = _mountOptions; + const callbacks = _afterRenderCallbacks; + _afterRenderCallbacks = []; + if (callbacks.length) { + const run = () => { + let callback: Function | undefined; + while ((callback = callbacks.shift())) { + callback(); + } + }; + if (sync) { + run(); + } else { + if (global.requestIdleCallback) { + global.requestIdleCallback(run); + } else { + setTimeout(run); } } - nodeToRemove(oldChild!, transitions, projectionOptions); - }; - const findNewIndex = findIndexOfChild(newChildren, oldChild, newIndex + 1); + } + } - if (findNewIndex === -1) { - removeChild(); - oldIndex++; - continue; + function processProperties(next: VNodeWrapper, previousProperties: any) { + if (next.node.attributes && next.node.events) { + updateAttributes( + next.domNode as HTMLElement, + previousProperties.attributes || {}, + next.node.attributes, + next.namespace + ); + setProperties(next.domNode as HTMLElement, previousProperties.properties, next, false); + const events = next.node.events || {}; + if (previousProperties.events) { + removeOrphanedEvents( + next.domNode as HTMLElement, + previousProperties.events || {}, + next.node.events, + true + ); + } + previousProperties.events = previousProperties.events || {}; + Object.keys(events).forEach((event) => { + updateEvent( + next.domNode as HTMLElement, + event, + events[event], + next.node.bind, + previousProperties.events[event] + ); + }); + } else { + setProperties(next.domNode as HTMLElement, previousProperties.properties, next); } + } - addChild(); - removeChild(); - oldIndex++; - newIndex++; + function mount(mountOptions: Partial = {}) { + _mountOptions = { ..._mountOptions, ...mountOptions }; + const { domNode } = _mountOptions; + const renderResult = renderer(); + const nextWrapper = { + node: renderResult, + depth: 1 + }; + _parentWrapperMap.set(nextWrapper, { depth: 0, domNode, node: v('fake') }); + _processQueue.push({ + current: [], + next: [nextWrapper], + meta: { mergeNodes: arrayFrom(domNode.childNodes) } + }); + _runProcessQueue(); + _mountOptions.merge = false; + _runDomInstructionQueue(); + _runCallbacks(); } - if (oldChildrenLength > oldIndex) { - // Remove child fragments - for (i = oldIndex; i < oldChildrenLength; i++) { - const indexToCheck = i; - projectorState.afterRenderCallbacks.push(() => { - checkDistinguishable(oldChildren, indexToCheck, parentInstance); + + function invalidate() { + parentInvalidate && parentInvalidate(); + } + + function _schedule(): void { + const { sync } = _mountOptions; + if (sync) { + _runInvalidationQueue(); + } else if (!_renderScheduled) { + _renderScheduled = global.requestAnimationFrame(() => { + _runInvalidationQueue(); }); - let childToRemove = oldChildren[i]; - if (isWNode(childToRemove)) { - const item = instanceMap.get(childToRemove.instance); + } + } + + function _runInvalidationQueue() { + _renderScheduled = undefined; + const invalidationQueue = [..._invalidationQueue]; + const previouslyRendered = []; + _invalidationQueue = []; + invalidationQueue.sort((a, b) => b.depth - a.depth); + let item: InvalidationQueueItem | undefined; + while ((item = invalidationQueue.pop())) { + let { instance } = item; + if (previouslyRendered.indexOf(instance) === -1 && _instanceToWrapperMap.has(instance!)) { + previouslyRendered.push(instance); + const current = _instanceToWrapperMap.get(instance)!; + const instanceData = widgetInstanceMap.get(instance)!; + const parent = _parentWrapperMap.get(current); + const sibling = _wrapperSiblingMap.get(current); + const { constructor, children } = instance; + const next = { + node: { + type: WNODE, + widgetConstructor: constructor as WidgetBaseConstructor, + properties: instanceData.inputProperties, + children: children, + bind: current.node.bind + }, + instance, + depth: current.depth + }; + + parent && _parentWrapperMap.set(next, parent); + sibling && _wrapperSiblingMap.set(next, sibling); + const { item } = _updateWidget({ current, next }); if (item) { - childToRemove = item.dnode; + _processQueue.push(item); + instance && _instanceToWrapperMap.set(instance, next); + _runProcessQueue(); } } - nodeToRemove(childToRemove, transitions, projectionOptions); } + _runDomInstructionQueue(); + _runCallbacks(); } - return textUpdated; -} -function addChildren( - parentVNode: InternalVNode, - children: InternalDNode[] | undefined, - projectionOptions: ProjectionOptions, - parentInstance: DefaultWidgetBaseInterface, - insertBefore: Node | undefined = undefined, - childNodes?: (Element | Text)[] -) { - if (children === undefined) { - return; + function _runProcessQueue() { + let item: AttachApplication | ProcessItem | undefined; + while ((item = _processQueue.pop())) { + if (isAttachApplication(item)) { + _applicationQueue.push(item); + } else { + const { current, next, meta } = item; + _process(current || EMPTY_ARRAY, next || EMPTY_ARRAY, meta); + } + } } - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - if (projectorState.merge && childNodes === undefined) { - childNodes = arrayFrom(parentVNode.domNode!.childNodes) as (Element | Text)[]; - } - const transitions = projectionOptions.transitions!; - projectionOptions = { ...projectionOptions, depth: projectionOptions.depth + 1 }; - - for (let i = 0; i < children.length; i++) { - const child = children[i]; - const nextSiblings = children.slice(i + 1); - - if (isVNode(child)) { - if (projectorState.merge && childNodes) { - let domElement: Element | undefined = undefined; - while (child.domNode === undefined && childNodes.length > 0) { - domElement = childNodes.shift() as Element; - if (domElement && domElement.tagName === (child.tag.toUpperCase() || undefined)) { - child.domNode = domElement; + function _runDomInstructionQueue(): void { + _applicationQueue.reverse(); + let item: ApplicationInstruction | undefined; + while ((item = _applicationQueue.pop())) { + if (item.type === 'create') { + const { + parentDomNode, + next, + next: { + domNode, + merged, + requiresInsertBefore, + node: { properties } } + } = item; + + processProperties(next, {}); + runDeferredProperties(next); + if (!merged) { + let insertBefore: any; + if (requiresInsertBefore) { + insertBefore = findInsertBefore(next); + } + parentDomNode.insertBefore(domNode!, insertBefore); + } + runEnterAnimation(next, _mountOptions.transition); + const instanceData = widgetInstanceMap.get(next.node.bind as WidgetBase); + if (properties.key != null && instanceData) { + instanceData.nodeHandler.add(domNode as HTMLElement, `${properties.key}`); + } + item.next.inserted = true; + } else if (item.type === 'update') { + const { + next, + next: { domNode, node }, + current + } = item; + const parent = _parentWrapperMap.get(next); + if (parent && isWNodeWrapper(parent) && parent.instance) { + const instanceData = widgetInstanceMap.get(parent.instance); + instanceData && instanceData.nodeHandler.addRoot(); + } + + const previousProperties = buildPreviousProperties(domNode, current, next); + const instanceData = widgetInstanceMap.get(next.node.bind as WidgetBase); + + processProperties(next, previousProperties); + runDeferredProperties(next); + + if (instanceData && node.properties.key != null) { + instanceData.nodeHandler.add(next.domNode as HTMLElement, `${node.properties.key}`); + } + } else if (item.type === 'delete') { + const { current } = item; + if (current.node.properties.exitAnimation) { + runExitAnimation(current, _mountOptions.transition); + } else { + current.domNode!.parentNode!.removeChild(current.domNode!); + current.domNode = undefined; } + } else { + const { instance, attached } = item; + const instanceData = widgetInstanceMap.get(instance)!; + instanceData.nodeHandler.addRoot(); + attached && instanceData.onAttach(); } - createDom(child, parentVNode, nextSiblings, insertBefore, projectionOptions, parentInstance); - } else { - createDom(child, parentVNode, nextSiblings, insertBefore, projectionOptions, parentInstance, childNodes); } - nodeAdded(child, transitions); } -} -function initPropertiesAndChildren( - domNode: Element, - dnode: InternalVNode, - parentInstance: DefaultWidgetBaseInterface, - projectionOptions: ProjectionOptions -) { - addChildren(dnode, dnode.children, projectionOptions, parentInstance, undefined); - if (typeof dnode.deferredPropertiesCallback === 'function' && dnode.inserted === undefined) { - addDeferredProperties(dnode, projectionOptions); + function _runCallbacks() { + runAfterRenderCallbacks(); + runDeferredRenderCallbacks(); } - if (dnode.attributes && dnode.events) { - updateAttributes(domNode, {}, dnode.attributes, projectionOptions); - updateProperties(domNode, {}, dnode.properties, projectionOptions, false); - removeOrphanedEvents(domNode, {}, dnode.events, projectionOptions, true); - const events = dnode.events; - Object.keys(events).forEach((event) => { - updateEvent(domNode, event, events[event], projectionOptions, dnode.properties.bind); - }); - } else { - updateProperties(domNode, {}, dnode.properties, projectionOptions); + function _processMergeNodes(next: DNodeWrapper, mergeNodes: Node[]) { + const { merge } = _mountOptions; + if (merge && mergeNodes.length) { + if (isVNodeWrapper(next)) { + let { + node: { tag } + } = next; + for (let i = 0; i < mergeNodes.length; i++) { + const domElement = mergeNodes[i] as Element; + if (tag.toUpperCase() === (domElement.tagName || '')) { + mergeNodes.splice(i, 1); + next.domNode = domElement; + break; + } + } + } else { + next.mergeNodes = mergeNodes; + } + } } - if (dnode.properties.key !== null && dnode.properties.key !== undefined) { - const instanceData = widgetInstanceMap.get(parentInstance)!; - instanceData.nodeHandler.add(domNode as HTMLElement, `${dnode.properties.key}`); + + function _process(current: DNodeWrapper[], next: DNodeWrapper[], meta: ProcessMeta = {}): void { + let { mergeNodes = [], oldIndex = 0, newIndex = 0 } = meta; + const currentLength = current.length; + const nextLength = next.length; + const hasPreviousSiblings = currentLength > 1 || (currentLength > 0 && currentLength < nextLength); + const instructions: Instruction[] = []; + if (newIndex < nextLength) { + let currentWrapper = oldIndex < currentLength ? current[oldIndex] : undefined; + const nextWrapper = next[newIndex]; + nextWrapper.hasPreviousSiblings = hasPreviousSiblings; + + _processMergeNodes(nextWrapper, mergeNodes); + + if (currentWrapper && same(currentWrapper, nextWrapper)) { + oldIndex++; + newIndex++; + if (isVNodeWrapper(currentWrapper) && isVNodeWrapper(nextWrapper)) { + nextWrapper.inserted = currentWrapper.inserted; + } + instructions.push({ current: currentWrapper, next: nextWrapper }); + } else if (!currentWrapper || findIndexOfChild(current, nextWrapper, oldIndex + 1) === -1) { + newIndex++; + instructions.push({ current: undefined, next: nextWrapper }); + } else if (findIndexOfChild(next, currentWrapper, newIndex + 1) === -1) { + instructions.push({ current: currentWrapper, next: undefined }); + oldIndex++; + } else { + instructions.push({ current: currentWrapper, next: undefined }); + instructions.push({ current: undefined, next: nextWrapper }); + oldIndex++; + newIndex++; + } + } + + if (newIndex < nextLength) { + _processQueue.push({ current, next, meta: { mergeNodes, oldIndex, newIndex } }); + } + + if (currentLength > oldIndex && newIndex >= nextLength) { + for (let i = oldIndex; i < currentLength; i++) { + instructions.push({ current: current[i], next: undefined }); + } + } + + for (let i = 0; i < instructions.length; i++) { + const { item, dom, widget } = _processOne(instructions[i]); + widget && _processQueue.push(widget); + item && _processQueue.push(item); + dom && _applicationQueue.push(dom); + } } - dnode.inserted = true; -} -function createDom( - dnode: InternalDNode, - parentVNode: InternalVNode, - nextSiblings: InternalDNode[], - insertBefore: Node | undefined, - projectionOptions: ProjectionOptions, - parentInstance: DefaultWidgetBaseInterface, - childNodes?: (Element | Text)[] -) { - let domNode: Element | Text | undefined; - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - if (isWNode(dnode)) { - let { widgetConstructor } = dnode; - const parentInstanceData = widgetInstanceMap.get(parentInstance)!; - if (!isWidgetBaseConstructor(widgetConstructor)) { - const item = parentInstanceData.registry().get(widgetConstructor); - if (item === null) { - return; + function _processOne({ current, next }: Instruction): ProcessResult { + if (current !== next) { + if (!current && next) { + if (isVNodeWrapper(next)) { + return _createDom({ next }); + } else { + return _createWidget({ next }); + } + } else if (current && next) { + if (isVNodeWrapper(current) && isVNodeWrapper(next)) { + return _updateDom({ current, next }); + } else if (isWNodeWrapper(current) && isWNodeWrapper(next)) { + return _updateWidget({ current, next }); + } + } else if (current && !next) { + if (isVNodeWrapper(current)) { + return _removeDom({ current }); + } else if (isWNodeWrapper(current)) { + return _removeWidget({ current }); + } } - widgetConstructor = item; } - const instance = new widgetConstructor(); - dnode.instance = instance; - nextSiblingMap.set(instance, nextSiblings); + return {}; + } + + function _createWidget({ next }: CreateWidgetInstruction): ProcessResult { + let { + node: { widgetConstructor } + } = next; + let { registry } = _mountOptions; + if (!isWidgetBaseConstructor(widgetConstructor)) { + return {}; + } + const instance = new widgetConstructor() as WidgetBase; + if (registry) { + instance.registry.base = registry; + } const instanceData = widgetInstanceMap.get(instance)!; instanceData.invalidate = () => { instanceData.dirty = true; - if (instanceData.rendering === false) { - projectorState.renderQueue.push({ instance, depth: projectionOptions.depth }); - scheduleRender(projectionOptions); + if (!instanceData.rendering && _instanceToWrapperMap.has(instance)) { + _invalidationQueue.push({ instance, depth: next.depth }); + _schedule(); } }; instanceData.rendering = true; - instance.__setCoreProperties__(dnode.coreProperties); - instance.__setChildren__(dnode.children); - instance.__setProperties__(dnode.properties); - const rendered = instance.__render__(); + instance.__setProperties__(next.node.properties, next.node.bind); + instance.__setChildren__(next.node.children); + next.instance = instance; + let rendered = instance.__render__(); instanceData.rendering = false; if (rendered) { - const filteredRendered = filterAndDecorateChildren(rendered, instance); - dnode.rendered = filteredRendered; - addChildren(parentVNode, filteredRendered, projectionOptions, instance, insertBefore, childNodes); + rendered = Array.isArray(rendered) ? rendered : [rendered]; + next.childrenWrappers = renderedToWrapper(rendered, next, null); } - instanceMap.set(instance, { dnode, parentVNode }); - instanceData.nodeHandler.addRoot(); - projectorState.afterRenderCallbacks.push(() => { - instanceData.onAttach(); - }); - } else { - if (projectorState.merge && projectorState.mergeElement !== undefined) { - domNode = dnode.domNode = projectionOptions.mergeElement; - projectorState.mergeElement = undefined; - initPropertiesAndChildren(domNode!, dnode, parentInstance, projectionOptions); - return; - } - const doc = parentVNode.domNode!.ownerDocument; - if (!dnode.tag && typeof dnode.text === 'string') { - if (dnode.domNode !== undefined && parentVNode.domNode) { - const newDomNode = dnode.domNode.ownerDocument.createTextNode(dnode.text!); - if (parentVNode.domNode === dnode.domNode.parentNode) { - parentVNode.domNode.replaceChild(newDomNode, dnode.domNode); - } else { - parentVNode.domNode.appendChild(newDomNode); - dnode.domNode.parentNode && dnode.domNode.parentNode.removeChild(dnode.domNode); - } - dnode.domNode = newDomNode; - } else { - domNode = dnode.domNode = doc.createTextNode(dnode.text!); - if (insertBefore !== undefined) { - parentVNode.domNode!.insertBefore(domNode, insertBefore); - } else { - parentVNode.domNode!.appendChild(domNode); - } - } - } else { - if (dnode.domNode === undefined) { - if (dnode.tag === 'svg') { - projectionOptions = { ...projectionOptions, ...{ namespace: NAMESPACE_SVG } }; - } - if (projectionOptions.namespace !== undefined) { - domNode = dnode.domNode = doc.createElementNS(projectionOptions.namespace, dnode.tag); - } else { - domNode = dnode.domNode = dnode.domNode || doc.createElement(dnode.tag); - } - } else { - domNode = dnode.domNode; - } - initPropertiesAndChildren(domNode! as Element, dnode, parentInstance, projectionOptions); - if (insertBefore !== undefined) { - parentVNode.domNode!.insertBefore(domNode, insertBefore); - } else if (domNode!.parentNode !== parentVNode.domNode!) { - parentVNode.domNode!.appendChild(domNode); + if (next.instance) { + _instanceToWrapperMap.set(next.instance, next); + if (!parentInvalidate) { + parentInvalidate = next.instance.invalidate.bind(next.instance); } } + return { + item: { next: next.childrenWrappers, meta: { mergeNodes: next.mergeNodes } }, + widget: { type: 'attach', instance, attached: true } + }; } -} -function updateDom( - previous: any, - dnode: InternalDNode, - projectionOptions: ProjectionOptions, - parentVNode: InternalVNode, - parentInstance: DefaultWidgetBaseInterface, - oldNextSiblings: InternalDNode[], - nextSiblings: InternalDNode[] -) { - if (isWNode(dnode)) { - const { instance } = previous; - const { parentVNode, dnode: node } = instanceMap.get(instance)!; - const previousRendered = node ? node.rendered : previous.rendered; + function _updateWidget({ current, next }: UpdateWidgetInstruction): ProcessResult { + current = (current.instance && _instanceToWrapperMap.get(current.instance)) || current; + const { instance, domNode, hasAnimations } = current; + if (!instance) { + return [] as ProcessResult; + } const instanceData = widgetInstanceMap.get(instance)!; + next.instance = instance; + next.domNode = domNode; + next.hasAnimations = hasAnimations; instanceData.rendering = true; - instance.__setCoreProperties__(dnode.coreProperties); - instance.__setChildren__(dnode.children); - instance.__setProperties__(dnode.properties); - nextSiblingMap.set(instance, nextSiblings); - dnode.instance = instance; - if (instanceData.dirty === true) { - const rendered = instance.__render__(); - instanceData.rendering = false; - dnode.rendered = filterAndDecorateChildren(rendered, instance); - updateChildren(parentVNode, oldNextSiblings, previousRendered, dnode.rendered, instance, projectionOptions); - } else { + instance!.__setProperties__(next.node.properties, next.node.bind); + instance!.__setChildren__(next.node.children); + _instanceToWrapperMap.set(next.instance!, next); + if (instanceData.dirty) { + let rendered = instance!.__render__(); instanceData.rendering = false; - dnode.rendered = previousRendered; - } - instanceMap.set(instance, { dnode, parentVNode }); - instanceData.nodeHandler.addRoot(); - } else { - if (previous === dnode) { - return false; - } - const domNode = (dnode.domNode = previous.domNode); - let textUpdated = false; - let updated = false; - if (!dnode.tag && typeof dnode.text === 'string') { - if (dnode.text !== previous.text) { - const newDomNode = domNode.ownerDocument.createTextNode(dnode.text!); - domNode.parentNode!.replaceChild(newDomNode, domNode); - dnode.domNode = newDomNode; - textUpdated = true; - return textUpdated; - } - } else { - if (dnode.tag && dnode.tag.lastIndexOf('svg', 0) === 0) { - projectionOptions = { ...projectionOptions, ...{ namespace: NAMESPACE_SVG } }; - } - if (previous.children !== dnode.children) { - const children = filterAndDecorateChildren(dnode.children, parentInstance); - dnode.children = children; - updated = - updateChildren( - dnode, - oldNextSiblings, - previous.children, - children, - parentInstance, - projectionOptions - ) || updated; - } - - const previousProperties = buildPreviousProperties(domNode, previous, dnode); - if (dnode.attributes && dnode.events) { - updateAttributes(domNode, previousProperties.attributes, dnode.attributes, projectionOptions); - updated = - updateProperties( - domNode, - previousProperties.properties, - dnode.properties, - projectionOptions, - false - ) || updated; - removeOrphanedEvents(domNode, previousProperties.events, dnode.events, projectionOptions, true); - const events = dnode.events; - Object.keys(events).forEach((event) => { - updateEvent( - domNode, - event, - events[event], - projectionOptions, - dnode.properties.bind, - previousProperties.events[event] - ); - }); - } else { - updated = - updateProperties(domNode, previousProperties.properties, dnode.properties, projectionOptions) || - updated; - } - - if (dnode.properties.key !== null && dnode.properties.key !== undefined) { - const instanceData = widgetInstanceMap.get(parentInstance)!; - instanceData.nodeHandler.add(domNode, `${dnode.properties.key}`); + if (rendered) { + rendered = Array.isArray(rendered) ? rendered : [rendered]; + next.childrenWrappers = renderedToWrapper(rendered, next, current); } + return { + item: { current: current.childrenWrappers, next: next.childrenWrappers, meta: {} }, + widget: { type: 'attach', instance, attached: false } + }; } - if (updated && dnode.properties && dnode.properties.updateAnimation) { - dnode.properties.updateAnimation(domNode as Element, dnode.properties, previous.properties); - } + instanceData.rendering = false; + next.childrenWrappers = current.childrenWrappers; + return { + widget: { type: 'attach', instance, attached: false } + }; } -} -function addDeferredProperties(vnode: InternalVNode, projectionOptions: ProjectionOptions) { - // transfer any properties that have been passed - as these must be decorated properties - vnode.decoratedDeferredProperties = vnode.properties; - const properties = vnode.deferredPropertiesCallback!(!!vnode.inserted); - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - vnode.properties = { ...properties, ...vnode.decoratedDeferredProperties }; - projectorState.deferredRenderCallbacks.push(() => { - const properties = { - ...vnode.deferredPropertiesCallback!(!!vnode.inserted), - ...vnode.decoratedDeferredProperties + function _removeWidget({ current }: RemoveWidgetInstruction): ProcessResult { + current = current.instance ? _instanceToWrapperMap.get(current.instance)! : current; + _wrapperSiblingMap.delete(current); + _parentWrapperMap.delete(current); + _instanceToWrapperMap.delete(current.instance!); + if (current.instance) { + const instanceData = widgetInstanceMap.get(current.instance!); + instanceData && instanceData.onDetach(); + } + current.domNode = undefined; + current.node.bind = undefined; + current.instance = undefined; + + return { + item: { current: current.childrenWrappers, meta: {} } }; - updateProperties(vnode.domNode! as Element, vnode.properties, properties, projectionOptions); - vnode.properties = properties; - }); -} + } -function runDeferredRenderCallbacks(projectionOptions: ProjectionOptions) { - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - if (projectorState.deferredRenderCallbacks.length) { - if (projectionOptions.sync) { - while (projectorState.deferredRenderCallbacks.length) { - const callback = projectorState.deferredRenderCallbacks.shift(); - callback && callback(); + function _createDom({ next }: CreateDomInstruction): ProcessResult { + let mergeNodes: Node[] = []; + if (!next.domNode) { + if ((next.node as any).domNode) { + next.domNode = (next.node as any).domNode; + } else { + if (next.node.tag === 'svg') { + next.namespace = NAMESPACE_SVG; + } + if (next.node.tag) { + if (next.namespace) { + next.domNode = global.document.createElementNS(next.namespace, next.node.tag); + } else { + next.domNode = global.document.createElement(next.node.tag); + } + } else if (next.node.text != null) { + next.domNode = global.document.createTextNode(next.node.text); + } } } else { - global.requestAnimationFrame(() => { - while (projectorState.deferredRenderCallbacks.length) { - const callback = projectorState.deferredRenderCallbacks.shift(); - callback && callback(); - } - }); + next.merged = true; } - } -} - -function runAfterRenderCallbacks(projectionOptions: ProjectionOptions) { - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - if (projectionOptions.sync) { - while (projectorState.afterRenderCallbacks.length) { - const callback = projectorState.afterRenderCallbacks.shift(); - callback && callback(); + if (next.domNode) { + if (_mountOptions.merge) { + mergeNodes = arrayFrom(next.domNode.childNodes); + } + if (next.node.children) { + next.childrenWrappers = renderedToWrapper(next.node.children, next, null); + } } - } else { - if (global.requestIdleCallback) { - global.requestIdleCallback(() => { - while (projectorState.afterRenderCallbacks.length) { - const callback = projectorState.afterRenderCallbacks.shift(); - callback && callback(); - } - }); - } else { - setTimeout(() => { - while (projectorState.afterRenderCallbacks.length) { - const callback = projectorState.afterRenderCallbacks.shift(); - callback && callback(); - } - }); + const parentWNodeWrapper = findParentWNodeWrapper(next); + if (parentWNodeWrapper && !parentWNodeWrapper.domNode) { + parentWNodeWrapper.domNode = next.domNode; + } + const dom: ApplicationInstruction = { + next: next!, + parentDomNode: findParentDomNode(next)!, + type: 'create' + }; + if (next.childrenWrappers) { + return { + item: { current: [], next: next.childrenWrappers, meta: { mergeNodes } }, + dom + }; } + return { dom }; } -} -function scheduleRender(projectionOptions: ProjectionOptions) { - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - if (projectionOptions.sync) { - render(projectionOptions); - } else if (projectorState.renderScheduled === undefined) { - projectorState.renderScheduled = global.requestAnimationFrame(() => { - render(projectionOptions); - }); + function _updateDom({ current, next }: UpdateDomInstruction): ProcessResult { + const parentDomNode = findParentDomNode(current); + next.domNode = current.domNode; + next.namespace = current.namespace; + if (next.node.text && next.node.text !== current.node.text) { + const updatedTextNode = parentDomNode!.ownerDocument.createTextNode(next.node.text!); + parentDomNode!.replaceChild(updatedTextNode, next.domNode!); + next.domNode = updatedTextNode; + } else if (next.node.children) { + const children = renderedToWrapper(next.node.children, next, current); + next.childrenWrappers = children; + } + return { + item: { current: current.childrenWrappers, next: next.childrenWrappers, meta: {} }, + dom: { type: 'update', next, current } + }; } -} -function render(projectionOptions: ProjectionOptions) { - const projectorState = projectorStateMap.get(projectionOptions.projectorInstance)!; - projectorState.renderScheduled = undefined; - const renderQueue = projectorState.renderQueue; - const renders = [...renderQueue]; - projectorState.renderQueue = []; - renders.sort((a, b) => a.depth - b.depth); - const previouslyRendered = []; - while (renders.length) { - const { instance } = renders.shift()!; - if (instanceMap.has(instance) && previouslyRendered.indexOf(instance) === -1) { - previouslyRendered.push(instance); - const { parentVNode, dnode } = instanceMap.get(instance)!; - const instanceData = widgetInstanceMap.get(instance)!; - const nextSiblings = nextSiblingMap.get(instance)!; - updateDom( - dnode, - toInternalWNode(instance, instanceData), - projectionOptions, - parentVNode, - instance, - nextSiblings, - nextSiblings - ); + function _removeDom({ current }: RemoveDomInstruction): ProcessResult { + _wrapperSiblingMap.delete(current); + _parentWrapperMap.delete(current); + current.node.bind = undefined; + if (current.hasAnimations) { + return { + item: { current: current.childrenWrappers, meta: {} }, + dom: { type: 'delete', current } + }; } - } - runAfterRenderCallbacks(projectionOptions); - runDeferredRenderCallbacks(projectionOptions); -} -export const dom = { - append: function( - parentNode: Element, - instance: DefaultWidgetBaseInterface, - projectionOptions: Partial = {} - ): Projection { - const instanceData = widgetInstanceMap.get(instance)!; - const finalProjectorOptions = getProjectionOptions(projectionOptions, instance); - const projectorState: ProjectorState = { - afterRenderCallbacks: [], - deferredRenderCallbacks: [], - nodeMap: new WeakMap(), - renderScheduled: undefined, - renderQueue: [], - merge: projectionOptions.merge || false, - mergeElement: projectionOptions.mergeElement - }; - projectorStateMap.set(instance, projectorState); + if (current.childrenWrappers) { + _afterRenderCallbacks.push(() => { + let wrappers = current.childrenWrappers || []; + let wrapper: DNodeWrapper | undefined; + while ((wrapper = wrappers.pop())) { + if (wrapper.childrenWrappers) { + wrappers.push(...wrapper.childrenWrappers); + wrapper.childrenWrappers = undefined; + } + if (isWNodeWrapper(wrapper)) { + if (wrapper.instance) { + _instanceToWrapperMap.delete(wrapper.instance); + const instanceData = widgetInstanceMap.get(wrapper.instance); + instanceData && instanceData.onDetach(); + } + wrapper.instance = undefined; + } + _wrapperSiblingMap.delete(wrapper); + _parentWrapperMap.delete(wrapper); + wrapper.domNode = undefined; + wrapper.node.bind = undefined; + } + }); + } - finalProjectorOptions.rootNode = parentNode; - const parentVNode = toParentVNode(finalProjectorOptions.rootNode); - const node = toInternalWNode(instance, instanceData); - instanceMap.set(instance, { dnode: node, parentVNode }); - instanceData.invalidate = () => { - instanceData.dirty = true; - if (instanceData.rendering === false) { - projectorState.renderQueue.push({ instance, depth: finalProjectorOptions.depth }); - scheduleRender(finalProjectorOptions); - } - }; - updateDom(node, node, finalProjectorOptions, parentVNode, instance, [], []); - projectorState.afterRenderCallbacks.push(() => { - instanceData.onAttach(); - }); - runDeferredRenderCallbacks(finalProjectorOptions); - runAfterRenderCallbacks(finalProjectorOptions); return { - domNode: finalProjectorOptions.rootNode + dom: { type: 'delete', current } }; - }, - create: function(instance: DefaultWidgetBaseInterface, projectionOptions?: Partial): Projection { - return this.append(document.createElement('div'), instance, projectionOptions); - }, - merge: function( - element: Element, - instance: DefaultWidgetBaseInterface, - projectionOptions: Partial = {} - ): Projection { - projectionOptions.merge = true; - projectionOptions.mergeElement = element; - const projection = this.append(element.parentNode as Element, instance, projectionOptions); - const projectorState = projectorStateMap.get(instance)!; - projectorState.merge = false; - return projection; } -}; + + return { + mount, + invalidate + }; +} diff --git a/tests/routing/unit/Link.ts b/tests/routing/unit/Link.ts index ba2c584eb..47fb770d3 100644 --- a/tests/routing/unit/Link.ts +++ b/tests/routing/unit/Link.ts @@ -48,7 +48,7 @@ describe('Link', () => { it('Generate link component for basic outlet', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: 'foo', registry }); const dNode: any = link.__render__(); assert.strictEqual(dNode.tag, 'a'); @@ -57,7 +57,7 @@ describe('Link', () => { it('Generate link component for outlet with specified params', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: 'foo2', params: { foo: 'foo' }, registry }); const dNode: any = link.__render__(); assert.strictEqual(dNode.tag, 'a'); @@ -66,7 +66,7 @@ describe('Link', () => { it('Generate link component for fixed href', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: '#foo/static', isOutlet: false, registry }); const dNode: any = link.__render__(); assert.strictEqual(dNode.tag, 'a'); @@ -75,7 +75,7 @@ describe('Link', () => { it('Set router path on click', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: '#foo/static', isOutlet: false, registry }); const dNode: any = link.__render__(); assert.strictEqual(dNode.tag, 'a'); @@ -86,7 +86,7 @@ describe('Link', () => { it('Custom onClick handler can prevent default', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: 'foo', registry, @@ -103,7 +103,7 @@ describe('Link', () => { it('Does not set router path when target attribute is set', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: 'foo', registry, @@ -118,7 +118,7 @@ describe('Link', () => { it('Does not set router path on right click', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: 'foo', registry @@ -132,7 +132,7 @@ describe('Link', () => { it('throw error if the injected router cannot be found with the router key', () => { const link = new Link(); - link.__setCoreProperties__({ bind: link, baseRegistry: registry }); + link.registry.base = registry; link.__setProperties__({ to: '#foo/static', isOutlet: false, routerKey: 'fake-key' }); try { link.__render__(); diff --git a/tests/routing/unit/Outlet.ts b/tests/routing/unit/Outlet.ts index af6c33ce5..de674a1ab 100644 --- a/tests/routing/unit/Outlet.ts +++ b/tests/routing/unit/Outlet.ts @@ -76,7 +76,7 @@ describe('Outlet', () => { assert.deepEqual(renderResult.properties, {}); router.setPath('/foo/bar'); renderResult = outlet.__render__() as WNode; - assert.isNull(renderResult); + assert.isUndefined(renderResult); }); it('Should render the error component only for error matches', () => { @@ -110,7 +110,7 @@ describe('Outlet', () => { const TestOutlet = Outlet({ index: Widget }, 'baz', { mapParams }); const outlet = new TestOutlet(); outlet.__setProperties__({ router } as any); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(mapParams.calledOnce); assert.isTrue( mapParams.calledWith({ @@ -151,13 +151,13 @@ describe('Outlet', () => { const TestOutlet = Outlet({ index: Widget }, 'baz'); const outlet = new TestOutlet(); outlet.__setProperties__({ router } as any); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnEnter.calledOnce); router.setPath('/baz/bar'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnEnter.calledTwice); router.setPath('/baz/baz'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnEnter.calledThrice); }); @@ -203,16 +203,16 @@ describe('Outlet', () => { const TestOutlet = Outlet(OuterWidget, 'baz'); const outlet = new TestOutlet(); outlet.__setProperties__({ router } as any); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnEnter.calledOnce); router.setPath('/baz/bar'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnEnter.calledTwice); router.setPath('/baz/bar/qux'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnEnter.calledTwice); router.setPath('/baz/foo/qux'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnEnter.calledThrice); }); @@ -241,16 +241,16 @@ describe('Outlet', () => { const TestOutlet = Outlet({ index: Widget }, 'foo'); const outlet = new TestOutlet(); outlet.__setProperties__({ router } as any); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnExit.notCalled); router.setPath('/foo/bar'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnExit.calledOnce); router.setPath('/baz'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnExit.calledOnce); router.setPath('/foo'); - outlet.__render__() as WNode; + outlet.__render__(); assert.isTrue(configOnExit.calledOnce); }); diff --git a/tests/stores/unit/StoreInjector.ts b/tests/stores/unit/StoreInjector.ts index 8f23b5b65..d4a40704c 100644 --- a/tests/stores/unit/StoreInjector.ts +++ b/tests/stores/unit/StoreInjector.ts @@ -95,7 +95,7 @@ describe('StoreInjector', () => { class TestWidget extends WidgetBase {} const widget = new TestWidget(); registry.defineInjector('state', () => () => store); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.registry.base = registry; widget.__setProperties__({}); const invalidateSpy = spy(widget, 'invalidate'); assert.strictEqual(widget.properties.foo, undefined); @@ -121,7 +121,7 @@ describe('StoreInjector', () => { class TestWidget extends WidgetBase {} const widget = new TestWidget(); registry.defineInjector('state', () => () => store); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.registry.base = registry; widget.__setProperties__({}); const invalidateSpy = spy(widget, 'invalidate'); assert.strictEqual(widget.properties.foo, undefined); @@ -149,7 +149,7 @@ describe('StoreInjector', () => { class TestWidget extends WidgetBase {} const widget = new TestWidget(); registry.defineInjector('state', () => () => store); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.registry.base = registry; widget.__setProperties__({}); const invalidateSpy = spy(widget, 'invalidate'); assert.strictEqual(widget.properties.foo, undefined); @@ -189,15 +189,15 @@ describe('StoreInjector', () => { } const widget = new TestWidget(); registry.defineInjector('state', () => () => store); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.registry.base = registry; widget.__setProperties__({}); fooProcess(store)({}); - assert.strictEqual(invalidateCounter, 3); + assert.strictEqual(invalidateCounter, 2); barProcess(store)({}); - assert.strictEqual(invalidateCounter, 4); + assert.strictEqual(invalidateCounter, 3); widget.destroy(); barProcess(store)({}); - assert.strictEqual(invalidateCounter, 4); + assert.strictEqual(invalidateCounter, 3); }); it('path based invalidate listeners are removed when widget is destroyed', () => { @@ -222,15 +222,15 @@ describe('StoreInjector', () => { } const widget = new TestWidget(); registry.defineInjector('state', () => () => store); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.registry.base = registry; widget.__setProperties__({}); fooProcess(store)({}); - assert.strictEqual(invalidateCounter, 3); + assert.strictEqual(invalidateCounter, 2); fooProcess(store)({}); - assert.strictEqual(invalidateCounter, 4); + assert.strictEqual(invalidateCounter, 3); widget.destroy(); fooProcess(store)({}); - assert.strictEqual(invalidateCounter, 4); + assert.strictEqual(invalidateCounter, 3); }); }); diff --git a/tests/testing/unit/support/assertRender.ts b/tests/testing/unit/support/assertRender.ts index a37da93fc..072678f06 100644 --- a/tests/testing/unit/support/assertRender.ts +++ b/tests/testing/unit/support/assertRender.ts @@ -15,7 +15,13 @@ class MockWidget extends WidgetBase { class OtherWidget extends WidgetBase { render() { - return v('div', { key: 'one', classes: 'class' }, ['text node', undefined, w(MockWidget, {})]); + return v('div', { key: 'one', classes: 'class' }, ['text node', undefined, '', null, w(MockWidget, {})]); + } +} + +class FalsyChildren extends WidgetBase { + render() { + return v('div', { key: 'one', classes: 'class' }, [undefined, '', null]); } } @@ -86,11 +92,19 @@ describe('support/assertRender', () => { assert.doesNotThrow(() => { assertRender( renderResult, - v('div', { classes: 'class', key: 'one' }, ['text node', undefined, w(MockWidget, {})]) + v('div', { classes: 'class', key: 'one' }, ['text node', undefined, '', null, w(MockWidget, {})]) ); }); }); + it('Should not throw when all the children are falsy', () => { + const widget = new FalsyChildren(); + const renderResult = widget.__render__(); + assert.doesNotThrow(() => { + assertRender(renderResult, v('div', { classes: 'class', key: 'one' }, [undefined, '', null])); + }); + }); + it('Should throw when actual and expected do not match', () => { const widget = new OtherWidget(); const renderResult = widget.__render__(); diff --git a/tests/widget-core/unit/Container.ts b/tests/widget-core/unit/Container.ts index c2649acef..3db5b6418 100644 --- a/tests/widget-core/unit/Container.ts +++ b/tests/widget-core/unit/Container.ts @@ -4,7 +4,7 @@ import { v, w } from '../../../src/widget-core/d'; import { WidgetBase } from '../../../src/widget-core/WidgetBase'; import { Container } from '../../../src/widget-core/Container'; import { Registry } from '../../../src/widget-core/Registry'; -import { ProjectorMixin } from '../../../src/widget-core/mixins/Projector'; +import { renderer } from '../../../src/widget-core/vdom'; interface TestWidgetProperties { foo: string; @@ -51,8 +51,8 @@ registerSuite('mixins/Container', { }; const TestWidgetContainer = Container(TestWidget, 'test-state-1', { getProperties }); const widget = new TestWidgetContainer(); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); - widget.__setProperties__({ foo: 'bar' }); + widget.registry.base = registry; + widget.__setProperties__({ foo: 'bar' }, widget); widget.__setChildren__([]); widget.__render__(); }, @@ -70,8 +70,8 @@ registerSuite('mixins/Container', { }; const TestWidgetContainer = Container(TestWidget, 'test-state-1', { getProperties }); const widget = new TestWidgetContainer(); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); - widget.__setProperties__({ foo: 'bar' }); + widget.registry.base = registry; + widget.__setProperties__({ foo: 'bar' }, widget); widget.__setChildren__([child]); widget.__render__(); }, @@ -111,15 +111,14 @@ registerSuite('mixins/Container', { return super.render(); } } - class Parent extends ProjectorMixin(WidgetBase) { + class Parent extends WidgetBase { render() { return w(ContainerClass, {}); } } - const projector = new Parent(); - projector.setProperties({ registry }); - projector.async = false; - projector.append(); + + const r = renderer(() => w(Parent, {})); + r.mount({ sync: true, registry }); renderCount = 0; testInvalidate.invalidator(); diff --git a/tests/widget-core/unit/WidgetBase.ts b/tests/widget-core/unit/WidgetBase.ts index 8e5b72e8d..864dee552 100644 --- a/tests/widget-core/unit/WidgetBase.ts +++ b/tests/widget-core/unit/WidgetBase.ts @@ -3,16 +3,16 @@ const { assert } = intern.getPlugin('chai'); import { spy, stub, SinonStub } from 'sinon'; import { WidgetBase, noBind } from '../../../src/widget-core/WidgetBase'; -import { v } from '../../../src/widget-core/d'; +import { v, w } from '../../../src/widget-core/d'; import { WIDGET_BASE_TYPE } from '../../../src/widget-core/Registry'; -import { VNode, WidgetMetaConstructor, WidgetMetaBase } from '../../../src/widget-core/interfaces'; +import { VNode, WidgetMetaConstructor, WidgetMetaBase, WNode } from '../../../src/widget-core/interfaces'; import { handleDecorator } from '../../../src/widget-core/decorators/handleDecorator'; import { diffProperty } from '../../../src/widget-core/decorators/diffProperty'; -import { Registry } from '../../../src/widget-core/Registry'; import { Base } from '../../../src/widget-core/meta/Base'; import { NodeEventType } from '../../../src/widget-core/NodeHandler'; import { widgetInstanceMap } from '../../../src/widget-core/vdom'; import { afterRender } from '../../../src/widget-core/decorators/afterRender'; +import { registry } from '../../../src/widget-core/decorators/registry'; interface TestProperties { foo?: string; @@ -90,14 +90,6 @@ describe('WidgetBase', () => { assert.isTrue(invalidateStub.calledTwice); }); - it('updated core properties available on instance data', () => { - const child = new BaseTestWidget(); - const instanceData = widgetInstanceMap.get(child)!; - assert.deepEqual(instanceData.coreProperties, {}); - child.__setCoreProperties__({ bind: child, baseRegistry: 'base' }); - assert.deepEqual(instanceData.coreProperties, { bind: child, baseRegistry: 'base' }); - }); - describe('__render__', () => { it('returns render result', () => { class TestWidget extends BaseTestWidget { @@ -109,7 +101,75 @@ describe('WidgetBase', () => { const renderResult = widget.__render__() as VNode; assert.strictEqual(renderResult.tag, 'my-app'); assert.lengthOf(renderResult.children!, 1); - assert.strictEqual(renderResult.children![0], 'child'); + assert.strictEqual((renderResult.children![0] as any).text, 'child'); + }); + + it('Deferred properties are run during __render__', () => { + class TestWidget extends BaseTestWidget { + render() { + return v('my-app', () => ({ foo: 'bar' }), ['child']); + } + } + const widget = new TestWidget(); + const renderResult = widget.__render__() as VNode; + assert.strictEqual(renderResult.tag, 'my-app'); + assert.isFunction(renderResult.deferredPropertiesCallback); + assert.deepEqual(renderResult.properties, { foo: 'bar' }); + assert.lengthOf(renderResult.children!, 1); + assert.strictEqual((renderResult.children![0] as any).text, 'child'); + }); + + it('Decorated properties are stored separately to resolved deferred properties', () => { + class TestWidget extends BaseTestWidget { + @afterRender() + after(node: VNode) { + node.properties = { + bar: 'foo' + }; + return node; + } + + render() { + return v('my-app', () => ({ foo: 'bar' }), ['child']); + } + } + const widget = new TestWidget(); + const renderResult = widget.__render__() as VNode; + assert.strictEqual(renderResult.tag, 'my-app'); + assert.isFunction(renderResult.deferredPropertiesCallback); + assert.deepEqual(renderResult.properties, { foo: 'bar', bar: 'foo' }); + assert.deepEqual(renderResult.originalProperties, { bar: 'foo' }); + assert.lengthOf(renderResult.children!, 1); + assert.strictEqual((renderResult.children![0] as any).text, 'child'); + }); + + it('Empty nodes are filtered from children', () => { + class TestWidget extends BaseTestWidget { + render() { + return v('my-app', ['child', undefined]); + } + } + const widget = new TestWidget(); + const renderResult = widget.__render__() as VNode; + assert.strictEqual(renderResult.tag, 'my-app'); + assert.lengthOf(renderResult.children!, 1); + assert.strictEqual((renderResult.children![0] as any).text, 'child'); + }); + + it('Resolves registry items', () => { + class Bar extends WidgetBase {} + + @registry('bar', Bar) + class TestWidget extends BaseTestWidget { + render() { + return w('bar', {}, [w('bar', {})]); + } + } + const widget = new TestWidget(); + const renderResult = widget.__render__() as WNode; + assert.strictEqual(renderResult.widgetConstructor, Bar); + assert.lengthOf(renderResult.children!, 1); + assert.strictEqual((renderResult.children![0] as WNode).widgetConstructor, Bar); }); }); @@ -197,8 +257,7 @@ describe('WidgetBase', () => { const widget = new TestWidget(); - widget.__setCoreProperties__({ bind: widget } as any); - widget.__setProperties__({ baz }); + widget.__setProperties__({ baz }, widget); widget.properties.baz && widget.properties.baz(); assert.isTrue(widget.called); }); @@ -215,9 +274,7 @@ describe('WidgetBase', () => { (baz as any)[noBind] = true; const widget = new TestWidget(); - - widget.__setCoreProperties__({ bind: widget } as any); - widget.__setProperties__({ baz }); + widget.__setProperties__({ baz }, widget); widget.properties.baz && widget.properties.baz(); assert.isFalse(widget.called); }); @@ -289,41 +346,6 @@ describe('WidgetBase', () => { assert.isTrue(invalidateSpy.calledThrice); }); - describe('__setCoreProperties__', () => { - it('new baseRegistry is added to RegistryHandler and triggers an invalidation', () => { - const baseRegistry = new Registry(); - const injector = () => 'item'; - baseRegistry.defineInjector('label', () => injector); - const widget = new BaseTestWidget(); - const invalidateSpy = spy(widget, 'invalidate'); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - assert.isTrue(invalidateSpy.calledOnce); - assert.strictEqual(widget.registry.getInjector('label')!.injector, injector); - }); - - it('The same baseRegistry does not causes an invalidation', () => { - const baseRegistry = new Registry(); - const widget = new BaseTestWidget(); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - const invalidateSpy = spy(widget, 'invalidate'); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - assert.isTrue(invalidateSpy.notCalled); - }); - - it('different baseRegistry replaces the RegistryHandlers baseRegistry and triggers an invalidation', () => { - const baseRegistry = new Registry(); - const injector = () => 'item'; - baseRegistry.defineInjector('label', () => injector); - const widget = new BaseTestWidget(); - widget.__setCoreProperties__({ bind: widget, baseRegistry: new Registry() }); - assert.isNull(widget.registry.getInjector('label')); - const invalidateSpy = spy(widget, 'invalidate'); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - assert.strictEqual(widget.registry.getInjector('label')!.injector, injector); - assert.isTrue(invalidateSpy.called); - }); - }); - it('destroys registry when WidgetBase is detached', () => { const widget = new BaseTestWidget(); const registry = widget.registry; diff --git a/tests/widget-core/unit/d.ts b/tests/widget-core/unit/d.ts index e25362254..9e920fd2a 100644 --- a/tests/widget-core/unit/d.ts +++ b/tests/widget-core/unit/d.ts @@ -3,8 +3,7 @@ const { registerSuite } = intern.getPlugin('jsdom'); import { assign } from '../../../src/shim/object'; import { DNode, VNode, WNode, WidgetProperties } from '../../../src/widget-core/interfaces'; import { WidgetBase } from '../../../src/widget-core/WidgetBase'; -import { v, w, decorate, WNODE, VNODE, isWNode, isVNode, dom } from '../../../src/widget-core/d'; -import { InternalVNode } from '../../../src/widget-core/vdom'; +import { dom, v, w, decorate, WNODE, VNODE, isWNode, isVNode } from '../../../src/widget-core/d'; interface ChildProperties extends WidgetProperties { myChildProperty: string; @@ -263,7 +262,7 @@ registerSuite('d', { attrs: { baz: 'baz' } }, [v('div'), w(WidgetBase, {})] - ) as InternalVNode; + ); assert.strictEqual(vnode.domNode, div); assert.strictEqual(vnode.tag, 'div'); assert.deepEqual(vnode.properties, { foo: 1, bar: 'bar' }); @@ -275,7 +274,7 @@ registerSuite('d', { const span = document.createElement('span'); const vnode = dom({ node: span - }) as InternalVNode; + }); assert.strictEqual(vnode.domNode, span); assert.strictEqual(vnode.tag, 'span'); assert.deepEqual(vnode.properties, {}); @@ -289,7 +288,7 @@ registerSuite('d', { const vnode = dom({ node: span, diffType: 'dom' - }) as InternalVNode; + }); assert.strictEqual(vnode.domNode, span); assert.strictEqual(vnode.tag, 'span'); assert.deepEqual(vnode.properties, {}); diff --git a/tests/widget-core/unit/decorators/alwaysRender.ts b/tests/widget-core/unit/decorators/alwaysRender.ts index c0b2bc3c1..2df97a782 100644 --- a/tests/widget-core/unit/decorators/alwaysRender.ts +++ b/tests/widget-core/unit/decorators/alwaysRender.ts @@ -4,7 +4,7 @@ const { assert } = intern.getPlugin('chai'); import { WidgetBase } from './../../../../src/widget-core/WidgetBase'; import { w } from './../../../../src/widget-core/d'; -import { ProjectorMixin } from './../../../../src/widget-core/mixins/Projector'; +import { renderer } from './../../../../src/widget-core/vdom'; import { alwaysRender } from './../../../../src/widget-core/decorators/alwaysRender'; describe('decorators/alwaysRender', () => { @@ -19,19 +19,23 @@ describe('decorators/alwaysRender', () => { } } - class Parent extends ProjectorMixin(WidgetBase) { + let invalidate: any; + class Parent extends WidgetBase { + constructor() { + super(); + invalidate = this.invalidate.bind(this); + } render() { return w(Widget, {}); } } - const projector = new Parent(); - projector.async = false; - projector.setProperties({}); - projector.append(); + const r = renderer(() => w(Parent, {})); + r.mount({ sync: true }); + renderCount = 0; + invalidate(); assert.strictEqual(renderCount, 1); - - projector.invalidate(); + invalidate(); assert.strictEqual(renderCount, 2); }); }); diff --git a/tests/widget-core/unit/decorators/beforeRender.ts b/tests/widget-core/unit/decorators/beforeRender.ts index d48e04d76..31a9fed99 100644 --- a/tests/widget-core/unit/decorators/beforeRender.ts +++ b/tests/widget-core/unit/decorators/beforeRender.ts @@ -3,7 +3,7 @@ const { assert } = intern.getPlugin('chai'); import { stub, SinonStub } from 'sinon'; import { v } from './../../../../src/widget-core/d'; -import { DNode, Render } from './../../../../src/widget-core/interfaces'; +import { DNode, Render, VNode } from './../../../../src/widget-core/interfaces'; import { beforeRender } from './../../../../src/widget-core/decorators/beforeRender'; import { WidgetBase } from './../../../../src/widget-core/WidgetBase'; @@ -105,7 +105,7 @@ registerSuite('decorators/beforeRender', { const widget = new TestWidget(); const renderResult = widget.__render__(); - assert.strictEqual(renderResult, 'first render'); + assert.strictEqual((renderResult as VNode).text, 'first render'); assert.isTrue(consoleStub.calledOnce); assert.isTrue( consoleStub.calledWith('Render function not returned from beforeRender, using previous render') diff --git a/tests/widget-core/unit/decorators/inject.ts b/tests/widget-core/unit/decorators/inject.ts index 3c85b50ec..9f34b7e49 100644 --- a/tests/widget-core/unit/decorators/inject.ts +++ b/tests/widget-core/unit/decorators/inject.ts @@ -28,8 +28,8 @@ registerSuite('decorators/inject', { @inject({ name: 'inject-one', getProperties }) class TestWidget extends WidgetBase {} const widget = new TestWidget(); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); - widget.__setProperties__({}); + widget.registry.base = registry; + widget.__setProperties__({}, widget); assert.strictEqual(widget.properties.foo, 'bar'); }, @@ -45,8 +45,8 @@ registerSuite('decorators/inject', { @inject({ name: 'inject-two', getProperties: getPropertiesTwo }) class TestWidget extends WidgetBase {} const widget = new TestWidget(); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); - widget.__setProperties__({}); + widget.registry.base = registry; + widget.__setProperties__({}, widget); assert.strictEqual(widget.properties.foo, 'bar'); assert.strictEqual(widget.properties.bar, 'foo'); }, @@ -66,8 +66,8 @@ registerSuite('decorators/inject', { } } const widget = new TestWidget(); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); - widget.__setProperties__({}); + widget.registry.base = registry; + widget.__setProperties__({}, widget); assert.strictEqual(widget.properties.foo, 'bar'); assert.strictEqual(widget.properties.bar, 'foo'); }, @@ -96,15 +96,15 @@ registerSuite('decorators/inject', { } } const widget = new TestWidget(); - widget.__setCoreProperties__({ bind: widget, baseRegistry: registry }); + widget.registry.base = registry; widget.__setProperties__({}); testInvalidate.invalidator(); - assert.strictEqual(invalidateCounter, 2); + assert.strictEqual(invalidateCounter, 1); testInvalidate.invalidator(); - assert.strictEqual(invalidateCounter, 3); + assert.strictEqual(invalidateCounter, 2); widget.destroy(); testInvalidate.invalidator(); - assert.strictEqual(invalidateCounter, 3); + assert.strictEqual(invalidateCounter, 2); } } }); diff --git a/tests/widget-core/unit/decorators/registry.ts b/tests/widget-core/unit/decorators/registry.ts index 022b5b673..fc4aa897b 100644 --- a/tests/widget-core/unit/decorators/registry.ts +++ b/tests/widget-core/unit/decorators/registry.ts @@ -6,7 +6,7 @@ const { describe } = intern.getPlugin('jsdom'); import { VNode } from './../../../../src/widget-core/interfaces'; import { registry } from './../../../../src/widget-core/decorators/registry'; import { WidgetBase } from './../../../../src/widget-core/WidgetBase'; -import ProjectorMixin from './../../../../src/widget-core/mixins/Projector'; +import { renderer } from './../../../../src/widget-core/vdom'; export class Widget1 extends WidgetBase { protected render(): VNode { @@ -29,12 +29,9 @@ describe('decorators/registry', () => { } } - const Projector = ProjectorMixin(TestWidget1); - const projector = new Projector(); - projector.async = false; - + const r = renderer(() => w(TestWidget1, {})); const root = document.createElement('div'); - projector.append(root); + r.mount({ domNode: root, sync: true }); assert.strictEqual(root.querySelectorAll('.widget1').length, 1); assert.strictEqual(root.querySelectorAll('.widget2').length, 0); @@ -51,12 +48,9 @@ describe('decorators/registry', () => { } } - const Projector = ProjectorMixin(TestWidget2); - const projector = new Projector(); - projector.async = false; - + const r = renderer(() => w(TestWidget2, {})); const root = document.createElement('div'); - projector.append(root); + r.mount({ domNode: root, sync: true }); assert.strictEqual(root.querySelectorAll('.widget1').length, 1); assert.strictEqual(root.querySelectorAll('.widget2').length, 1); diff --git a/tests/widget-core/unit/meta/Drag.ts b/tests/widget-core/unit/meta/Drag.ts index 022282d92..8005713c9 100644 --- a/tests/widget-core/unit/meta/Drag.ts +++ b/tests/widget-core/unit/meta/Drag.ts @@ -1,13 +1,13 @@ const { registerSuite } = intern.getPlugin('jsdom'); const { assert } = intern.getPlugin('chai'); import { SinonSpy } from 'sinon'; -import { v } from '../../../../src/widget-core/d'; +import { v, w } from '../../../../src/widget-core/d'; import DragCtor, { DragResults } from '../../../../src/widget-core/meta/Drag'; -import { ProjectorMixin } from '../../../../src/widget-core/mixins/Projector'; import { ThemedMixin } from '../../../../src/widget-core/mixins/Themed'; import { WidgetBase } from '../../../../src/widget-core/WidgetBase'; import sendEvent from '../../support/sendEvent'; import { createResolvers } from './../../support/util'; +import { renderer } from '../../../../src/widget-core/vdom'; const resolvers = createResolvers(); @@ -35,7 +35,7 @@ registerSuite('support/meta/Drag', { 'standard rendering'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -49,8 +49,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -78,7 +78,7 @@ registerSuite('support/meta/Drag', { 'standard rendering with a number key'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get(1234)); return v('div', { @@ -92,8 +92,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -110,7 +110,7 @@ registerSuite('support/meta/Drag', { 'pointer dragging a node'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -128,8 +128,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -238,7 +238,7 @@ registerSuite('support/meta/Drag', { 'delta should be culmative between renders'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -256,8 +256,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -385,7 +385,7 @@ registerSuite('support/meta/Drag', { 'render not done between drag and pointer up should be culmative'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -403,8 +403,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -520,7 +520,7 @@ registerSuite('support/meta/Drag', { 'movement ignored when start event missing'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -538,8 +538,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -594,7 +594,7 @@ registerSuite('support/meta/Drag', { 'dragging where descendent is target'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v( @@ -620,8 +620,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -721,7 +721,7 @@ registerSuite('support/meta/Drag', { 'dragging untracked node should not report results'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('child2')); return v( @@ -751,8 +751,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -824,7 +824,7 @@ registerSuite('support/meta/Drag', { 'non-primary button node dragging should be ignored'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -842,8 +842,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -906,7 +906,7 @@ registerSuite('support/meta/Drag', { 'two finger touch should stop dragging'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -924,8 +924,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -1027,7 +1027,13 @@ registerSuite('support/meta/Drag', { 'other invalidation properly reports empty delta'() { const dragResults: DragResults[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + let invalidate: any; + class TestWidget extends ThemedMixin(WidgetBase) { + constructor() { + super(); + invalidate = this.invalidate.bind(this); + } + render() { dragResults.push(this.meta(Drag).get('root')); return v('div', { @@ -1045,8 +1051,8 @@ registerSuite('support/meta/Drag', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div }); resolvers.resolve(); resolvers.resolve(); @@ -1085,7 +1091,7 @@ registerSuite('support/meta/Drag', { resolvers.resolve(); - widget.invalidate(); + invalidate(); resolvers.resolve(); diff --git a/tests/widget-core/unit/meta/Matches.ts b/tests/widget-core/unit/meta/Matches.ts index e0395eeee..cd454c995 100644 --- a/tests/widget-core/unit/meta/Matches.ts +++ b/tests/widget-core/unit/meta/Matches.ts @@ -3,12 +3,12 @@ const { registerSuite } = intern.getPlugin('jsdom'); import sendEvent from '../../support/sendEvent'; import { createResolvers } from './../../support/util'; -import { v } from '../../../../src/widget-core/d'; -import { ProjectorMixin } from '../../../../src/widget-core/mixins/Projector'; +import { v, w } from '../../../../src/widget-core/d'; import { WidgetBase } from '../../../../src/widget-core/WidgetBase'; import { ThemedMixin } from '../../../../src/widget-core/mixins/Themed'; import Matches from '../../../../src/widget-core/meta/Matches'; +import { renderer } from '../../../../src/widget-core/vdom'; const resolvers = createResolvers(); @@ -25,7 +25,7 @@ registerSuite('support/meta/Matches', { 'node matches'() { const results: boolean[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { private _onclick(evt: MouseEvent) { results.push(this.meta(Matches).get('root', evt)); } @@ -43,23 +43,18 @@ registerSuite('support/meta/Matches', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); - - resolvers.resolve(); - resolvers.resolve(); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div, sync: true }); sendEvent(div.firstChild as Element, 'click'); - assert.deepEqual(results, [true], 'should have been called and the target matched'); - document.body.removeChild(div); }, 'node matches with number key'() { const results: boolean[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { private _onclick(evt: MouseEvent) { results.push(this.meta(Matches).get(1234, evt)); } @@ -77,11 +72,8 @@ registerSuite('support/meta/Matches', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); - - resolvers.resolve(); - resolvers.resolve(); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div, sync: true }); sendEvent(div.firstChild as Element, 'click'); @@ -93,7 +85,7 @@ registerSuite('support/meta/Matches', { 'node does not match'() { const results: boolean[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { private _onclick(evt: MouseEvent) { results.push(this.meta(Matches).get('root', evt)); } @@ -119,11 +111,8 @@ registerSuite('support/meta/Matches', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); - - resolvers.resolve(); - resolvers.resolve(); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div, sync: true }); sendEvent(div.firstChild!.firstChild as Element, 'click', { eventInit: { @@ -139,7 +128,7 @@ registerSuite('support/meta/Matches', { 'node only exists on some renders'() { const results: boolean[] = []; - class TestWidget extends ProjectorMixin(ThemedMixin(WidgetBase)) { + class TestWidget extends ThemedMixin(WidgetBase) { private _renderSecond = false; private _onclick(evt: MouseEvent) { results.push(this.meta(Matches).get('child1', evt)); @@ -169,11 +158,8 @@ registerSuite('support/meta/Matches', { document.body.appendChild(div); - const widget = new TestWidget(); - widget.append(div); - - resolvers.resolve(); - resolvers.resolve(); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div, sync: true }); sendEvent(div.firstChild!.firstChild as Element, 'click', { eventInit: { diff --git a/tests/widget-core/unit/meta/meta.ts b/tests/widget-core/unit/meta/meta.ts index c04d484c1..14601cc94 100644 --- a/tests/widget-core/unit/meta/meta.ts +++ b/tests/widget-core/unit/meta/meta.ts @@ -4,9 +4,9 @@ import { Base as MetaBase } from '../../../../src/widget-core/meta/Base'; import { stub, spy } from 'sinon'; import { createResolvers } from './../../support/util'; import NodeHandler, { NodeEventType } from '../../../../src/widget-core/NodeHandler'; -import { v } from '../../../../src/widget-core/d'; -import { ProjectorMixin } from '../../../../src/widget-core/mixins/Projector'; +import { v, w } from '../../../../src/widget-core/d'; import { WidgetBase } from '../../../../src/widget-core/WidgetBase'; +import { renderer } from '../../../../src/widget-core/vdom'; const resolvers = createResolvers(); let bindInstance: WidgetBase; @@ -167,7 +167,19 @@ registerSuite('meta base', { } } - class TestWidget extends ProjectorMixin(WidgetBase) { + const onFoo = stub(); + const onBar = stub(); + const onWidget = stub(); + let meta: any; + class TestWidget extends WidgetBase { + constructor() { + super(); + meta = this.meta(MyMeta); + const nodeHandler = meta.getNodeHandler(); + nodeHandler.on('foo', onFoo); + nodeHandler.on('bar', onBar); + nodeHandler.on(NodeEventType.Widget, onWidget); + } render() { return v('div', { key: 'foo' }, [v('div', { key: 'bar' }, ['hello world'])]); } @@ -177,21 +189,9 @@ registerSuite('meta base', { } } - const widget = new TestWidget(); - const meta = widget.getMeta(); - - const nodeHandler = meta.getNodeHandler(); - const onFoo = stub(); - const onBar = stub(); - const onWidget = stub(); - - nodeHandler.on('foo', onFoo); - nodeHandler.on('bar', onBar); - nodeHandler.on(NodeEventType.Widget, onWidget); - const div = document.createElement('div'); - widget.append(div); - resolvers.resolve(); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div, sync: true }); assert.isTrue(meta.has('foo'), '1'); assert.isTrue(meta.has('bar'), '2'); @@ -210,7 +210,20 @@ registerSuite('meta base', { } } - class TestWidget extends ProjectorMixin(WidgetBase) { + const onFoo = stub(); + const onBar = stub(); + const onWidget = stub(); + let meta: any; + class TestWidget extends WidgetBase { + constructor() { + super(); + meta = this.meta(MyMeta); + const nodeHandler = meta.getNodeHandler(); + nodeHandler.on('foo', onFoo); + nodeHandler.on('bar', onBar); + nodeHandler.on(NodeEventType.Widget, onWidget); + } + render() { return [v('div', { key: 'foo' }), v('div', { key: 'bar' })]; } @@ -219,22 +232,9 @@ registerSuite('meta base', { return this.meta(MyMeta); } } - - const widget = new TestWidget(); - const meta = widget.getMeta(); - - const nodeHandler = meta.getNodeHandler(); - const onFoo = stub(); - const onBar = stub(); - const onWidget = stub(); - - nodeHandler.on('foo', onFoo); - nodeHandler.on('bar', onBar); - nodeHandler.on(NodeEventType.Widget, onWidget); - const div = document.createElement('div'); - widget.append(div); - resolvers.resolve(); + const r = renderer(() => w(TestWidget, {})); + r.mount({ domNode: div, sync: true }); assert.isTrue(meta.has('foo')); assert.isTrue(meta.has('bar')); diff --git a/tests/widget-core/unit/mixins/I18n.ts b/tests/widget-core/unit/mixins/I18n.ts index 30a54c9fb..286de43cc 100644 --- a/tests/widget-core/unit/mixins/I18n.ts +++ b/tests/widget-core/unit/mixins/I18n.ts @@ -195,7 +195,7 @@ registerSuite('mixins/I18nMixin', { registry.defineInjector(INJECTOR_KEY, injector); localized = new Localized(); - localized.__setCoreProperties__({ bind: localized, baseRegistry: registry }); + localized.registry.base = registry; localized.__setProperties__({}); const result = localized.__render__(); @@ -209,7 +209,7 @@ registerSuite('mixins/I18nMixin', { registry.defineInjector(INJECTOR_KEY, injector); localized = new Localized(); - localized.__setCoreProperties__({ bind: localized, baseRegistry: registry }); + localized.registry.base = registry; localized.__setProperties__({ locale: 'fr', rtl: false }); const result = localized.__render__(); @@ -222,7 +222,7 @@ registerSuite('mixins/I18nMixin', { const injector = registerI18nInjector({ locale: 'fr' }, registry); localized = new Localized(); - localized.__setCoreProperties__({ bind: localized, baseRegistry: registry }); + localized.registry.base = registry; localized.__setProperties__({}); let result = localized.__render__(); diff --git a/tests/widget-core/unit/mixins/Projector.ts b/tests/widget-core/unit/mixins/Projector.ts index 6afbc3156..973a5e226 100644 --- a/tests/widget-core/unit/mixins/Projector.ts +++ b/tests/widget-core/unit/mixins/Projector.ts @@ -1,519 +1,74 @@ -const { registerSuite } = intern.getPlugin('jsdom'); +const { it } = intern.getInterface('bdd'); +const { describe: jsdomDescribe } = intern.getPlugin('jsdom'); const { assert } = intern.getPlugin('chai'); -import global from '../../../../src/shim/global'; -import has from '../../../../src/has/has'; -import { spy, stub, SinonStub } from 'sinon'; -import { v } from '../../../../src/widget-core/d'; -import { ProjectorMixin, ProjectorAttachState } from '../../../../src/widget-core/mixins/Projector'; import { WidgetBase } from '../../../../src/widget-core/WidgetBase'; -import { VNode } from '../../../../src/widget-core/interfaces'; - -let GlobalEvent: typeof Event; - -class BaseTestWidget extends ProjectorMixin(WidgetBase) {} - -let result: any; - -class MyWidget extends BaseTestWidget { - render() { - return result; - } -} - -function dispatchEvent(element: Element, eventType: string) { - try { - element.dispatchEvent(new CustomEvent(eventType)); - } catch (e) { - const event = document.createEvent('CustomEvent'); - event.initCustomEvent(eventType, false, false, {}); - element.dispatchEvent(event); - } -} - -function sendAnimationEndEvents(element: Element) { - dispatchEvent(element, 'webkitTransitionEnd'); - dispatchEvent(element, 'webkitAnimationEnd'); - dispatchEvent(element, 'transitionend'); - dispatchEvent(element, 'animationend'); -} - -let rafStub: SinonStub; -let cancelRafStub: SinonStub; -let projector: BaseTestWidget | MyWidget; - -registerSuite('mixins/projectorMixin', { - before() { - GlobalEvent = global.window.Event; - }, - - beforeEach() { - result = null; - rafStub = stub(global, 'requestAnimationFrame').returns(1); - rafStub.yields(); - cancelRafStub = stub(global, 'cancelAnimationFrame'); - }, +import { ProjectorMixin } from '../../../../src/widget-core/mixins/Projector'; +import { v } from '../../../../src/widget-core/d'; - afterEach() { - if (projector) { - projector.destroy(); - projector = undefined as any; +class App extends WidgetBase<{ child: string }> { + protected render() { + if (this.children.length) { + return v('span', this.children); } - rafStub.restore(); - cancelRafStub.restore(); - }, - - tests: { - render: { - 'string root node'() { - result = 'my string'; - projector = new MyWidget(); - - const renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'span'); - assert.strictEqual(renderedResult.children![0], 'my string'); - }, - 'string root node after an initial render'() { - result = v('h1', ['my string']); - projector = new MyWidget(); - - let renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'h1'); - assert.strictEqual(renderedResult.children![0], 'my string'); - - result = 'my string'; - renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'span'); - assert.strictEqual(renderedResult.children![0], 'my string'); - }, - 'null root node'() { - result = null; - projector = new MyWidget(); - - const renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'span'); - assert.isNull(renderedResult.children![0]); - }, - 'null root node after an initial render'() { - result = v('h1', ['my string']); - projector = new MyWidget(); - - let renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'h1'); - assert.strictEqual(renderedResult.children![0], 'my string'); - projector.invalidate(); - result = null; - renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'span'); - assert.isNull(renderedResult.children![0]); - }, - 'undefined root node'() { - result = undefined; - projector = new MyWidget(); - - const renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'span'); - assert.isUndefined(renderedResult.children![0]); - }, - 'undefined root node after an initial render'() { - result = v('h1', ['my string']); - projector = new MyWidget(); - - let renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'h1'); - assert.strictEqual(renderedResult.children![0], 'my string'); - projector.invalidate(); - result = undefined; - renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult.tag, 'span'); - assert.isUndefined(renderedResult.children![0]); - }, - 'array root node'() { - result = [v('h1', ['my string'])]; - projector = new MyWidget(); - - const renderedResult = projector.__render__() as VNode; - assert.strictEqual(renderedResult, result); - } - }, - 'attach projector': { - append() { - const childNodeLength = document.body.childNodes.length; - projector = new BaseTestWidget(); - - projector.setChildren([v('h2', ['foo'])]); - projector.append(); - assert.strictEqual( - document.body.childNodes.length, - childNodeLength + 1, - 'child should have been added' - ); - const child = document.body.lastChild as HTMLElement; - assert.strictEqual(child.innerHTML, '

foo

'); - assert.strictEqual(child.tagName.toLowerCase(), 'div'); - assert.strictEqual((child.firstChild as HTMLElement).tagName.toLowerCase(), 'h2'); - }, - merge: { - standard() { - const div = document.createElement('div'); - document.body.appendChild(div); - const projector = new BaseTestWidget(); - - projector.setChildren([v('h2', ['foo'])]); - projector.merge(div); - assert.strictEqual(div.childNodes.length, 1, 'child should have been added'); - const child = div.lastChild as HTMLElement; - assert.strictEqual(child.innerHTML, 'foo'); - assert.strictEqual(child.tagName.toLowerCase(), 'h2'); - document.body.removeChild(div); - } - } - }, - sandbox: { - attaching() { - const childNodeLength = document.body.childNodes.length; - projector = new BaseTestWidget(); - projector.setChildren([v('h2', ['foo'])]); - - projector.sandbox(); - - assert.strictEqual( - document.body.childNodes.length, - childNodeLength, - 'No nodes should be added to body' - ); - assert.isTrue( - projector.root instanceof global.window.DocumentFragment, - 'the root should be a document fragment' - ); - const child = projector.root.firstChild as HTMLElement; - assert.strictEqual(child.innerHTML, '

foo

'); - assert.strictEqual(child.tagName.toLocaleLowerCase(), 'div'); - assert.strictEqual((child.firstChild as HTMLElement).tagName.toLocaleLowerCase(), 'h2'); - - projector.destroy(); - assert.strictEqual(projector.root, document.body, 'Root should be reverted to document.body'); - }, - 'operates synchronously'() { - let count = 0; - const projector = new class extends BaseTestWidget { - render() { - count++; - return v('div', [String(count)]); - } - }(); - - projector.sandbox(); - assert.strictEqual(count, 1, 'render should have been called once'); - assert.strictEqual(projector.root.firstChild!.textContent, '1', 'should have rendered "1"'); - projector.invalidate(); - assert.strictEqual(count, 2, 'render should have been called synchronously'); - assert.strictEqual(projector.root.firstChild!.textContent, '2', 'should have rendered "2"'); - projector.destroy(); - }, - 'accepts other documents'() { - const doc = { - createDocumentFragment: spy(() => document.createDocumentFragment()) - } as any; - const projector = new BaseTestWidget(); - projector.sandbox(doc); - assert.isTrue(doc.createDocumentFragment.called, 'createDocumentFragment should have been called'); - projector.destroy(); - } - }, - 'get root'() { - const projector = new BaseTestWidget(); - const root = document.createElement('div'); - assert.equal(projector.root, document.body); - projector.root = root; - assert.equal(projector.root, root); - }, - 'get projector state'() { - const projector = new BaseTestWidget(); - - assert.equal(projector.projectorState, ProjectorAttachState.Detached); - projector.append(); - assert.equal(projector.projectorState, ProjectorAttachState.Attached); - projector.destroy(); - assert.equal(projector.projectorState, ProjectorAttachState.Detached); - }, - async: { - 'can set async mode on projector'() { - const projector = new BaseTestWidget(); - assert.isTrue(projector.async); - projector.async = false; - assert.isFalse(projector.async); - }, - 'cannot set async mode on projector that is already attached'() { - const projector = new BaseTestWidget(); - projector.append(); - assert.throws( - () => { - projector.async = false; - }, - Error, - 'Projector already attached, cannot change async mode' - ); - } - }, - 'toHtml()': { - appended() { - const projector = new BaseTestWidget(); - projector.setChildren([v('h2', ['foo'])]); - - const div = document.createElement('div'); - projector.append(div); - assert.strictEqual(projector.toHtml(), `

foo

`); - assert.strictEqual(projector.toHtml(), (projector.root.lastChild as Element).outerHTML); - projector.destroy(); - }, - merged() { - const root = document.createElement('div'); - const div = document.createElement('div'); - document.body.appendChild(root); - root.appendChild(div); - - const projector = new BaseTestWidget(); - projector.setChildren([v('h2', ['foo'])]); - - projector.merge(div); - assert.strictEqual(projector.toHtml(), `

foo

`); - assert.strictEqual(projector.toHtml(), (projector.root as Element).outerHTML); - projector.destroy(); - }, - 'not attached throws'() { - const projector = new BaseTestWidget(); - assert.throws( - () => { - projector.toHtml(); - }, - Error, - 'Projector is not attached, cannot return an HTML string of projection.' - ); - } - }, - 'setProperties guards against original property interface'() { - interface Props { - foo: string; - } - - class TestClass extends WidgetBase {} - const ProjectorClass = ProjectorMixin(TestClass); - const projector = new ProjectorClass(); - projector.setProperties({ foo: 'f' }); - // Demonstrates the type guarding for widget properties - - // projector.setProperties({ foo: true }); - }, - 'invalidate before attached'() { - const projector: any = new BaseTestWidget(); - - projector.invalidate(); - - assert.isFalse(rafStub.called); - }, - 'invalidate after attached'() { - const projector: any = new BaseTestWidget(); - - projector.append(); - projector.invalidate(); - assert.isTrue(rafStub.called); - }, - reattach() { - const root = document.createElement('div'); - const projector = new BaseTestWidget(); - const promise = projector.append(root); - assert.strictEqual(promise, projector.append(), 'same promise should be returned'); - }, - 'setRoot throws when already attached'() { - const projector = new BaseTestWidget(); - const div = document.createElement('div'); - projector.root = div; - projector.append(); - assert.throws( - () => { - projector.root = document.body; - }, - Error, - 'already attached' - ); - }, - 'sandbox throws when already attached'() { - const projector = new BaseTestWidget(); - projector.append(); - assert.throws( - () => { - projector.sandbox(); - }, - Error, - 'Projector already attached, cannot create sandbox' - ); - }, - 'can attach an event handler'() { - let domEvent: any; - const oninput = (evt: any) => { - domEvent = evt; - }; - - const Projector = class extends BaseTestWidget { - render() { - return v('div', { oninput, id: 'handler-test-root' }); - } - }; - - const projector = new Projector(); - projector.append(); - const domNode = document.getElementById('handler-test-root'); - dispatchEvent(domNode as HTMLElement, 'input'); - assert.isTrue(domEvent instanceof GlobalEvent); - }, - 'can attach an event listener'() { - let domEvent: any; - const onpointermove = (evt: any) => { - domEvent = evt; - }; - - const Projector = class extends BaseTestWidget { - render() { - return v('div', { onpointermove, id: 'listener-test-root' }); - } - }; - - const projector = new Projector(); - projector.append(); - const domNode = document.getElementById('listener-test-root'); - dispatchEvent(domNode as HTMLElement, 'pointermove'); - assert.isTrue(domEvent instanceof GlobalEvent); - }, - '-active gets appended to enter/exit animations by default'(this: any) { - if (!has('host-browser')) { - this.skip('This test can only be run in a browser'); - } - - let children: any[] = []; - - class TestProjector extends BaseTestWidget { - root = document.body; - - render() { - return v('div', { id: 'root' }, children); - } - } - - const projector = new TestProjector(); - projector.async = false; - projector.append(); - - children = [ - v('div', { - id: 'test-element', - enterAnimation: 'fade-in', - exitAnimation: 'fade-out' - }) - ]; - - projector.invalidate(); - - const domNode = document.getElementById('test-element')!; - assert.isNotNull(domNode); - assert.isTrue(domNode.classList.contains('fade-in')); - assert.isTrue(domNode.classList.contains('fade-in-active')); - - sendAnimationEndEvents(domNode); - - children = []; - projector.invalidate(); - - assert.isTrue(domNode.classList.contains('fade-out')); - assert.isTrue(domNode.classList.contains('fade-out-active')); - - domNode.parentElement!.removeChild(domNode); - }, - async 'active/exit classes can be customized'(this: any) { - if (!has('host-browser')) { - this.skip('This test can only be run in a browser'); - } - - let children: any[] = []; - - class TestProjector extends BaseTestWidget { - root = document.body; - - render() { - return v('div', {}, children); - } - } - - const projector = new TestProjector(); - projector.async = false; - projector.append(); - - children = [ - v('div', { - id: 'test-element', - enterAnimation: 'fade-in', - enterAnimationActive: 'active-fade-in', - exitAnimation: 'fade-out', - exitAnimationActive: 'active-fade-out' - }) - ]; - - projector.invalidate(); - - const domNode = document.getElementById('test-element')!; - assert.isNotNull(domNode); - assert.isTrue(domNode.classList.contains('fade-in')); - assert.isTrue(domNode.classList.contains('active-fade-in')); - - sendAnimationEndEvents(domNode); - - children = []; - projector.invalidate(); - - assert.isTrue(domNode.classList.contains('fade-out')); - assert.isTrue(domNode.classList.contains('active-fade-out')); - - domNode.parentElement!.removeChild(domNode); - }, - - 'dom nodes get removed after exit animations'(this: any) { - if (!has('host-browser')) { - this.skip('This test can only be run in a browser'); - } - - let children: any[] = [ - v('div', { - id: 'test-element', - enterAnimation: 'fade-in', - exitAnimation: 'fade-out' - }) - ]; - - class TestProjector extends BaseTestWidget { - root = document.body; - - render() { - return v('div', {}, children); - } - } - - const projector = new TestProjector(); - projector.async = false; - projector.append(); - - const domNode = document.getElementById('test-element')!; - assert.isNotNull(domNode); - - children = []; - projector.invalidate(); - - assert.isTrue(domNode.classList.contains('fade-out')); - assert.isTrue(domNode.classList.contains('fade-out-active')); - - // manually fire the transition end events - sendAnimationEndEvents(domNode); - - assert.isNull(document.getElementById('test-element')); + if (this.properties.child) { + return v('span', [this.properties.child]); } + return v('span', ['widget']); } +} + +jsdomDescribe('Projector', () => { + it('should render the widget using append', () => { + const div = document.createElement('div'); + const Projector = ProjectorMixin(App); + const projector = new Projector(); + projector.append(div); + assert.strictEqual((div.childNodes[0].childNodes[0] as Text).data, 'widget'); + }); + + it('should render the widget using merge', () => { + const div = document.createElement('div'); + const span = document.createElement('span'); + div.appendChild(span); + const Projector = ProjectorMixin(App); + const projector = new Projector(); + projector.merge(span); + assert.strictEqual((span.childNodes[0] as Text).data, 'widget'); + }); + + it('should set the properties for the widget', () => { + const div = document.createElement('div'); + const Projector = ProjectorMixin(App); + const projector = new Projector(); + projector.setProperties({ child: 'property' }); + projector.append(div); + assert.strictEqual((div.childNodes[0].childNodes[0] as Text).data, 'property'); + }); + + it('should set the children for the widget', () => { + const div = document.createElement('div'); + const Projector = ProjectorMixin(App); + const projector = new Projector(); + projector.setChildren(['child']); + projector.append(div); + assert.strictEqual((div.childNodes[0].childNodes[0] as Text).data, 'child'); + }); + + it('should return html string when ', () => { + const div = document.createElement('div'); + const Projector = ProjectorMixin(App); + const projector = new Projector(); + projector.append(div); + assert.strictEqual(projector.toHtml(), 'widget'); + }); + + it('should set the root node of the projector ', () => { + const div = document.createElement('div'); + const Projector = ProjectorMixin(App); + const projector = new Projector(); + projector.root = div; + projector.append(); + assert.strictEqual((div.childNodes[0].childNodes[0] as Text).data, 'widget'); + }); }); diff --git a/tests/widget-core/unit/mixins/Themed.ts b/tests/widget-core/unit/mixins/Themed.ts index 8e01fbc50..45a0e2d41 100644 --- a/tests/widget-core/unit/mixins/Themed.ts +++ b/tests/widget-core/unit/mixins/Themed.ts @@ -19,6 +19,7 @@ import * as extraClasses1 from './../../support/styles/extraClasses1.css'; import testTheme1 from './../../support/styles/theme1.css'; import testTheme2 from './../../support/styles/theme2.css'; import testTheme3 from './../../support/styles/theme3.css'; +import { VNode } from '../../../../src/widget-core/interfaces'; (baseThemeClasses1 as any)[' _key'] = 'testPath1'; (baseThemeClasses2 as any)[' _key'] = 'testPath2'; @@ -209,9 +210,9 @@ registerSuite('ThemedMixin', { } } const ThemedInstance = new InjectedTheme(); - ThemedInstance.__setCoreProperties__({ bind: ThemedInstance, baseRegistry: testRegistry }); + ThemedInstance.registry.base = testRegistry; ThemedInstance.__setProperties__({}); - const renderResult: any = ThemedInstance.__render__(); + const renderResult = ThemedInstance.__render__() as VNode; assert.deepEqual(renderResult.properties.classes, 'theme1Class1'); }, 'theme will not be injected if a theme has been passed via a property'() { @@ -223,9 +224,9 @@ registerSuite('ThemedMixin', { } } const ThemedInstance = new InjectedTheme(); - ThemedInstance.__setCoreProperties__({ bind: ThemedInstance, baseRegistry: testRegistry }); + ThemedInstance.registry.base = testRegistry; ThemedInstance.__setProperties__({ theme: testTheme2 }); - const renderResult: any = ThemedInstance.__render__(); + const renderResult = ThemedInstance.__render__() as VNode; assert.deepEqual(renderResult.properties.classes, 'theme2Class1'); }, 'does not attempt to inject if the ThemeInjector has not been defined in the registry'() { @@ -235,7 +236,7 @@ registerSuite('ThemedMixin', { } } const ThemedInstance = new InjectedTheme(); - const renderResult: any = ThemedInstance.__render__(); + const renderResult = ThemedInstance.__render__() as VNode; assert.deepEqual(renderResult.properties.classes, 'baseClass1'); }, 'setting the theme invalidates and the new theme is used'() { @@ -247,20 +248,20 @@ registerSuite('ThemedMixin', { } const testWidget = new InjectedTheme(); - testWidget.__setCoreProperties__({ bind: testWidget, baseRegistry: testRegistry }); - let renderResult: any = testWidget.__render__(); + testWidget.registry.base = testRegistry; + let renderResult = testWidget.__render__() as VNode; assert.deepEqual(renderResult.properties.classes, baseThemeClasses1.class1); themeInjectorContext.set(testTheme2); testWidget.__setProperties__({}); - renderResult = testWidget.__render__(); + renderResult = testWidget.__render__() as VNode; assert.deepEqual(renderResult.properties.classes, 'theme2Class1'); themeInjectorContext.set(testTheme1); testWidget.__setProperties__({}); - renderResult = testWidget.__render__(); + renderResult = testWidget.__render__() as VNode; assert.deepEqual(renderResult.properties.classes, 'theme1Class1'); themeInjectorContext.set(testTheme1); testWidget.__setProperties__({ foo: 'bar' }); - renderResult = testWidget.__render__(); + renderResult = testWidget.__render__() as VNode; assert.deepEqual(renderResult.properties.classes, 'theme1Class1'); } }, diff --git a/tests/widget-core/unit/mixins/all.ts b/tests/widget-core/unit/mixins/all.ts index a21bd1f51..782e9625d 100644 --- a/tests/widget-core/unit/mixins/all.ts +++ b/tests/widget-core/unit/mixins/all.ts @@ -1,4 +1,4 @@ import './Focus'; import './Themed'; -import './Projector'; import './I18n'; +import './Projector'; diff --git a/tests/widget-core/unit/registerCustomElement.ts b/tests/widget-core/unit/registerCustomElement.ts index fa5d71611..830589c06 100644 --- a/tests/widget-core/unit/registerCustomElement.ts +++ b/tests/widget-core/unit/registerCustomElement.ts @@ -178,7 +178,7 @@ describe('registerCustomElement', () => { (barB as any).myProp = 'set property on child'; resolvers.resolve(); - assert.strictEqual(2, childRenderCounter); + assert.strictEqual(3, childRenderCounter); const container = element.querySelector('.children'); const children = (container as any).children; diff --git a/tests/widget-core/unit/tsxIntegration.tsx b/tests/widget-core/unit/tsxIntegration.tsx index 20685f0bc..78db562db 100644 --- a/tests/widget-core/unit/tsxIntegration.tsx +++ b/tests/widget-core/unit/tsxIntegration.tsx @@ -36,16 +36,14 @@ registerSuite('tsx integration', { } const bar = new Bar(); - bar.__setCoreProperties__({ bind: bar, baseRegistry: registry }); - bar.__setProperties__({ registry }); + bar.registry.base = registry; const barRender = bar.__render__() as WNode; assert.deepEqual(barRender.properties, { hello: 'world' } as any); assert.strictEqual(barRender.widgetConstructor, Foo); assert.lengthOf(barRender.children, 0); const qux = new Qux(); - qux.__setCoreProperties__({ bind: qux, baseRegistry: registry }); - qux.__setProperties__({ registry }); + qux.registry.base = registry; const firstQuxRender = qux.__render__() as WNode; assert.strictEqual(firstQuxRender.widgetConstructor, 'LazyFoo'); } diff --git a/tests/widget-core/unit/vdom.ts b/tests/widget-core/unit/vdom.ts index a2bab72f1..a0e0cacc0 100644 --- a/tests/widget-core/unit/vdom.ts +++ b/tests/widget-core/unit/vdom.ts @@ -1,70 +1,78 @@ const { afterEach, beforeEach, describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); const { describe: jsdomDescribe } = intern.getPlugin('jsdom'); -import { match, spy, stub, SinonStub, SinonSpy } from 'sinon'; +import { match, spy, stub, SinonSpy } from 'sinon'; import { createResolvers } from './../support/util'; -import sendEvent from './../support/sendEvent'; +import sendEvent from '../support/sendEvent'; -import { dom, InternalVNode, InternalWNode, widgetInstanceMap, RenderResult } from '../../../src/widget-core/vdom'; -import { dom as d, v, w, VNODE } from '../../../src/widget-core/d'; -import { VNode, DNode } from '../../../src/widget-core/interfaces'; +import { renderer, widgetInstanceMap } from '../../../src/widget-core/vdom'; +import { v, w, dom as d, VNODE } from '../../../src/widget-core/d'; +import { VNode, DNode, DomVNode } from '../../../src/widget-core/interfaces'; import { WidgetBase } from '../../../src/widget-core/WidgetBase'; +import Registry from '../../../src/widget-core/Registry'; import { I18nMixin } from '../../../src/widget-core/mixins/I18n'; -import { Registry } from '../../../src/widget-core/Registry'; - -let consoleStub: SinonStub; +import registry from '../../../src/widget-core/decorators/registry'; const resolvers = createResolvers(); -function getWidget(renderResult: RenderResult) { - return new class extends WidgetBase { - private _renderResult: RenderResult | (() => RenderResult) = renderResult; - private _nodeHandlerStub = { - add: stub(), - addRoot: stub() - }; - private _onElementCreatedStub = stub(); - private _onElementUpdatedStub = stub(); - private _onAttachStub = stub(); - private _onDetachStub = stub(); - - constructor() { - super(); - const instanceData = widgetInstanceMap.get(this)!; - const stubs: any = { - nodeHandler: this._nodeHandlerStub, - onElementCreated: this._onElementCreatedStub, - onElementUpdated: this._onElementUpdatedStub, - onAttach: this._onAttachStub, - onDetach: this._onDetachStub +function getWidget(renderResult: any) { + let meta: any = {}; + return [ + class extends WidgetBase { + private _renderResult: any | (() => any) = renderResult; + private _nodeHandlerStub = { + add: stub(), + addRoot: stub() }; - widgetInstanceMap.set(this, { ...instanceData, ...stubs }); - } + private _onElementCreatedStub = stub(); + private _onElementUpdatedStub = stub(); + private _onAttachStub = stub(); + private _onDetachStub = stub(); + + constructor() { + super(); + const instanceData = widgetInstanceMap.get(this)!; + const stubs: any = { + nodeHandler: this._nodeHandlerStub, + onElementCreated: this._onElementCreatedStub, + onElementUpdated: this._onElementUpdatedStub, + onAttach: this._onAttachStub, + onDetach: this._onDetachStub + }; + meta.setRenderResult = this.setRenderResult; + meta.nodeHandlerStub = this._nodeHandlerStub; + meta.onAttachStub = this._onAttachStub; + meta.onDetachStub = this._onDetachStub; + meta.invalidate = this.invalidate.bind(this); + widgetInstanceMap.set(this, { ...instanceData, ...stubs }); + } - render() { - if (typeof this._renderResult === 'function') { - return this._renderResult(); + render() { + if (typeof this._renderResult === 'function') { + return this._renderResult(); + } + return this._renderResult; } - return this._renderResult; - } - public set renderResult(renderResult: RenderResult) { - this._renderResult = renderResult; - this.invalidate(); - } + public setRenderResult = (renderResult: any) => { + this._renderResult = renderResult; + this.invalidate(); + }; - public get nodeHandlerStub() { - return this._nodeHandlerStub; - } + public get nodeHandlerStub() { + return this._nodeHandlerStub; + } - public get onAttachStub() { - return this._onAttachStub; - } + public get onAttachStub() { + return this._onAttachStub; + } - public get onDetachStub() { - return this._onDetachStub; - } - }(); + public get onDetachStub() { + return this._onDetachStub; + } + }, + meta + ]; } class MainBar extends WidgetBase { @@ -97,12 +105,10 @@ jsdomDescribe('vdom', () => { const spys: SinonSpy[] = []; beforeEach(() => { - consoleStub = stub(console, 'warn'); resolvers.stub(); }); afterEach(() => { - consoleStub.restore(); resolvers.restore(); for (let spy of spys) { spy.restore(); @@ -111,13 +117,66 @@ jsdomDescribe('vdom', () => { }); describe('widgets', () => { - it('should create elements for widgets', () => { - const widget = new TestWidget(); - widget.__setCoreProperties__({ bind: widget } as any); - widget.__setProperties__({ show: true }); + it('Should render nodes in the correct order with mix of vnode and wnodes', () => { + class WidgetOne extends WidgetBase { + render() { + return w(WidgetTwo, {}); + } + } - const projection = dom.create(widget, { sync: true }); - const span = (projection.domNode.childNodes[0] as Element) as HTMLSpanElement; + class WidgetTwo extends WidgetBase { + render() { + return v('div', ['dom2']); + } + } + + class WidgetThree extends WidgetBase { + render() { + return ['dom3', 'dom3a']; + } + } + + class WidgetFour extends WidgetBase { + render() { + return w(WidgetFive, {}); + } + } + + class WidgetFive extends WidgetBase { + render() { + return w(WidgetSix, {}); + } + } + + class WidgetSix extends WidgetBase { + render() { + return 'dom5'; + } + } + + class Parent extends WidgetBase { + render() { + return ['dom1', w(WidgetOne, {}), w(WidgetThree, {}), 'dom4', w(WidgetFour, {}), 'dom6']; + } + } + + const r = renderer(() => w(Parent, {})); + const root: any = document.createElement('div'); + r.mount({ domNode: root }); + assert.strictEqual(root.childNodes[0].data, 'dom1'); + assert.strictEqual(root.childNodes[1].childNodes[0].data, 'dom2'); + assert.strictEqual(root.childNodes[2].data, 'dom3'); + assert.strictEqual(root.childNodes[3].data, 'dom3a'); + assert.strictEqual(root.childNodes[4].data, 'dom4'); + assert.strictEqual(root.childNodes[5].data, 'dom5'); + assert.strictEqual(root.childNodes[6].data, 'dom6'); + }); + + it('should create elements for widgets', () => { + const r = renderer(() => w(TestWidget, { show: true })); + const root = document.createElement('div'); + r.mount({ domNode: root }); + const span = (root.childNodes[0] as Element) as HTMLSpanElement; assert.lengthOf(span.childNodes, 1); const div = span.childNodes[0] as HTMLDivElement; assert.lengthOf(div.childNodes, 5); @@ -144,90 +203,6 @@ jsdomDescribe('vdom', () => { assert.strictEqual(headerTwo.innerHTML, 'span'); }); - it('should update elements for widget changes', () => { - const widget = new TestWidget(); - widget.__setCoreProperties__({ bind: widget } as any); - widget.__setProperties__({ show: true }); - - const projection = dom.create(widget, { sync: true }); - const root = (projection.domNode.childNodes[0] as Element) as HTMLSpanElement; - - assert.lengthOf(root.childNodes, 1); - let rootChild = root.childNodes[0] as HTMLDivElement; - assert.lengthOf(rootChild.childNodes, 5); - assert.strictEqual(rootChild.getAttribute('foo'), 'bar'); - - let headerOne = rootChild.childNodes[0] as HTMLHeadElement; - let spanOne = rootChild.childNodes[1] as HTMLSpanElement; - let spanTwo = rootChild.childNodes[2] as HTMLSpanElement; - let text = rootChild.childNodes[3] as Text; - let headerTwo = rootChild.childNodes[4] as HTMLHeadElement; - - assert.lengthOf(headerOne.childNodes, 1); - assert.strictEqual((headerOne.childNodes[0] as Text).data, 'Hello Widget'); - - assert.lengthOf(spanOne.childNodes, 1); - assert.strictEqual(spanOne.innerHTML, 'Bar'); - - assert.lengthOf(spanTwo.childNodes, 1); - assert.strictEqual(spanTwo.innerHTML, 'Bar'); - - assert.strictEqual(text.data, 'text node'); - - assert.lengthOf(headerTwo.childNodes, 1); - assert.strictEqual(headerTwo.innerHTML, 'span'); - - widget.__setProperties__({ show: false }); - - assert.lengthOf(root.childNodes, 1); - rootChild = root.childNodes[0] as HTMLDivElement; - assert.lengthOf(rootChild.childNodes, 4); - assert.strictEqual(rootChild.getAttribute('foo'), 'bar'); - - headerOne = rootChild.childNodes[0] as HTMLHeadElement; - let insertedDiv = rootChild.childNodes[1] as HTMLDivElement; - text = rootChild.childNodes[2] as Text; - headerTwo = rootChild.childNodes[3] as HTMLHeadElement; - - assert.lengthOf(headerOne.childNodes, 1); - assert.strictEqual((headerOne.childNodes[0] as Text).data, 'Hello Widget'); - - assert.lengthOf(insertedDiv.childNodes, 1); - assert.strictEqual((insertedDiv.childNodes[0] as Text).data, 'me'); - - assert.strictEqual(text.data, 'text node'); - - assert.lengthOf(headerTwo.childNodes, 1); - assert.strictEqual(headerTwo.innerHTML, 'span'); - - widget.__setProperties__({ show: true }); - - assert.lengthOf(root.childNodes, 1); - rootChild = root.childNodes[0] as HTMLDivElement; - assert.lengthOf(rootChild.childNodes, 5); - assert.strictEqual(rootChild.getAttribute('foo'), 'bar'); - - headerOne = rootChild.childNodes[0] as HTMLHeadElement; - spanOne = rootChild.childNodes[1] as HTMLSpanElement; - spanTwo = rootChild.childNodes[2] as HTMLSpanElement; - text = rootChild.childNodes[3] as Text; - headerTwo = rootChild.childNodes[4] as HTMLHeadElement; - - assert.lengthOf(headerOne.childNodes, 1); - assert.strictEqual((headerOne.childNodes[0] as Text).data, 'Hello Widget'); - - assert.lengthOf(spanOne.childNodes, 1); - assert.strictEqual(spanOne.innerHTML, 'Bar'); - - assert.lengthOf(spanTwo.childNodes, 1); - assert.strictEqual(spanTwo.innerHTML, 'Bar'); - - assert.strictEqual(text.data, 'text node'); - - assert.lengthOf(headerTwo.childNodes, 1); - assert.strictEqual(headerTwo.innerHTML, 'span'); - }); - it('invalidates up the widget tree', () => { class Foo extends WidgetBase { private _id = 0; @@ -254,10 +229,11 @@ jsdomDescribe('vdom', () => { } } - const widget = new Baz(); - const projection = dom.create(widget, { sync: true }); + const div = document.createElement('div'); + const r = renderer(() => w(Baz, {})); + r.mount({ domNode: div, sync: true }); - const root = (projection.domNode.childNodes[0] as Element) as HTMLElement; + const root = div.childNodes[0] as HTMLElement; assert.lengthOf(root.childNodes, 1); const barDiv = root.childNodes[0]; assert.lengthOf(barDiv.childNodes, 2); @@ -281,6 +257,7 @@ jsdomDescribe('vdom', () => { assert.strictEqual(fooTwoDiv.childNodes[0], fooTwoTextNode); assert.strictEqual(fooOneTextNode.data, '0'); assert.strictEqual(fooTwoTextNode.data, '0'); + sendEvent(fooOneDiv, 'click'); assert.lengthOf(root.childNodes, 1); @@ -294,6 +271,7 @@ jsdomDescribe('vdom', () => { assert.strictEqual(fooTwoDiv.childNodes[0], fooTwoTextNode); const updatedFooOneTextNode = fooOneDiv.childNodes[0] as Text; assert.strictEqual(updatedFooOneTextNode.data, '1'); + sendEvent(fooTwoDiv, 'click'); assert.lengthOf(root.childNodes, 1); @@ -324,11 +302,10 @@ jsdomDescribe('vdom', () => { return v('div', { onclick: this.properties.onClick }); } } + let clickerCount = 0; class App extends WidgetBase { - public onClickCount = 0; - _onClick() { - this.onClickCount++; + clickerCount++; } render() { @@ -344,22 +321,22 @@ jsdomDescribe('vdom', () => { } } - const widget = new App(); - const projection: any = dom.create(widget, { sync: true }); - sendEvent(projection.domNode.childNodes[0], 'click', { eventInit: { bubbles: false } }); - sendEvent(projection.domNode.childNodes[0].childNodes[0], 'click', { eventInit: { bubbles: false } }); - sendEvent(projection.domNode.childNodes[0].childNodes[0].childNodes[0], 'click', { + const div = document.createElement('div'); + const r = renderer(() => w(App, {})); + r.mount({ domNode: div, sync: true }); + sendEvent(div.childNodes[0] as any, 'click', { eventInit: { bubbles: false } }); + sendEvent(div.childNodes[0].childNodes[0] as any, 'click', { eventInit: { bubbles: false } }); + sendEvent(div.childNodes[0].childNodes[0].childNodes[0] as any, 'click', { eventInit: { bubbles: false } }); - sendEvent(projection.domNode.childNodes[0].childNodes[0].childNodes[0].childNodes[0], 'click', { + sendEvent(div.childNodes[0].childNodes[0].childNodes[0].childNodes[0] as any, 'click', { eventInit: { bubbles: false } }); - assert.strictEqual(widget.onClickCount, 4); + assert.strictEqual(clickerCount, 4); }); it('supports widget registry items', () => { - const baseRegistry = new Registry(); - + const registry = new Registry(); class Foo extends WidgetBase { render() { return v('h1', [this.properties.text]); @@ -371,18 +348,18 @@ jsdomDescribe('vdom', () => { } } - baseRegistry.define('foo', Foo); - baseRegistry.define('bar', Bar); + registry.define('foo', Foo); + registry.define('bar', Bar); class Baz extends WidgetBase { render() { return v('div', [w('foo', { text: 'foo' }), w('bar', { text: 'bar' })]); } } - const widget = new Baz(); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - const projection: any = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true, registry }); + const root = div.childNodes[0]; const headerOne = root.childNodes[0]; const headerOneText = headerOne.childNodes[0] as Text; const headerTwo = root.childNodes[1]; @@ -393,7 +370,7 @@ jsdomDescribe('vdom', () => { it('registry items', () => { let resolver = () => {}; - const baseRegistry = new Registry(); + const registry = new Registry(); class Widget extends WidgetBase { render() { return v('div', ['Hello, world!']); @@ -409,16 +386,15 @@ jsdomDescribe('vdom', () => { resolve(RegistryWidget); }; }); - baseRegistry.define('registry-item', promise); + registry.define('registry-item', promise); class App extends WidgetBase { render() { return [w('registry-item', {}), w(Widget, {})]; } } - const widget = new App(); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode as HTMLElement; + const r = renderer(() => w(App, {})); + const root = document.createElement('div'); + r.mount({ domNode: root, sync: true, registry }); assert.lengthOf(root.childNodes, 1); assert.strictEqual((root.childNodes[0].childNodes[0] as Text).data, 'Hello, world!'); resolver(); @@ -430,7 +406,7 @@ jsdomDescribe('vdom', () => { }); it('should invalidate when a registry items is loaded', () => { - const baseRegistry = new Registry(); + const registry = new Registry(); class Foo extends WidgetBase { render() { @@ -458,13 +434,13 @@ jsdomDescribe('vdom', () => { } } - const widget = new Baz(); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true, registry }); + const root = div.childNodes[0] as Element; assert.lengthOf(root.childNodes, 0); - baseRegistry.define('foo', Foo); - baseRegistry.define('bar', Bar); + registry.define('foo', Foo); + registry.define('bar', Bar); const headerOne = root.childNodes[0]; const headerOneText = headerOne.childNodes[0] as Text; @@ -474,6 +450,32 @@ jsdomDescribe('vdom', () => { assert.strictEqual(headerTwoText.data, 'bar'); }); + it('scopes registry to the widget that the WNode is defined', () => { + class Foo extends WidgetBase { + render() { + return this.children; + } + } + + class Bar extends WidgetBase { + render() { + return 'BAR'; + } + } + + @registry('bar', Bar) + class Qux extends WidgetBase { + render() { + return v('div', [w(Foo, {}, [w('bar', {})])]); + } + } + + const r = renderer(() => w(Qux, {})); + const div: any = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + assert.strictEqual(div.childNodes[0].childNodes[0].data, 'BAR'); + }); + it('supports an array of DNodes', () => { class Foo extends WidgetBase { private myClass = false; @@ -496,9 +498,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Bar(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Bar, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0] as Element; assert.lengthOf(root.childNodes, 3); const childOne = root.childNodes[0]; assert.lengthOf(childOne.childNodes, 1); @@ -512,12 +515,6 @@ jsdomDescribe('vdom', () => { assert.lengthOf(childThree.childNodes, 1); const textNodeThree = childThree.childNodes[0] as Text; assert.strictEqual(textNodeThree.data, '3'); - - widget.invalidate(); - const secondRenderResult = widget.__render__() as VNode; - const firstWNode = secondRenderResult.children![0] as InternalWNode; - const secondWNode = secondRenderResult.children![0] as InternalWNode; - assert.strictEqual(firstWNode.rendered, secondWNode.rendered); }); it('supports null and undefined return from render', () => { @@ -539,9 +536,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Baz(); - const projection: any = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0] as Element; assert.lengthOf(root.childNodes, 0); }); @@ -565,10 +563,16 @@ jsdomDescribe('vdom', () => { } } + let show: any; class Baz extends WidgetBase { private _show = false; - set show(value: boolean) { + constructor() { + super(); + show = this.show.bind(this); + } + + show(value: boolean) { this._show = value; this.invalidate(); } @@ -578,11 +582,12 @@ jsdomDescribe('vdom', () => { } } - const widget = new Baz(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0] as Element; assert.lengthOf(root.childNodes, 0); - widget.show = true; + show(true); assert.lengthOf(root.childNodes, 1); const fooDiv = root.childNodes[0] as HTMLDivElement; @@ -609,47 +614,53 @@ jsdomDescribe('vdom', () => { } } + let setProperties: any; class Baz extends WidgetBase { + constructor() { + super(); + setProperties = this.__setProperties__.bind(this); + } render() { const { widget = 'default' } = this.properties; return v('div', [ - v('div', ['first']), - w(widget, {}), - w(widget, {}), - v('div', ['second']), - w(widget, {}) + v('div', { key: '1' }, ['first']), + w(widget, { key: '2' }), + w(widget, { key: '3' }), + v('div', { key: '4' }, ['second']), + w(widget, { key: '5' }) ]); } } - const baseRegistry = new Registry(); - baseRegistry.define('foo', Foo); - baseRegistry.define('bar', Bar); - const widget = new Baz(); - widget.__setCoreProperties__({ bind: widget, baseRegistry }); - const projection = dom.create(widget, { sync: true }); - const root: any = projection.domNode.childNodes[0]; + const registry = new Registry(); + registry.define('foo', Foo); + registry.define('bar', Bar); + + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true, registry }); + const root: any = div.childNodes[0] as Element; assert.strictEqual(root.childNodes[0].childNodes[0].data, 'first'); assert.strictEqual(root.childNodes[1].childNodes[0].data, 'second'); - widget.__setProperties__({ widget: 'other' }); + setProperties({ widget: 'other' }); assert.strictEqual(root.childNodes[0].childNodes[0].data, 'first'); assert.strictEqual(root.childNodes[1].childNodes[0].data, 'second'); - widget.__setProperties__({ widget: 'foo' }); + setProperties({ widget: 'foo' }); assert.strictEqual(root.childNodes[0].childNodes[0].data, 'first'); assert.strictEqual(root.childNodes[1].childNodes[0].data, 'foo'); assert.strictEqual(root.childNodes[2].childNodes[0].data, 'foo'); assert.strictEqual(root.childNodes[3].childNodes[0].data, 'second'); assert.strictEqual(root.childNodes[4].childNodes[0].data, 'foo'); - widget.__setProperties__({ widget: 'bar' }); + setProperties({ widget: 'bar' }); assert.strictEqual(root.childNodes[0].childNodes[0].data, 'first'); assert.strictEqual(root.childNodes[1].childNodes[0].data, 'bar'); assert.strictEqual(root.childNodes[2].childNodes[0].data, 'bar'); assert.strictEqual(root.childNodes[3].childNodes[0].data, 'second'); assert.strictEqual(root.childNodes[4].childNodes[0].data, 'bar'); - widget.__setProperties__({ widget: 'other' }); + setProperties({ widget: 'other' }); assert.strictEqual(root.childNodes[0].childNodes[0].data, 'first'); assert.strictEqual(root.childNodes[1].childNodes[0].data, 'second'); - widget.__setProperties__({ widget: 'bar' }); + setProperties({ widget: 'bar' }); assert.strictEqual(root.childNodes[0].childNodes[0].data, 'first'); assert.strictEqual(root.childNodes[1].childNodes[0].data, 'bar'); assert.strictEqual(root.childNodes[2].childNodes[0].data, 'bar'); @@ -658,6 +669,7 @@ jsdomDescribe('vdom', () => { }); it('should only insert before nodes that are not orphaned', () => { + let swap: Function; class Parent extends WidgetBase { private items: DNode[] = [w(ChildOne, {}), w(ChildTwo, {})]; render() { @@ -668,6 +680,11 @@ jsdomDescribe('vdom', () => { this.items = [w(ChildThree, {})]; this.invalidate(); } + + constructor() { + super(); + swap = this.swap.bind(this); + } } let hideOne: Function; @@ -720,15 +737,48 @@ jsdomDescribe('vdom', () => { } } - const widget = new Parent(); - const projection = dom.create(widget, { sync: true }); + const r = renderer(() => w(Parent, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root: any = div.childNodes[0] as Element; hideOne!(); hideTwo!(); - widget.swap(); - const root: any = projection.domNode.childNodes[0]; + swap!(); assert.strictEqual(root.childNodes[0].childNodes[0].data, 'hello 3'); }); + it('A', () => { + class GrandParent extends WidgetBase { + render() { + return v('div', [w(Parent, {}), w(ChildOne, {}), v('div', ['insert before me'])]); + } + } + + class Parent extends WidgetBase { + render() { + return [w(ChildOne, { key: '1' }), w(ChildOne, { key: '2' })]; + } + } + + class ChildOne extends WidgetBase { + render() { + return w(ChildTwo, {}); + } + } + + class ChildTwo extends WidgetBase { + render() { + return v('div', ['Two']); + } + } + + const r = renderer(() => w(GrandParent, {})); + const div = document.createElement('div'); + r.mount({ domNode: div }); + const root: any = div.childNodes[0] as Element; + assert.lengthOf(root.childNodes, 4); + }); + it('should only insert before nodes that are not orphaned when returning from an array', () => { class VeryParent extends WidgetBase { render() { @@ -760,34 +810,79 @@ jsdomDescribe('vdom', () => { } } - let invalidateTwo: any; + let invalidateTwo: any[] = []; class ChildTwo extends WidgetBase { constructor() { super(); - invalidateTwo = this.invalidate.bind(this); + invalidateTwo.push(this.invalidate.bind(this)); } render() { return hide ? null : v('div', ['Two']); } } - const widget = new VeryParent(); - const projection = dom.create(widget); - const root = projection.domNode.childNodes[0] as any; - resolvers.resolve(); - invalidateTwo(); + const r = renderer(() => w(VeryParent, {})); + const div = document.createElement('div'); + r.mount({ domNode: div }); + const root: any = div.childNodes[0] as Element; + assert.lengthOf(root.childNodes, 4); + invalidateTwo.forEach((invalidate) => invalidate()); resolvers.resolve(); + assert.lengthOf(root.childNodes, 4); hide = true; - invalidateTwo(); + invalidateTwo.forEach((invalidate) => invalidate()); resolvers.resolve(); + assert.lengthOf(root.childNodes, 1); parentInvalidate(); resolvers.resolve(); - assert.lengthOf(root.childNodes, 4); - assert.strictEqual(root.childNodes[2].childNodes[0].data, 'New'); - assert.strictEqual(root.childNodes[3].childNodes[0].data, 'insert before me'); + assert.lengthOf(root.childNodes, 2); + assert.strictEqual(root.childNodes[0].childNodes[0].data, 'New'); + assert.strictEqual(root.childNodes[1].childNodes[0].data, 'insert before me'); + }); + + it('Should insert result from widget in correct position', () => { + class Menu extends WidgetBase { + render() { + return 'Menu'; + } + } + class View extends WidgetBase { + render() { + return 'View'; + } + } + + let switcher: any; + class App extends WidgetBase { + private _show = true; + + private switcher = () => { + this._show = !this._show; + this.invalidate(); + }; + + constructor() { + super(); + switcher = this.switcher; + } + + render() { + return v('div', [this._show ? w(Menu, {}) : null, v('div', [this._show ? w(View, {}) : null])]); + } + } + + const r = renderer(() => w(App, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + assert.strictEqual(div.outerHTML, '
Menu
View
'); + switcher(); + assert.strictEqual(div.outerHTML, '
'); + switcher(); + assert.strictEqual(div.outerHTML, '
Menu
View
'); }); it('Should not render widgets that have been detached', () => { + let switcher: any; class ChildOne extends WidgetBase { render() { return 'Child One'; @@ -809,6 +904,11 @@ jsdomDescribe('vdom', () => { class Parent extends WidgetBase { private _items: any[] = [w(ChildTwo, {})]; + + constructor() { + super(); + switcher = this.switch.bind(this); + } render() { return v('main', this._items); } @@ -819,17 +919,15 @@ jsdomDescribe('vdom', () => { } } - const widget = new Parent(); - const projection = dom.create(widget); - resolvers.resolve(); + const r = renderer(() => w(Parent, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); renderResult = v('span', ['me']); childTwoInvalidate!(); - resolvers.resolve(); - widget.switch(); + switcher(); childTwoInvalidate!(); - resolvers.resolve(); - assert.lengthOf(projection.domNode.childNodes[0]!.childNodes, 1); - assert.strictEqual((projection.domNode.childNodes[0]!.childNodes[0] as Text).data, 'Child One'); + assert.lengthOf(div.childNodes[0]!.childNodes, 1); + assert.strictEqual((div.childNodes[0]!.childNodes[0] as Text).data, 'Child One'); }); it('should allow a widget returned from render', () => { @@ -841,21 +939,17 @@ jsdomDescribe('vdom', () => { class Baz extends WidgetBase { render() { - return w(Bar, { foo: this.properties.foo }); + return w(Bar, { foo: 'foo' }); } } - const widget = new Baz(); - widget.__setProperties__({ foo: 'foo' }); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0] as Element; assert.lengthOf(root.childNodes, 1); let textNodeOne = root.childNodes[0] as Text; assert.strictEqual(textNodeOne.data, 'Hello, foo!'); - widget.__setProperties__({ foo: 'bar' }); - - textNodeOne = root.childNodes[0] as Text; - assert.strictEqual(textNodeOne.data, 'Hello, bar!'); }); it('should create nodes for an array returned from the top level via a widget', () => { @@ -871,9 +965,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Bar(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode; + const r = renderer(() => w(Bar, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div; assert.lengthOf(root.childNodes, 3); const firstTextNodeChild = root.childNodes[0].childNodes[0] as Text; const secondTextNodeChild = root.childNodes[1].childNodes[0] as Text; @@ -913,9 +1008,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Parent(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0]; + const r = renderer(() => w(Parent, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0]; assert.lengthOf(root.childNodes, 2); assert.strictEqual((root.childNodes[0].childNodes[0] as Text).data, 'one'); assert.strictEqual((root.childNodes[1].childNodes[0] as Text).data, 'one'); @@ -965,9 +1061,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Foo(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0]; + const r = renderer(() => w(Foo, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0]; assert.strictEqual((root.childNodes[0].childNodes[0] as Text).data, 'one'); assert.strictEqual((root.childNodes[1].childNodes[0] as Text).data, 'two'); showBar = true; @@ -1010,9 +1107,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Foo(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0]; + const r = renderer(() => w(Foo, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0]; assert.strictEqual((root.childNodes[0].childNodes[0] as Text).data, 'one'); assert.strictEqual((root.childNodes[1].childNodes[0] as Text).data, 'two'); showBar = true; @@ -1057,9 +1155,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Foo(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0]; + const r = renderer(() => w(Foo, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0]; assert.strictEqual((root.childNodes[0].childNodes[0] as Text).data, 'one'); assert.strictEqual((root.childNodes[1].childNodes[0] as Text).data, 'two'); showBar = true; @@ -1103,9 +1202,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new App(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode; + const r = renderer(() => w(App, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div; const h2 = root.childNodes[0].childNodes[0].childNodes[0]; const p = root.childNodes[0].childNodes[0].childNodes[1]; assert.lengthOf(h2.childNodes, 0); @@ -1119,8 +1219,13 @@ jsdomDescribe('vdom', () => { }); it('should update an array of nodes to single node', () => { + let invalidate: any; class Foo extends WidgetBase { private _array = false; + constructor() { + super(); + invalidate = this.invalidate.bind(this); + } render() { this._array = !this._array; return this._array @@ -1129,9 +1234,10 @@ jsdomDescribe('vdom', () => { } } - const widget = new Foo(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode; + const r = renderer(() => w(Foo, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div; assert.lengthOf(root.childNodes, 3); const firstTextNodeChild = root.childNodes[0].childNodes[0] as Text; const secondTextNodeChild = root.childNodes[1].childNodes[0] as Text; @@ -1139,7 +1245,7 @@ jsdomDescribe('vdom', () => { assert.strictEqual(firstTextNodeChild.data, '1'); assert.strictEqual(secondTextNodeChild.data, '2'); assert.strictEqual(thirdTextNodeChild.data, '3'); - widget.invalidate(); + invalidate(); assert.lengthOf(root.childNodes, 1); const textNodeChild = root.childNodes[0].childNodes[0] as Text; @@ -1198,8 +1304,9 @@ jsdomDescribe('vdom', () => { } } - const widget = new Foo(); - dom.create(widget); + const r = renderer(() => w(Foo, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); assert.strictEqual(parentRenderCount, 1); assert.strictEqual(barRenderCount, 1); assert.strictEqual(bazRenderCount, 1); @@ -1222,10 +1329,10 @@ jsdomDescribe('vdom', () => { } } + const r = renderer(() => w(Foo, {})); const div = document.createElement('div'); - const widget = new Foo(); - const projection = dom.append(div, widget); - const root = projection.domNode as Element; + r.mount({ domNode: div, sync: true }); + const root = div; assert.lengthOf(root.childNodes, 3); const firstTextNodeChild = root.childNodes[0].childNodes[0] as Text; const secondTextNodeChild = root.childNodes[1].childNodes[0] as Text; @@ -1248,10 +1355,10 @@ jsdomDescribe('vdom', () => { } } + const r = renderer(() => w(Bar, {})); const div = document.createElement('div'); - const widget = new Bar(); - const projection = dom.append(div, widget); - const root = projection.domNode; + r.mount({ domNode: div, sync: true }); + const root = div; assert.lengthOf(root.childNodes, 3); const firstTextNodeChild = root.childNodes[0].childNodes[0] as Text; const secondTextNodeChild = root.childNodes[1].childNodes[0] as Text; @@ -1262,10 +1369,21 @@ jsdomDescribe('vdom', () => { }); it('Do not break early for the same WNode', () => { + let selected: any; class Foo extends WidgetBase { + private _selected = 0; + constructor() { + super(); + selected = this.selected; + } + + selected = () => { + this._selected = 1; + this.invalidate(); + }; render() { const children = this.children.map((child: any, index: number) => { - child.properties.selected = this.properties.selected === index; + child.properties.selected = this._selected === index; return child; }); @@ -1279,18 +1397,16 @@ jsdomDescribe('vdom', () => { } } - const widget = new Foo(); - widget.__setChildren__([w(Bar, { key: '1' }), w(Bar, { key: '2' })]); - widget.__setProperties__({ selected: 0 }); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0]; + const r = renderer(() => w(Foo, {}, [w(Bar, { key: '1' }), w(Bar, { key: '2' })])); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0]; assert.lengthOf(root.childNodes, 2); let firstTextNode = root.childNodes[0].childNodes[0] as Text; let secondTextNode = root.childNodes[1].childNodes[0] as Text; assert.strictEqual(firstTextNode.data, 'selected'); assert.strictEqual(secondTextNode.data, 'not selected'); - widget.__setProperties__({ selected: 1 }); - + selected(); firstTextNode = root.childNodes[0].childNodes[0] as Text; secondTextNode = root.childNodes[1].childNodes[0] as Text; assert.strictEqual(firstTextNode.data, 'not selected'); @@ -1322,13 +1438,19 @@ jsdomDescribe('vdom', () => { } } + let foo: any; class Baz extends WidgetBase { private _foo = true; - set foo(value: boolean) { + constructor() { + super(); + foo = this.foo; + } + + foo = (value: boolean) => { this._foo = value; this.invalidate(); - } + }; render() { return v('div', [ @@ -1338,30 +1460,51 @@ jsdomDescribe('vdom', () => { } } - const widget = new Baz(); - dom.create(widget); + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div }); resolvers.resolve(); assert.isTrue(fooCreated); - widget.foo = false; - widget.invalidate(); + foo(false); resolvers.resolve(); assert.strictEqual(barCreatedCount, 3); }); it('calls onAttach when widget is rendered', () => { let onAttachCallCount = 0; + let invalidate: any; + class Bar extends WidgetBase { + onAttach() { + onAttachCallCount++; + } + + render() { + return [v('div')]; + } + } + class Foo extends WidgetBase { + constructor() { + super(); + invalidate = this.invalidate.bind(this); + } + onAttach() { onAttachCallCount++; } + + render() { + return w(Bar, {}); + } } - const widget = new Foo(); - dom.create(widget); + const r = renderer(() => w(Foo, {})); + const div = document.createElement('div'); + r.mount({ domNode: div }); resolvers.resolve(); - assert.strictEqual(onAttachCallCount, 1); - widget.invalidate(); + assert.strictEqual(onAttachCallCount, 2); + invalidate(); resolvers.resolve(); - assert.strictEqual(onAttachCallCount, 1); + assert.strictEqual(onAttachCallCount, 2); }); it('calls onDetach when widget is removed', () => { @@ -1410,9 +1553,15 @@ jsdomDescribe('vdom', () => { class FooBar extends WidgetBase {} + let invalidate: any; class Baz extends WidgetBase { private _foo = false; + constructor() { + super(); + invalidate = this.invalidate.bind(this); + } + onAttach() { bazAttachCount++; } @@ -1431,8 +1580,9 @@ jsdomDescribe('vdom', () => { ]); } } - const widget = new Baz(); - dom.create(widget); + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div }); resolvers.resolve(); assert.strictEqual(bazAttachCount, 1); assert.strictEqual(bazDetachCount, 0); @@ -1442,7 +1592,8 @@ jsdomDescribe('vdom', () => { assert.strictEqual(barDetachCount, 0); assert.strictEqual(quxAttachCount, 4); assert.strictEqual(quxDetachCount, 0); - widget.invalidate(); + invalidate(); + resolvers.resolve(); resolvers.resolve(); assert.strictEqual(bazAttachCount, 1); assert.strictEqual(bazDetachCount, 0); @@ -1452,7 +1603,8 @@ jsdomDescribe('vdom', () => { assert.strictEqual(barDetachCount, 0); assert.strictEqual(quxAttachCount, 4); assert.strictEqual(quxDetachCount, 4); - widget.invalidate(); + invalidate(); + resolvers.resolve(); resolvers.resolve(); assert.strictEqual(bazAttachCount, 1); assert.strictEqual(bazDetachCount, 0); @@ -1462,7 +1614,8 @@ jsdomDescribe('vdom', () => { assert.strictEqual(barDetachCount, 1); assert.strictEqual(quxAttachCount, 8); assert.strictEqual(quxDetachCount, 4); - widget.invalidate(); + invalidate(); + resolvers.resolve(); resolvers.resolve(); assert.strictEqual(bazAttachCount, 1); assert.strictEqual(bazDetachCount, 0); @@ -1506,8 +1659,14 @@ jsdomDescribe('vdom', () => { } } + let toggleShow: any; class Bar extends WidgetBase { private _show = true; + constructor() { + super(); + toggleShow = this.toggleShow; + } + toggleShow = () => { this._show = !this._show; this.invalidate(); @@ -1517,13 +1676,14 @@ jsdomDescribe('vdom', () => { } } - const widget = new Bar(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Bar, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0] as Element; assert.lengthOf(root.childNodes, 0); showFooNodes(); assert.lengthOf(root.childNodes, 2); - widget.toggleShow(); + toggleShow(); assert.lengthOf(root.childNodes, 1); }); @@ -1540,19 +1700,26 @@ jsdomDescribe('vdom', () => { } } + let invalidate: any; class Baz extends WidgetBase { private _show = false; + constructor() { + super(); + invalidate = this.invalidate.bind(this); + } + render() { this._show = !this._show; return this._show ? w(Bar, {}) : null; } } - const widget = new Baz(); - dom.create(widget); + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div }); resolvers.resolve(); - widget.invalidate(); + invalidate(); assert.doesNotThrow(() => { resolvers.resolve(); }); @@ -1571,64 +1738,35 @@ jsdomDescribe('vdom', () => { } } + let show: any; class Baz extends WidgetBase { private _show = true; - set show(value: boolean) { + constructor() { + super(); + show = this.setShow; + } + + setShow = (value: boolean) => { this._show = value; this.invalidate(); - } + }; render() { return v('div', [this._show ? w(Bar, {}) : null]); } } - const widget = new Baz(); - const projection = dom.create(widget, { sync: true }); - const root = projection.domNode.childNodes[0] as Element; + const r = renderer(() => w(Baz, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + const root = div.childNodes[0] as Element; const fooDiv = root.childNodes[0] as HTMLDivElement; assert.strictEqual(fooDiv.getAttribute('id'), 'foo'); - widget.show = false; - + show(false); assert.isNull(fooDiv.parentNode); }); - it('should warn in the console for siblings for the same widgets with no key when added or removed', () => { - class Foo extends WidgetBase { - render() { - return v('div', [this.properties.text]); - } - } - - class Baz extends WidgetBase { - show = false; - - render() { - return v('div', [ - w(Foo, { text: '1' }), - this.show ? w(Foo, { text: '2' }) : null, - w(Foo, { text: '3' }), - v('div', [w(Foo, { text: '4' })]) - ]); - } - } - - const widgetName = (Foo as any).name || 'unknown'; - const parentName = (Baz as any).name || 'unknown'; - - const errorMsg = `A widget (${parentName}) has had a child addded or removed, but they were not able to uniquely identified. It is recommended to provide a unique 'key' property when using the same widget or element (${widgetName}) multiple times as siblings`; - - const widget = new Baz(); - dom.create(widget); - assert.isTrue(consoleStub.notCalled); - widget.show = true; - widget.invalidate(); - resolvers.resolve(); - assert.isTrue(consoleStub.calledOnce); - assert.isTrue(consoleStub.calledWith(errorMsg)); - }); - it('Should support widgets using deferred properties', () => { let deferredPropertyCallCount = 0; @@ -1657,7 +1795,12 @@ jsdomDescribe('vdom', () => { } } + let invalidate: any; class Foo extends WidgetBase { + constructor() { + super(); + invalidate = this.invalidate.bind(this); + } render() { return v( 'div', @@ -1683,10 +1826,12 @@ jsdomDescribe('vdom', () => { } } - const widget = new Foo(); - const projection = dom.create(widget, { sync: true }); + const r = renderer(() => w(Foo, {})); + const div = document.createElement('div'); + r.mount({ domNode: div, sync: true }); + resolvers.resolve(); assert.strictEqual(deferredPropertyCallCount, 8); - const root: any = projection.domNode.childNodes[0]; + const root = div.childNodes[0] as Element; assert.lengthOf(root.childNodes, 2); const fooContainer = root.childNodes[0]; assert.lengthOf(fooContainer.childNodes, 1); @@ -1698,7 +1843,8 @@ jsdomDescribe('vdom', () => { assert.lengthOf(barContainer.childNodes, 1); const barLabel = barContainer.childNodes[0] as Text; assert.strictEqual(barLabel.data, 'bar-container'); - widget.invalidate(); + invalidate(); + resolvers.resolve(); assert.strictEqual(deferredPropertyCallCount, 12); }); @@ -1706,11 +1852,11 @@ jsdomDescribe('vdom', () => { it('Supports merging DNodes onto existing HTML', () => { const iframe = document.createElement('iframe'); document.body.appendChild(iframe); - iframe.contentDocument!.write( + iframe.contentDocument.write( `
` ); - iframe.contentDocument!.close(); - const root = iframe.contentDocument!.body.firstChild as HTMLElement; + iframe.contentDocument.close(); + const root = iframe.contentDocument.body.firstChild as HTMLElement; const childElementCount = root.childElementCount; const select = root.childNodes[1] as HTMLSelectElement; const button = root.childNodes[2] as HTMLButtonElement; @@ -1764,8 +1910,8 @@ jsdomDescribe('vdom', () => { return w(Foo, {}); } } - const widget = new Bar(); - dom.merge(root, widget, { sync: true }); + const r = renderer(() => w(Bar, {})); + r.mount({ domNode: iframe.contentDocument.body, sync: true }); assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); assert.strictEqual( root.childElementCount, @@ -1793,11 +1939,11 @@ jsdomDescribe('vdom', () => { it('Supports merging DNodes with widgets onto existing HTML', () => { const iframe = document.createElement('iframe'); document.body.appendChild(iframe); - iframe.contentDocument!.write( + iframe.contentDocument.write( `
label
last node
` ); - iframe.contentDocument!.close(); - const root = iframe.contentDocument!.body.firstChild as HTMLElement; + iframe.contentDocument.close(); + const root = iframe.contentDocument.body.firstChild as HTMLElement; const childElementCount = root.childElementCount; const label = root.childNodes[0] as HTMLLabelElement; const select = root.childNodes[1] as HTMLSelectElement; @@ -1855,8 +2001,8 @@ jsdomDescribe('vdom', () => { return w(Foo, {}); } } - const widget = new Bar(); - dom.merge(root, widget, { sync: true }); + const r = renderer(() => w(Bar, {})); + r.mount({ domNode: iframe.contentDocument.body, sync: true }); assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); assert.strictEqual( root.childElementCount, @@ -1887,7 +2033,7 @@ jsdomDescribe('vdom', () => { it('Skips unknown nodes when merging', () => { const iframe = document.createElement('iframe'); document.body.appendChild(iframe); - iframe.contentDocument!.write(` + iframe.contentDocument.write(`
` ); - iframe.contentDocument!.close(); - const root = iframe.contentDocument!.body.firstChild as HTMLElement; + iframe.contentDocument.close(); + const root = iframe.contentDocument.body.firstChild as HTMLElement; const childElementCount = root.childElementCount; const select = root.childNodes[1] as HTMLSelectElement; const button = root.childNodes[2] as HTMLButtonElement; @@ -3748,36 +3896,31 @@ jsdomDescribe('vdom', () => { ); } } - const widget = new Foo(); - dom.merge(root, widget); + const r = renderer(() => w(Foo, {})); + r.mount({ domNode: iframe.contentDocument.body }); assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); assert.strictEqual(root.childElementCount, childElementCount, 'should have the same number of children'); assert.strictEqual(select, root.childNodes[1], 'should have been reused'); assert.strictEqual(button, root.childNodes[2], 'should have been reused'); assert.isFalse(select.disabled, 'select should be enabled'); assert.isFalse(button.disabled, 'button should be enabled'); - assert.strictEqual(select.value, 'foo', 'foo should be selected'); assert.strictEqual(select.children.length, 3, 'should have 3 children'); - assert.isFalse(onclickListener.called, 'onclickListener should not have been called'); - const clickEvent = document.createEvent('CustomEvent'); clickEvent.initEvent('click', true, true); button.dispatchEvent(clickEvent); assert.isTrue(onclickListener.called, 'onclickListener should have been called'); - document.body.removeChild(iframe); }); - it('Supports merging DNodes with widgets onto existing HTML', () => { const iframe = document.createElement('iframe'); document.body.appendChild(iframe); - iframe.contentDocument!.write( + iframe.contentDocument.write( `
label
last node
` ); - iframe.contentDocument!.close(); - const root = iframe.contentDocument!.body.firstChild as HTMLElement; + iframe.contentDocument.close(); + const root = iframe.contentDocument.body.firstChild as HTMLElement; const childElementCount = root.childElementCount; const label = root.childNodes[0] as HTMLLabelElement; const select = root.childNodes[1] as HTMLSelectElement; @@ -3786,7 +3929,6 @@ jsdomDescribe('vdom', () => { const div = root.childNodes[4] as HTMLElement; assert.strictEqual(select.value, 'bar', 'bar should be selected'); const onclickListener = spy(); - class Button extends WidgetBase { render() { return [ @@ -3830,8 +3972,8 @@ jsdomDescribe('vdom', () => { ); } } - const widget = new Foo(); - dom.merge(root, widget); + const r = renderer(() => w(Foo, {})); + r.mount({ domNode: iframe.contentDocument.body }); assert.strictEqual(root.className, 'foo bar', 'should have added bar class'); assert.strictEqual(root.childElementCount, childElementCount, 'should have the same number of children'); assert.strictEqual(label, root.childNodes[0], 'should have been reused'); @@ -3841,24 +3983,19 @@ jsdomDescribe('vdom', () => { assert.strictEqual(div, root.childNodes[4], 'should have been reused'); assert.isFalse(select.disabled, 'select should be enabled'); assert.isFalse(button.disabled, 'button should be enabled'); - assert.strictEqual(select.value, 'foo', 'foo should be selected'); assert.strictEqual(select.children.length, 3, 'should have 3 children'); - assert.isFalse(onclickListener.called, 'onclickListener should not have been called'); - const clickEvent = document.createEvent('CustomEvent'); clickEvent.initEvent('click', true, true); button.dispatchEvent(clickEvent); assert.isTrue(onclickListener.called, 'onclickListener should have been called'); - document.body.removeChild(iframe); }); - it('Skips unknown nodes when merging', () => { const iframe = document.createElement('iframe'); document.body.appendChild(iframe); - iframe.contentDocument!.write(` + iframe.contentDocument.write(`