-
Notifications
You must be signed in to change notification settings - Fork 39
Add evented Node handler to meta #666
Changes from 25 commits
838903a
ed9bc3e
6f75f56
f61028d
8aa52fe
2bb5260
35ca787
66b4d05
2d4e39f
47b4f75
0162789
43ab419
ea370b8
1142321
076085d
4d31f7d
5decea2
0aa14cc
e88241b
fda32dd
b9787b5
3e50f14
6207249
1e7cbd6
5fdcf8b
b067ace
2e80508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
|
||
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; |
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 { | ||
|
@@ -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'; | ||
|
||
/** | ||
|
@@ -177,16 +179,19 @@ 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 | ||
*/ | ||
|
@@ -200,14 +205,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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Need to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
||
|
@@ -219,9 +220,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); | ||
|
@@ -230,52 +230,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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. needs the |
||
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._setNode(element, properties); | ||
this._nodeHandler.add(element, properties); | ||
this.onElementUpdated(element, String(properties.key)); | ||
} | ||
|
||
private afterRootUpdateCallback( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. needs the |
||
element: HTMLElement, | ||
projectionOptions: ProjectionOptions, | ||
vnodeSelector: string, | ||
properties: VNodeProperties | ||
): void { | ||
this._addElementToNodeHandler(element, projectionOptions, properties); | ||
this.onElementUpdated(element, String(properties.key)); | ||
} | ||
|
||
private _addElementToNodeHandler(element: HTMLElement, projectionOptions: ProjectionOptions, properties: VNodeProperties) { | ||
this._currentRootNode += 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not |
||
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. | ||
|
@@ -298,17 +317,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; | ||
} | ||
|
@@ -442,6 +450,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; | ||
|
@@ -452,12 +461,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; | ||
} | ||
|
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, () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe should own this |
||
handle.destroy(); | ||
this._requestedNodeKeys.delete(key); | ||
this.invalidate(); | ||
} | ||
}); | ||
|
||
this.own(handle); | ||
this._requestedNodeKeys.add(key); | ||
} | ||
|
||
return node; | ||
} | ||
|
||
protected invalidate(): void { | ||
this._invalidate(); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
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.