Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Add evented Node handler to meta #666

Merged
merged 27 commits into from
Sep 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/NodeHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Evented } from '@dojo/core/Evented';
import { VNodeProperties } from '@dojo/interfaces/vdom';
import Map from '@dojo/shim/Map';
import { NodeHandlerInterface } from './interfaces';

/**
* Enum to identify the type of event.
* Listening to 'Projector' will notify when projector is created or updated
* Listening to 'Widget' will notifiy when widget root is created or updated
*/
export enum NodeEventType {
Projector = 'Projector',
Widget = 'Widget'
}

export class NodeHandler extends Evented implements NodeHandlerInterface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two things I've been struggling with using meta that this could perhaps help with. The first is doing a reverse lookup and the second is knowing what the currently rendered keys are. I need the reverse lookup for intersection meta where I have a single observer that receives multiple events. I don't have a good use case for knowing all the keys, but I could see it being potentially helpful/related to whatever we'd have to do for a reverse lookup.


private _nodeMap = new Map<string, HTMLElement>();

public get(key: string): HTMLElement | undefined {
return this._nodeMap.get(key);
}

public has(key: string): boolean {
return this._nodeMap.has(key);
}

public add(element: HTMLElement, properties: VNodeProperties): void {
const key = String(properties.key);
this._nodeMap.set(key, element);
this.emit({ type: key });
}

public addRoot(element: HTMLElement, properties: VNodeProperties): void {
if (properties && properties.key) {
this.add(element, properties);
}

this.emit({ type: NodeEventType.Widget });
}

public addProjector(): void {
this.emit({ type: NodeEventType.Projector });
}

public clear(): void {
this._nodeMap.clear();
}
}

