Skip to content

Commit

Permalink
feat(templating): add the low-level render location abstraction (#57)
Browse files Browse the repository at this point in the history
* feat(templating): add the low-level render location abstraction

* refactor(runtime): use "render location" terminology throughout

Some minor refactoring in the render location tests as well.

* fix(dom): correct TS error
  • Loading branch information
EisenbergEffect committed Aug 11, 2018
1 parent 19854e4 commit 3c9735e
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 89 deletions.
29 changes: 16 additions & 13 deletions packages/runtime/src/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export interface INode extends INodeLike {

export const INode = DI.createInterface<INode>().noDefault();

export interface IRenderLocation extends INode { }


export const IRenderLocation = DI.createInterface<IRenderLocation>().noDefault();

/**
* Represents a DocumentFragment
*/
Expand Down Expand Up @@ -94,10 +99,6 @@ export const DOM = {
return document.createElement(name);
},

createAnchor(): INode {
return document.createComment('anchor');
},

createNodeObserver(target: INode, callback: MutationCallback, options: MutationObserverInit) {
const observer = new MutationObserver(callback);
observer.observe(target as Node, options);
Expand Down Expand Up @@ -219,20 +220,22 @@ export const DOM = {
(<any>node).auInterpolationTarget = true;
},

convertToAnchor(node: INode, proxy?: boolean): INode {
const anchor = <CommentProxy>DOM.createAnchor();
convertToRenderLocation(node: INode, proxy?: boolean): IRenderLocation {
const location = <CommentProxy>document.createComment('au-loc');

if (proxy) {
anchor.$proxyTarget = <Element>node;
// binding explicitly to the anchor instead of implicitly to ensure the correct 'this' assignment
anchor.hasAttribute = hasAttribute.bind(anchor);
anchor.getAttribute = getAttribute.bind(anchor);
anchor.setAttribute = setAttribute.bind(anchor);
location.$proxyTarget = <Element>node;
// binding explicitly to the comment instead of implicitly
// to ensure the correct 'this' assignment
location.hasAttribute = hasAttribute.bind(location);
location.getAttribute = getAttribute.bind(location);
location.setAttribute = setAttribute.bind(location);
}

// let this throw if node does not have a parent
(<Node>node.parentNode).replaceChild(anchor, <any>node);
(<Node>node.parentNode).replaceChild(location, <any>node);

return anchor;
return location;
},

registerElementResolver(container: IContainer, resolver: IResolver): void {
Expand Down
10 changes: 5 additions & 5 deletions packages/runtime/src/templating/custom-element.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Constructable, IContainer, Immutable, PLATFORM, Registration, Writable, Reporter } from '@aurelia/kernel';
import { BindingContext } from '../binding/binding-context';
import { BindingFlags } from '../binding/binding-flags';
import { DOM, INode, IView } from '../dom';
import { DOM, INode, IView, IRenderLocation } from '../dom';
import { IResourceKind, IResourceType } from '../resource';
import { IHydrateElementInstruction, ITemplateSource, TemplateDefinition } from './instructions';
import { AttachLifecycle, DetachLifecycle, IAttach, IBindSelf } from './lifecycle';
Expand Down Expand Up @@ -322,11 +322,11 @@ class ShadowDOMProjector implements IViewProjector {
}

class ContainerlessProjector implements IViewProjector {
private anchor: INode;
private location: IRenderLocation;

constructor(customElement: ICustomElement, host: INode) {
this.anchor = DOM.convertToAnchor(host, true);
(this.anchor as any).$customElement = customElement;
this.location = DOM.convertToRenderLocation(host, true);
(this.location as any).$customElement = customElement;
}

get children(): ArrayLike<INode> {
Expand All @@ -346,7 +346,7 @@ class ContainerlessProjector implements IViewProjector {
}

public project(view: IView): void {
view.insertBefore(this.anchor);
view.insertBefore(this.location);
}
}

Expand Down
29 changes: 16 additions & 13 deletions packages/runtime/src/templating/render-context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IContainer, Immutable, ImmutableArray, IResolver, IServiceLocator, PLATFORM } from '@aurelia/kernel';
import { DOM, INode } from '../dom';
import { IContainer, Immutable, ImmutableArray, IResolver, IServiceLocator, PLATFORM, IDisposable } from '@aurelia/kernel';
import { DOM, INode, IRenderLocation } from '../dom';
import { ICustomAttribute } from './custom-attribute';
import { ICustomElement } from './custom-element';
import { IHydrateElementInstruction, ITargetedInstruction, TargetedInstructionType, TemplateDefinition, TemplatePartDefinitions } from './instructions';
Expand All @@ -13,13 +13,12 @@ export interface IRenderContext extends IServiceLocator {
render(owner: IViewOwner, targets: ArrayLike<INode>, templateDefinition: TemplateDefinition, host?: INode, parts?: TemplatePartDefinitions);
hydrateElement(owner: IViewOwner, target: any, instruction: Immutable<IHydrateElementInstruction>): void;
hydrateElementInstance(owner: IViewOwner, target: INode, instruction: Immutable<IHydrateElementInstruction>, component: ICustomElement): void;
beginComponentOperation(owner: IViewOwner, target: any, instruction: Immutable<ITargetedInstruction>, factory?: IVisualFactory, parts?: TemplatePartDefinitions, anchor?: INode, anchorIsContainer?: boolean): IComponentOperation;
};
beginComponentOperation(owner: IViewOwner, target: any, instruction: Immutable<ITargetedInstruction>, factory?: IVisualFactory, parts?: TemplatePartDefinitions, location?: IRenderLocation, locationIsContainer?: boolean): IComponentOperation;
}

export interface IComponentOperation {
export interface IComponentOperation extends IDisposable {
tryConnectTemplateControllerToSlot(owner: ICustomAttribute): void;
tryConnectElementToSlot(owner: ICustomElement): void;
dispose();
}

/*@internal*/
Expand All @@ -32,6 +31,7 @@ export function createRenderContext(renderingEngine: IRenderingEngine, parentRen
const instructionProvider = new InstanceProvider<ITargetedInstruction>();
const factoryProvider = new ViewFactoryProvider(renderingEngine);
const slotProvider = new RenderSlotProvider();
const renderLocationProvider = new InstanceProvider<IRenderLocation>();
const renderer = renderingEngine.createRenderer(context);

DOM.registerElementResolver(context, elementProvider);
Expand All @@ -40,6 +40,7 @@ export function createRenderContext(renderingEngine: IRenderingEngine, parentRen
context.registerResolver(IRenderSlot, slotProvider);
context.registerResolver(IViewOwner, ownerProvider);
context.registerResolver(ITargetedInstruction, instructionProvider);
context.registerResolver(IRenderLocation, renderLocationProvider);

if (dependencies) {
context.register(...dependencies);
Expand All @@ -49,7 +50,7 @@ export function createRenderContext(renderingEngine: IRenderingEngine, parentRen
renderer.render(owner, targets, templateDefinition, host, parts)
};

context.beginComponentOperation = function(owner: IViewOwner, target: any, instruction: ITargetedInstruction, factory?: IVisualFactory, parts?: TemplatePartDefinitions, anchor?: INode, anchorIsContainer?: boolean) {
context.beginComponentOperation = function(owner: IViewOwner, target: any, instruction: ITargetedInstruction, factory?: IVisualFactory, parts?: TemplatePartDefinitions, location?: IRenderLocation, locationIsContainer?: boolean) {
ownerProvider.prepare(owner);
elementProvider.prepare(target);
instructionProvider.prepare(instruction);
Expand All @@ -58,8 +59,9 @@ export function createRenderContext(renderingEngine: IRenderingEngine, parentRen
factoryProvider.prepare(factory, parts);
}

if (anchor) {
slotProvider.prepare(anchor, anchorIsContainer);
if (location) {
renderLocationProvider.prepare(location);
slotProvider.prepare(location, locationIsContainer);
}

return context;
Expand Down Expand Up @@ -87,6 +89,7 @@ export function createRenderContext(renderingEngine: IRenderingEngine, parentRen
ownerProvider.dispose();
instructionProvider.dispose();
elementProvider.dispose();
renderLocationProvider.dispose();
};

return context;
Expand Down Expand Up @@ -140,16 +143,16 @@ export class ViewFactoryProvider implements IResolver {
/*@internal*/
export class RenderSlotProvider implements IResolver {
private node: INode = null;
private anchorIsContainer = false;
private locationIsContainer = false;
private slot: IRenderSlot = null;

public prepare(element: INode, anchorIsContainer = false) {
public prepare(element: INode, locationIsContainer = false) {
this.node = element;
this.anchorIsContainer = anchorIsContainer;
this.locationIsContainer = locationIsContainer;
}

public resolve(handler: IContainer, requestor: IContainer) {
return this.slot || (this.slot = RenderSlot.create(this.node, this.anchorIsContainer));
return this.slot || (this.slot = RenderSlot.create(this.node, this.locationIsContainer));
}

public tryConnectTemplateControllerToSlot(owner) {
Expand Down
17 changes: 7 additions & 10 deletions packages/runtime/src/templating/render-slot.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { DI } from '@aurelia/kernel';
import { INode } from '../dom';
import { INode, IRenderLocation } from '../dom';
import { AttachLifecycle, DetachLifecycle, IAttach } from './lifecycle';
import { IVisual, MotionDirection } from './visual';

/*@internal*/
export function appendVisualToContainer(visual: IVisual) {
const parent = visual.parent as RenderSlotImplementation;
visual.$view.appendTo(parent.anchor);
visual.$view.appendTo(parent.location);
}

/*@internal*/
export function addVisual(visual: IVisual) {
const parent = visual.parent as RenderSlotImplementation;
visual.$view.insertBefore(parent.anchor);
visual.$view.insertBefore(parent.location);
}

/*@internal*/
Expand Down Expand Up @@ -99,8 +99,8 @@ export interface IRenderSlot extends IAttach {
}

export const RenderSlot = {
create(anchor: INode, anchorIsContainer: boolean): IRenderSlot {
return new RenderSlotImplementation(anchor, anchorIsContainer);
create(location: IRenderLocation, locationIsContainer: boolean): IRenderSlot {
return new RenderSlotImplementation(location, locationIsContainer);
}
};

Expand All @@ -113,11 +113,8 @@ export class RenderSlotImplementation implements IRenderSlot {

public children: IVisual[] = [];

constructor(public anchor: INode, anchorIsContainer: boolean) {
(anchor as any).$slot = this; // Usage: Shadow DOM Emulation
(anchor as any).$isContentProjectionSource = false; // Usage: Shadow DOM Emulation

this.addVisualCore = anchorIsContainer ? appendVisualToContainer : addVisual;
constructor(public location: IRenderLocation, locationIsContainer: boolean) {
this.addVisualCore = locationIsContainer ? appendVisualToContainer : addVisual;
this.insertVisualCore = insertVisual;
}

Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/src/templating/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class Renderer implements IRenderer {
const childInstructions = instruction.instructions;
const factory = this.renderingEngine.getVisualFactory(this.context, instruction.src);
const context = this.context;
const operation = context.beginComponentOperation(owner, target, instruction, factory, parts, DOM.convertToAnchor(target), false);
const operation = context.beginComponentOperation(owner, target, instruction, factory, parts, DOM.convertToRenderLocation(target), false);

const component = context.get<ICustomAttribute>(CustomAttributeResource.key(instruction.res));
component.$hydrate(this.renderingEngine);
Expand Down
75 changes: 28 additions & 47 deletions packages/runtime/test/unit/dom.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,6 @@ describe('DOM', () => {
}
});

describe('createAnchor', () => {
it('should call document.createComment(\'anchor\')', () => {
const spyCreateComment = document.createComment = spy();
DOM.createAnchor();
expect(spyCreateComment).to.have.been.calledWith('anchor');
});

it('should create an anchor comment', () => {
const el = DOM.createAnchor();
expect(el['textContent']).to.equal('anchor');
expect(el['nodeName']).to.equal('#comment');
});
});

describe('createChildObserver (no slot emulation)', () => {
it('should return a MutationObserver', () => {
const cb = spy();
Expand Down Expand Up @@ -605,62 +591,57 @@ describe('DOM', () => {
}
});

describe('convertToAnchor', () => {
it('should replace the provided node with an anchor node', () => {
describe('convertToRenderLocation', () => {
function createTestNodes() {
const node = document.createElement('div');
const childNode = document.createElement('div');
node.appendChild(childNode);
const anchor = DOM.convertToAnchor(childNode);
expect(anchor instanceof Comment).to.be.true;
expect(childNode === anchor).to.be.false;
return {node, childNode};
}

it('should replace the provided node with a comment node', () => {
const {node, childNode} = createTestNodes();
const location = DOM.convertToRenderLocation(childNode);
expect(location instanceof Comment).to.be.true;
expect(childNode === location).to.be.false;
expect(node.childNodes.length).to.equal(1);
expect(node.firstChild === anchor).to.be.true;
expect(node.firstChild === location).to.be.true;
});

it('should proxy the provided node via the anchor if proxy=true', () => {
const node = document.createElement('div');
const childNode = document.createElement('div');
node.appendChild(childNode);
const anchor: any = DOM.convertToAnchor(childNode, true);
expect(anchor.$proxyTarget === childNode).to.be.true;
it('should proxy the provided node via the comment if proxy=true', () => {
const {childNode} = createTestNodes();
const location: any = DOM.convertToRenderLocation(childNode, true);
expect(location.$proxyTarget === childNode).to.be.true;
});

it(`should pass "hasAttribute" through to the node if proxy=true`, () => {
const node = document.createElement('div');
const childNode = document.createElement('div');
const {childNode} = createTestNodes();
childNode.setAttribute('foo', 'bar');
node.appendChild(childNode);
const anchor: any = DOM.convertToAnchor(childNode, true);
const actual = anchor.hasAttribute('foo');
const location: any = DOM.convertToRenderLocation(childNode, true);
const actual = location.hasAttribute('foo');
expect(actual).to.be.true;
});

it(`should pass "getAttribute" through to the node if proxy=true`, () => {
const node = document.createElement('div');
const childNode = document.createElement('div');
const {childNode} = createTestNodes();
childNode.setAttribute('foo', 'bar');
node.appendChild(childNode);
const anchor: any = DOM.convertToAnchor(childNode, true);
const actual = anchor.getAttribute('foo');
const location: any = DOM.convertToRenderLocation(childNode, true);
const actual = location.getAttribute('foo');
expect(actual).to.equal('bar');
});

it(`should pass "setAttribute" through to the node if proxy=true`, () => {
const node = document.createElement('div');
const childNode = document.createElement('div');
node.appendChild(childNode);
const anchor: any = DOM.convertToAnchor(childNode, true);
anchor.setAttribute('foo', 'bar');
const {childNode} = createTestNodes();
const location: any = DOM.convertToRenderLocation(childNode, true);
location.setAttribute('foo', 'bar');
const actual = childNode.getAttribute('foo');
expect(actual).to.equal('bar');
});

it('should NOT proxy the provided node via the anchor if proxy is not true', () => {
const node = document.createElement('div');
const childNode = document.createElement('div');
node.appendChild(childNode);
const anchor: any = DOM.convertToAnchor(childNode);
expect(anchor.$proxyTarget).to.be.undefined;
it('should NOT proxy the provided node via the comment if proxy is not true', () => {
const {childNode} = createTestNodes();
const location: any = DOM.convertToRenderLocation(childNode);
expect(location.$proxyTarget).to.be.undefined;
});
});

Expand Down

0 comments on commit 3c9735e

Please sign in to comment.