Skip to content

Commit

Permalink
Remove Shadow DOM Emulation (#56)
Browse files Browse the repository at this point in the history
* chore(runtime): initial deletion of shadow emulation code

* feat(custom-elements): define basic abstraction for element projection

Still a few TODOs left for the basic implementation.

* doc(custom-element): add todo notes for DefaultSlotProjector

The current set of projectors will handle every custom element scenario. However, a set of common scenarios around default slots could be further optimized. This commit adds notes outlining how a DefaultSlotProjector would work and under what conditions it could be used.

* fix(templating): address trivial errors when removing emulation

* fix(templating): remove shadow dom from compose element

* fix(all): last few corrections from the merge

* test(dom): fix unit tests

* feat(runtime): enable getting the custom element behavior...

...for a particular node

* fix(runtime): various fixes related to compose
  • Loading branch information
EisenbergEffect committed Aug 7, 2018
1 parent 70b6696 commit 0a17b16
Show file tree
Hide file tree
Showing 17 changed files with 481 additions and 1,047 deletions.
10 changes: 9 additions & 1 deletion packages/debug/src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ interface IMessageInfo {
export const Reporter: typeof RuntimeReporter = Object.assign(RuntimeReporter, {
write(code: number, ...params: any[]): void {
let info = getMessageInfoForCode(code);

switch (info.type) {
case MessageType.debug:
console.debug(info.message, ...params);
Expand Down Expand Up @@ -133,5 +133,13 @@ const codeLookup: Record<string, IMessageInfo> = {
20: {
type: MessageType.error,
message: 'No template compiler found with the specified name. JIT support or a custom compiler is required.'
},
21: {
type: MessageType.error,
message: 'You cannot combine the containerless custom element option with Shadow DOM.'
},
22: {
type: MessageType.error,
message: 'A containerless custom element cannot be the root component of an application.'
}
};
14 changes: 1 addition & 13 deletions packages/jit/src/templating/template-compiler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { inject, Writable } from '@aurelia/kernel';
import { BindingType, CustomAttributeResource, DOM, IExpression, IExpressionParser, Interpolation, IResourceDescriptions, ITargetedInstruction, ITemplateCompiler, TemplateDefinition, TargetedInstructionType, TargetedInstruction, DelegationStrategy, ITextBindingInstruction, IPropertyBindingInstruction, IListenerBindingInstruction, ICallBindingInstruction, IRefBindingInstruction, IStylePropertyBindingInstruction, ISetPropertyInstruction, ISetAttributeInstruction, IHydrateSlotInstruction, IHydrateElementInstruction, IHydrateAttributeInstruction, IHydrateTemplateController, BindingMode, ITemplateSource, INode } from '@aurelia/runtime';
import { BindingType, CustomAttributeResource, DOM, IExpression, IExpressionParser, Interpolation, IResourceDescriptions, ITargetedInstruction, ITemplateCompiler, TemplateDefinition, TargetedInstructionType, TargetedInstruction, DelegationStrategy, ITextBindingInstruction, IPropertyBindingInstruction, IListenerBindingInstruction, ICallBindingInstruction, IRefBindingInstruction, IStylePropertyBindingInstruction, ISetPropertyInstruction, ISetAttributeInstruction, IHydrateElementInstruction, IHydrateAttributeInstruction, IHydrateTemplateController, BindingMode, ITemplateSource, INode } from '@aurelia/runtime';
import { Char } from '../binding/expression-parser';
import { BindingCommandResource } from './binding-command';

Expand Down Expand Up @@ -359,17 +359,6 @@ export class SetAttributeInstruction implements ITargetedInstruction {
this.dest = dest;
}
}
export class HydrateSlotInstruction implements IHydrateSlotInstruction {
public type: TargetedInstructionType.hydrateSlot;
public name?: string;
public dest?: string;
public fallback?: ITemplateSource;
constructor(name?: string, dest?: string, fallback?: ITemplateSource) {
this.name = name;
this.dest = dest;
this.fallback = fallback;
}
}
export class HydrateElementInstruction implements IHydrateElementInstruction {
public type: TargetedInstructionType.hydrateElement;
public res: any;
Expand Down Expand Up @@ -435,7 +424,6 @@ RefBindingInstruction.prototype.type = TargetedInstructionType.refBinding;
StylePropertyBindingInstruction.prototype.type = TargetedInstructionType.stylePropertyBinding;
SetPropertyInstruction.prototype.type = TargetedInstructionType.setProperty;
SetAttributeInstruction.prototype.type = TargetedInstructionType.setAttribute;
HydrateSlotInstruction.prototype.type = TargetedInstructionType.hydrateSlot;
HydrateElementInstruction.prototype.type = TargetedInstructionType.hydrateElement;
HydrateAttributeInstruction.prototype.type = TargetedInstructionType.hydrateAttribute;
HydrateTemplateController.prototype.type = TargetedInstructionType.hydrateTemplateController;
Expand Down
17 changes: 11 additions & 6 deletions packages/runtime/src/binding/select-value-observer.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { ICallable } from '@aurelia/kernel';
import { DOM, IChildObserver } from '../dom';
import { DOM, INodeObserver } from '../dom';
import { ITaskQueue } from '../task-queue';
import { IEventSubscriber } from './event-manager';
import { IObserverLocator } from './observer-locator';
import { SubscriberCollection } from './subscriber-collection';

const selectArrayContext = 'SelectValueObserver:array';
const childObserverOptions = {
childList: true,
subtree: true,
characterData: true
};

export class SelectValueObserver extends SubscriberCollection {
private value: any;
private oldValue: any;
private arrayObserver: any;
private initialSync = false;
private childObserver: IChildObserver;
private nodeObserver: INodeObserver;

constructor(
private node: HTMLSelectElement,
Expand Down Expand Up @@ -171,15 +176,15 @@ export class SelectValueObserver extends SubscriberCollection {
}

public bind() {
this.childObserver = DOM.createChildObserver(this.node, () => {
this.nodeObserver = DOM.createNodeObserver(this.node, () => {
this.synchronizeOptions();
this.synchronizeValue();
}, { childList: true, subtree: true, characterData: true });
}, childObserverOptions);
}

public unbind() {
this.childObserver.disconnect();
this.childObserver = null;
this.nodeObserver.disconnect();
this.nodeObserver = null;

if (this.arrayObserver) {
this.arrayObserver.unsubscribe(selectArrayContext, this);
Expand Down
87 changes: 26 additions & 61 deletions packages/runtime/src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { DI, IContainer, IResolver } from '@aurelia/kernel';
import { ICustomElement } from './templating/custom-element';

export interface INodeLike {
readonly firstChild: INode | null;
Expand All @@ -13,27 +12,22 @@ export interface INode extends INodeLike {
readonly previousSibling: INode | null;
}

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

/**
* Represents a DocumentFragment
*/
export interface IView extends INodeLike {
/**
* The child nodes of this view
* The child nodes of this view.
*/
childNodes: ReadonlyArray<INode>;

/**
* Find all au-targets underneath this view
* Find all instruction targets in this view.
*/
findTargets(): ArrayLike<INode> | ReadonlyArray<INode>;

/**
* Append child to this view
*/
appendChild(child: INode): void;

/**
* Insert this view as a sibling before refNode
*/
Expand All @@ -43,11 +37,14 @@ export interface IView extends INodeLike {
* Append this view as a child to parent
*/
appendTo(parent: INode): void;

/**
* Remove this view from its parent.
*/
remove(): void;
}

export interface IChildObserver {
childNodes: ArrayLike<INode>;
export interface INodeObserver {
disconnect(): void;
}

Expand All @@ -56,31 +53,19 @@ export function createView(fragment: DocumentFragment): IView {
return new TemplateView(<DocumentFragment>fragment.cloneNode(true));
}

// don't create a new options object for each call to createChildObserver
const childObserverOptions = { childList: true };

// pre-declare certain functions whose behavior depends on a once-checked global condition for better performance
function returnTrue(): true {
return true;
}

function returnFalse(): false {
return false;
}
function createElementViewHostWithShadowDOM(node: Element, options?: ShadowRootInit): INode {
if (options) {
return node.attachShadow(options);
}

(<any>node).$usingSlotEmulation = true;
return node;
}
function createElementViewHostWithNoShadowDOM(node: Element): INode {
(<any>node).$usingSlotEmulation = true;
return node;
}
function removeNormal(node: Element): void {
node.remove();
}

function removePolyfilled(node: Element): void {
// not sure if we still actually need this, this used to be an IE9/10 thing
node.parentNode.removeChild(node);
Expand Down Expand Up @@ -108,51 +93,35 @@ export const DOM = {
createElement(name: string): INode {
return document.createElement(name);
},

createAnchor(): INode {
return document.createComment('anchor');
},
/*@internal*/createTemplate(): INode {
return document.createElement('template');
},

createChildObserver(parent: INode, onChildrenChanged: MutationCallback, options?: MutationObserverInit): IChildObserver {
if ((<any>parent).$usingSlotEmulation) {
return (<any>parent).$component.$contentView.attachChildObserver(onChildrenChanged);
} else {
const observer = new MutationObserver(onChildrenChanged);
(<any>observer).childNodes = parent.childNodes;
observer.observe(<Node>parent, options || childObserverOptions);
return <any>observer;
}
createNodeObserver(target: INode, callback: MutationCallback, options: MutationObserverInit) {
const observer = new MutationObserver(callback);
observer.observe(target as Node, options);
return observer;
},

platformSupportsShadowDOM(): boolean {
if (HTMLElement.prototype.attachShadow === undefined) {
return (DOM.platformSupportsShadowDOM = returnFalse)();
} else {
return (DOM.platformSupportsShadowDOM = returnTrue)();
}
attachShadow(host: INode, options: ShadowRootInit): INode {
return (host as Element).attachShadow(options);
},

createElementViewHost(node: INode, options?: ShadowRootInit): INode {
// only check shadowDOM availability once and then set a short-circuited function directly to save a few cycles
if (DOM.platformSupportsShadowDOM()) {
return (DOM.createElementViewHost = createElementViewHostWithShadowDOM)(<Element>node, options);
} else {
return (DOM.createElementViewHost = createElementViewHostWithNoShadowDOM)(<Element>node);
}
/*@internal*/
createTemplate(): INode {
return document.createElement('template');
},

cloneNode(node: INode, deep?: boolean): INode {
return (<Node>node).cloneNode(deep !== false); // use true unless the caller explicitly passes in false
},

getCustomElementForNode(node: INode): ICustomElement | null {
return (<any>node).$component || null; //Set during component $hydrate
},

isUsingSlotEmulation(node: INode): boolean {
return (<any>node).$usingSlotEmulation === true;
migrateChildNodes(currentParent: INode, newParent: INode): void {
const append = DOM.appendChild;
while (currentParent.firstChild) {
append(newParent, currentParent.firstChild);
}
},

isNodeInstance(potentialNode: any): potentialNode is INode {
Expand Down Expand Up @@ -231,7 +200,7 @@ export const DOM = {
if ((<any>node).auInterpolationTarget === true) {
return false;
}
const text = (<any>node).textContent;
const text = (node as Node).textContent;
const len = text.length;
let i = 0;
// for perf benchmark of this compared to the regex method: http://jsben.ch/p70q2 (also a general case against using regex)
Expand Down Expand Up @@ -323,10 +292,6 @@ export class TemplateView implements IView {
this.childNodes = childNodesArr;
}

appendChild(child: Node) {
this.fragment.appendChild(child);
}

findTargets(): ArrayLike<Node> {
return this.fragment.querySelectorAll('.au');
}
Expand Down
8 changes: 4 additions & 4 deletions packages/runtime/src/templating/animator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@ export interface IAnimator {

export const IAnimator = DI.createInterface<IAnimator>()
.withDefault(x => x.singleton(class {
enter(node: INode): Promise<boolean> {
public enter(node: INode): Promise<boolean> {
return Promise.resolve(false);
}

leave(node: INode): Promise<boolean> {
public leave(node: INode): Promise<boolean> {
return Promise.resolve(false);
}

removeClass(node: INode, className: string): Promise<boolean> {
public removeClass(node: INode, className: string): Promise<boolean> {
(<Element>node).classList.remove(className);
return Promise.resolve(false);
}

addClass(node: INode, className: string): Promise<boolean> {
public addClass(node: INode, className: string): Promise<boolean> {
(<Element>node).classList.add(className);
return Promise.resolve(false);
}
Expand Down
Loading

0 comments on commit 0a17b16

Please sign in to comment.