export default NodeHandler;
128 changes: 76 additions & 52 deletions src/WidgetBase.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Evented } from '@dojo/core/Evented';
import { ProjectionOptions, VNodeProperties } from '@dojo/interfaces/vdom';
import { VNodeProperties } from '@dojo/interfaces/vdom';
import { ProjectionOptions } from './interfaces';
import Map from '@dojo/shim/Map';
import '@dojo/shim/Promise'; // Imported for side-effects
import WeakMap from '@dojo/shim/WeakMap';
import { Handle } from '@dojo/interfaces/core';
import { isWNode, v, isHNode } from './d';
import { auto, ignore } from './diff';
import {
Expand All @@ -20,10 +22,10 @@ import {
WidgetMetaConstructor,
WidgetBaseConstructor,
WidgetBaseInterface,
WidgetProperties,
WidgetMetaRequiredNodeCallback
WidgetProperties
} from './interfaces';
import RegistryHandler from './RegistryHandler';
import NodeHandler from './NodeHandler';
import { isWidgetBaseConstructor, WIDGET_BASE_TYPE, Registry } from './Registry';

/**
Expand Down Expand Up @@ -177,16 +179,20 @@ export class WidgetBase<P = WidgetProperties, C extends DNode = DNode> extends E

private _metaMap = new WeakMap<WidgetMetaConstructor<any>, WidgetMetaBase>();

private _nodeMap = new Map<string, HTMLElement>();

private _requiredNodes = new Map<string, ([ WidgetMetaBase, WidgetMetaRequiredNodeCallback ])[]>();

private _boundRenderFunc: Render;

private _boundInvalidate: () => void;

private _defaultRegistry = new Registry();

private _nodeHandler: NodeHandler;

private _projectorAttachEvent: Handle;

private _currentRootNode = 0;

private _numRootNodes = 0;

/**
* @constructor
*/
Expand All @@ -200,14 +206,10 @@ export class WidgetBase<P = WidgetProperties, C extends DNode = DNode> extends E
this._diffPropertyFunctionMap = new Map<string, string>();
this._bindFunctionPropertyMap = new WeakMap<(...args: any[]) => any, { boundFunc: (...args: any[]) => any, scope: any }>();
this._registries = new RegistryHandler();
this._nodeHandler = new NodeHandler();
Copy link
Contributor

@matt-gadd matt-gadd Sep 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should own this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to own this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

this._registries.add(this._defaultRegistry, true);
this.own(this._registries);
this.own({
destroy: () => {
this._nodeMap.clear();
this._requiredNodes.clear();
}
});
this.own(this._nodeHandler);
this._boundRenderFunc = this.render.bind(this);
this._boundInvalidate = this.invalidate.bind(this);

Expand All @@ -219,9 +221,8 @@ export class WidgetBase<P = WidgetProperties, C extends DNode = DNode> extends E
let cached = this._metaMap.get(MetaType);
if (!cached) {
cached = new MetaType({
nodes: this._nodeMap,
requiredNodes: this._requiredNodes,
invalidate: this._boundInvalidate
invalidate: this._boundInvalidate,
nodeHandler: this._nodeHandler
});
this._metaMap.set(MetaType, cached);
this.own(cached);
Expand All @@ -230,52 +231,71 @@ export class WidgetBase<P = WidgetProperties, C extends DNode = DNode> extends E
return cached as T;
}

/**
* A render decorator that verifies nodes required in
* 'meta' calls in this render,
*/
@beforeRender()
protected verifyRequiredNodes(renderFunc: () => DNode, properties: WidgetProperties, children: DNode[]): () => DNode {
return () => {
this._requiredNodes.forEach((element, key) => {
/* istanbul ignore else: only checking for errors */
if (!this._nodeMap.has(key)) {
throw new Error(`Required node ${key} not found`);
}
});
this._requiredNodes.clear();
const dNodes = renderFunc();
this._nodeMap.clear();
return dNodes;
};
}

/**
* vnode afterCreate callback that calls the onElementCreated lifecycle method.
*/
private _afterCreateCallback(
element: Element,
element: HTMLElement,
projectionOptions: ProjectionOptions,
vnodeSelector: string,
properties: VNodeProperties
): void {
this._nodeHandler.add(element, properties);
this.onElementCreated(element, String(properties.key));
}

private _afterRootCreateCallback(
element: HTMLElement,
projectionOptions: ProjectionOptions,
vnodeSelector: string,
properties: VNodeProperties
): void {
this._setNode(element, properties);
this._addElementToNodeHandler(element, projectionOptions, properties);
this.onElementCreated(element, String(properties.key));
}

/**
* vnode afterUpdate callback that calls the onElementUpdated lifecycle method.
*/
private _afterUpdateCallback(
element: Element,
element: HTMLElement,
projectionOptions: ProjectionOptions,
vnodeSelector: string,
properties: VNodeProperties
): void {
this._nodeHandler.add(element, properties);
this.onElementUpdated(element, String(properties.key));
}

private _afterRootUpdateCallback(
element: HTMLElement,
projectionOptions: ProjectionOptions,
vnodeSelector: string,
properties: VNodeProperties
): void {
this._setNode(element, properties);
this._addElementToNodeHandler(element, projectionOptions, properties);
this.onElementUpdated(element, String(properties.key));
}

private _addElementToNodeHandler(element: HTMLElement, projectionOptions: ProjectionOptions, properties: VNodeProperties) {
this._currentRootNode++;
const isLastRootNode = (this._currentRootNode === this._numRootNodes);

if (this._projectorAttachEvent === undefined) {
this._projectorAttachEvent = projectionOptions.nodeEvent.on('rendered', () => {
this._nodeHandler.addProjector();
});
this.own(this._projectorAttachEvent);
}

if (isLastRootNode) {
this._nodeHandler.addRoot(element, properties);
}
else {
this._nodeHandler.add(element, properties);
}
}

/**
* Widget lifecycle method that is called whenever a dom node is created for a vnode.
* Override this method to access the dom nodes that were inserted into the dom.
Expand All @@ -298,17 +318,6 @@ export class WidgetBase<P = WidgetProperties, C extends DNode = DNode> extends E
// Do nothing by default.
}

private _setNode(element: Element, properties: VNodeProperties): void {
const key = String(properties.key);
this._nodeMap.set(key, <HTMLElement> element);
const callbacks = this._requiredNodes.get(key);
if (callbacks) {
for (const [ meta, callback ] of callbacks) {
callback.call(meta, element);
}
}
}

public get properties(): Readonly<P> & Readonly<WidgetProperties> {
return this._properties;
}
Expand Down Expand Up @@ -442,6 +451,7 @@ export class WidgetBase<P = WidgetProperties, C extends DNode = DNode> extends E
this._decorateNodes(dNode);
const widget = this._dNodeToVNode(dNode);
this._manageDetachedChildren();
this._nodeHandler.clear();
this._cachedVNode = widget;
this._renderState = WidgetRenderState.IDLE;
return widget;
Expand All @@ -452,12 +462,26 @@ export class WidgetBase<P = WidgetProperties, C extends DNode = DNode> extends E

private _decorateNodes(node: DNode | DNode[]) {
let nodes = Array.isArray(node) ? [ ...node ] : [ node ];

this._numRootNodes = nodes.length;
this._currentRootNode = 0;
const rootNodes: DNode[] = [];

nodes.forEach(node => {
if (isHNode(node)) {
rootNodes.push(node);
node.properties = node.properties || {};
node.properties.afterCreate = this._afterRootCreateCallback;
node.properties.afterUpdate = this._afterRootUpdateCallback;
}
});

while (nodes.length) {
const node = nodes.pop();
if (isHNode(node) || isWNode(node)) {
node.properties = node.properties || {};
if (isHNode(node)) {
if (node.properties.key) {
if (rootNodes.indexOf(node) === -1 && node.properties.key) {
node.properties.afterCreate = this._afterCreateCallback;
node.properties.afterUpdate = this._afterUpdateCallback;
}
Expand Down
29 changes: 19 additions & 10 deletions src/interfaces.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { Destroyable } from '@dojo/core/Destroyable';
import { Evented } from '@dojo/core/Evented';
import { VNode, VNodeProperties, ProjectionOptions } from '@dojo/interfaces/vdom';
import { EventTargettedObject } from '@dojo/interfaces/core';
import { VNode, VNodeProperties, ProjectionOptions as MaquetteProjectionOptions } from '@dojo/interfaces/vdom';
import Map from '@dojo/shim/Map';

/**
* Extended Dojo 2 projection options
*/
export interface ProjectionOptions extends MaquetteProjectionOptions {
nodeEvent: Evented;
}

/**
* Generic constructor type
*/
Expand Down Expand Up @@ -409,20 +417,21 @@ export interface WidgetMetaConstructor<T extends WidgetMetaBase> {
new (properties: WidgetMetaProperties): T;
}

export interface NodeHandlerInterface extends Evented {
get(key: string): HTMLElement | undefined;
has(key: string): boolean;
add(element: HTMLElement, properties: VNodeProperties): void;
addRoot(element: HTMLElement, properties: VNodeProperties): void;
addProjector(element: HTMLElement, properties: VNodeProperties): void;
clear(): void;
}

/**
* Properties passed to meta Base constructors
*/
export interface WidgetMetaProperties {
nodes: Map<string, HTMLElement>;
requiredNodes: Map<string, ([ WidgetMetaBase, WidgetMetaRequiredNodeCallback ])[]>;
invalidate: () => void;
}

/**
* Callback when asking widget meta for a required node
*/
export interface WidgetMetaRequiredNodeCallback {
(node: HTMLElement): void;
nodeHandler: NodeHandlerInterface;
}

export interface Render {
Expand Down
48 changes: 23 additions & 25 deletions src/meta/Base.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,43 @@
import { Destroyable } from '@dojo/core/Destroyable';
import global from '@dojo/shim/global';
import Map from '@dojo/shim/Map';
import { WidgetMetaBase, WidgetMetaProperties, WidgetMetaRequiredNodeCallback } from '../interfaces';
import Set from '@dojo/shim/Set';
import { WidgetMetaBase, WidgetMetaProperties, NodeHandlerInterface } from '../interfaces';

export class Base extends Destroyable implements WidgetMetaBase {
private _invalidate: () => void;
private _invalidating: number;
private _requiredNodes: Map<string, ([ WidgetMetaBase, WidgetMetaRequiredNodeCallback ])[]>;
protected nodes: Map<string, HTMLElement>;
protected nodeHandler: NodeHandlerInterface;

private _requestedNodeKeys = new Set<string>();

constructor(properties: WidgetMetaProperties) {
super();

this._invalidate = properties.invalidate;
this._requiredNodes = properties.requiredNodes;

this.nodes = properties.nodes;
this.nodeHandler = properties.nodeHandler;
}

public has(key: string): boolean {
return this.nodes.has(key);
return this.nodeHandler.has(key);
}

protected invalidate(): void {
global.cancelAnimationFrame(this._invalidating);
this._invalidating = global.requestAnimationFrame(this._invalidate);
}
protected getNode(key: string): HTMLElement | undefined {
const node = this.nodeHandler.get(key);

protected requireNode(key: string, callback?: WidgetMetaRequiredNodeCallback): void {
const node = this.nodes.get(key);
if (node) {
callback && callback.call(this, node);
}
else {
const callbacks = this._requiredNodes.get(key) || [];
callback && callbacks.push([ this, callback ]);
this._requiredNodes.set(key, callbacks);
if (!callback) {
if (!node && !this._requestedNodeKeys.has(key)) {
const handle = this.nodeHandler.on(key, () => {
handle.destroy();
this._requestedNodeKeys.delete(key);
this.invalidate();
}
});

this.own(handle);
this._requestedNodeKeys.add(key);
}

return node;
}

protected invalidate(): void {
this._invalidate();
}
}

Expand Down
Loading