diff --git a/modules/@angular/core/src/view/anchor.ts b/modules/@angular/core/src/view/anchor.ts new file mode 100644 index 0000000000..757feb8ccd --- /dev/null +++ b/modules/@angular/core/src/view/anchor.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition} from './types'; + +export function anchorDef( + flags: NodeFlags, childCount: number, template?: ViewDefinition): NodeDef { + return { + type: NodeType.Anchor, + // will bet set by the view definition + index: undefined, + reverseChildIndex: undefined, + parent: undefined, + childFlags: undefined, + bindingIndex: undefined, + providerIndices: undefined, + // regular values + flags, + childCount, + bindings: [], + element: undefined, + provider: undefined, + text: undefined, + component: undefined, template + }; +} + +export function createAnchor(view: ViewData, renderHost: any, def: NodeDef): NodeData { + const parentNode = def.parent != null ? view.nodes[def.parent].renderNode : renderHost; + let renderNode: any; + if (view.renderer) { + renderNode = view.renderer.createTemplateAnchor(parentNode); + } else { + renderNode = document.createComment(''); + if (parentNode) { + parentNode.appendChild(renderNode); + } + } + return { + renderNode, + provider: undefined, + embeddedViews: (def.flags & NodeFlags.HasEmbeddedViews) ? [] : undefined, + componentView: undefined + }; +} diff --git a/modules/@angular/core/src/view/element.ts b/modules/@angular/core/src/view/element.ts new file mode 100644 index 0000000000..b26af2a336 --- /dev/null +++ b/modules/@angular/core/src/view/element.ts @@ -0,0 +1,212 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {SecurityContext} from '../security'; + +import {BindingDef, BindingType, NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewFlags} from './types'; +import {checkAndUpdateBinding, setBindingDebugInfo} from './util'; + +export function elementDef( + flags: NodeFlags, childCount: number, name: string, fixedAttrs: {[name: string]: string} = {}, + bindings: ([BindingType.ElementClass, string] | [BindingType.ElementStyle, string, string] | [ + BindingType.ElementAttribute | BindingType.ElementProperty, string, SecurityContext + ])[] = []): NodeDef { + const bindingDefs = new Array(bindings.length); + for (let i = 0; i < bindings.length; i++) { + const entry = bindings[i]; + let bindingDef: BindingDef; + const bindingType = entry[0]; + const name = entry[1]; + let securityContext: SecurityContext; + let suffix: string; + switch (bindingType) { + case BindingType.ElementStyle: + suffix = entry[2]; + break; + case BindingType.ElementAttribute: + case BindingType.ElementProperty: + securityContext = entry[2]; + break; + } + bindingDefs[i] = {type: bindingType, name, nonMinfiedName: name, securityContext, suffix}; + } + return { + type: NodeType.Element, + // will bet set by the view definition + index: undefined, + reverseChildIndex: undefined, + parent: undefined, + childFlags: undefined, + bindingIndex: undefined, + providerIndices: undefined, + // regular values + flags, + childCount, + bindings: bindingDefs, + element: {name, attrs: fixedAttrs}, + provider: undefined, + text: undefined, + component: undefined, + template: undefined + }; +} + +export function createElement(view: ViewData, renderHost: any, def: NodeDef): NodeData { + const parentNode = def.parent != null ? view.nodes[def.parent].renderNode : renderHost; + const elDef = def.element; + let el: any; + if (view.renderer) { + el = view.renderer.createElement(parentNode, elDef.name); + if (elDef.attrs) { + for (let attrName in elDef.attrs) { + view.renderer.setElementAttribute(el, attrName, elDef.attrs[attrName]); + } + } + } else { + el = document.createElement(elDef.name); + if (parentNode) { + parentNode.appendChild(el); + } + if (elDef.attrs) { + for (let attrName in elDef.attrs) { + el.setAttribute(attrName, elDef.attrs[attrName]); + } + } + } + return { + renderNode: el, + provider: undefined, + embeddedViews: (def.flags & NodeFlags.HasEmbeddedViews) ? [] : undefined, + componentView: undefined + }; +} + +export function checkAndUpdateElementInline( + view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, + v7: any, v8: any, v9: any) { + // Note: fallthrough is intended! + switch (def.bindings.length) { + case 10: + checkAndUpdateElementValue(view, def, 9, v9); + case 9: + checkAndUpdateElementValue(view, def, 8, v8); + case 8: + checkAndUpdateElementValue(view, def, 7, v7); + case 7: + checkAndUpdateElementValue(view, def, 6, v6); + case 6: + checkAndUpdateElementValue(view, def, 5, v5); + case 5: + checkAndUpdateElementValue(view, def, 4, v4); + case 4: + checkAndUpdateElementValue(view, def, 3, v3); + case 3: + checkAndUpdateElementValue(view, def, 2, v2); + case 2: + checkAndUpdateElementValue(view, def, 1, v1); + case 1: + checkAndUpdateElementValue(view, def, 0, v0); + } +} + +export function checkAndUpdateElementDynamic(view: ViewData, def: NodeDef, values: any[]) { + for (let i = 0; i < values.length; i++) { + checkAndUpdateElementValue(view, def, i, values[i]); + } +} + +function checkAndUpdateElementValue(view: ViewData, def: NodeDef, bindingIdx: number, value: any) { + if (!checkAndUpdateBinding(view, def, bindingIdx, value)) { + return; + } + + const binding = def.bindings[bindingIdx]; + const name = binding.name; + const renderNode = view.nodes[def.index].renderNode; + switch (binding.type) { + case BindingType.ElementAttribute: + setElementAttribute(view, binding, renderNode, name, value); + break; + case BindingType.ElementClass: + setElementClass(view, renderNode, name, value); + break; + case BindingType.ElementStyle: + setElementStyle(view, binding, renderNode, name, value); + break; + case BindingType.ElementProperty: + setElementProperty(view, binding, renderNode, name, value); + break; + } +} + +function setElementAttribute( + view: ViewData, binding: BindingDef, renderNode: any, name: string, value: any) { + const securityContext = binding.securityContext; + let renderValue = securityContext ? view.services.sanitize(securityContext, value) : value; + renderValue = renderValue != null ? renderValue.toString() : null; + if (view.renderer) { + view.renderer.setElementAttribute(renderNode, name, renderValue); + } else { + if (value != null) { + renderNode.setAttribute(name, renderValue); + } else { + renderNode.removeAttribute(name); + } + } +} + +function setElementClass(view: ViewData, renderNode: any, name: string, value: boolean) { + if (view.renderer) { + view.renderer.setElementClass(renderNode, name, value); + } else { + if (value) { + renderNode.classList.add(name); + } else { + renderNode.classList.remove(name); + } + } +} + +function setElementStyle( + view: ViewData, binding: BindingDef, renderNode: any, name: string, value: any) { + let renderValue = view.services.sanitize(SecurityContext.STYLE, value); + if (renderValue != null) { + renderValue = renderValue.toString(); + const unit = binding.suffix; + if (unit != null) { + renderValue = renderValue + unit; + } + } else { + renderValue = null; + } + if (view.renderer) { + view.renderer.setElementStyle(renderNode, name, renderValue); + } else { + if (renderValue != null) { + renderNode.style[name] = renderValue; + } else { + // IE requires '' instead of null + // see https://github.com/angular/angular/issues/7916 + (renderNode.style as any)[name] = ''; + } + } +} + +function setElementProperty( + view: ViewData, binding: BindingDef, renderNode: any, name: string, value: any) { + const securityContext = binding.securityContext; + let renderValue = securityContext ? view.services.sanitize(securityContext, value) : value; + if (view.renderer) { + view.renderer.setElementProperty(renderNode, name, renderValue); + if (view.def.flags & ViewFlags.LogBindingUpdate) { + setBindingDebugInfo(view.renderer, renderNode, name, renderValue); + } + } else { + renderNode[name] = renderValue; + } +} diff --git a/modules/@angular/core/src/view/index.ts b/modules/@angular/core/src/view/index.ts new file mode 100644 index 0000000000..42ee2c0544 --- /dev/null +++ b/modules/@angular/core/src/view/index.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +export {anchorDef} from './anchor'; +export {elementDef} from './element'; +export {providerDef} from './provider'; +export {textDef} from './text'; +export {checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, viewDef} from './view'; +export {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach'; + +export * from './types'; +export {DefaultServices} from './services'; diff --git a/modules/@angular/core/src/view/provider.ts b/modules/@angular/core/src/view/provider.ts new file mode 100644 index 0000000000..3fc2bb1657 --- /dev/null +++ b/modules/@angular/core/src/view/provider.ts @@ -0,0 +1,291 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {SimpleChange, SimpleChanges} from '../change_detection/change_detection'; +import {Injector} from '../di'; +import {stringify} from '../facade/lang'; +import {ElementRef} from '../linker/element_ref'; +import {TemplateRef} from '../linker/template_ref'; +import {ViewContainerRef} from '../linker/view_container_ref'; +import {Renderer} from '../render/api'; + +import {BindingDef, BindingType, DepDef, DepFlags, NodeData, NodeDef, NodeFlags, NodeType, Services, ViewData, ViewDefinition, ViewFlags} from './types'; +import {checkAndUpdateBinding, checkAndUpdateBindingWithChange, setBindingDebugInfo} from './util'; + +const _tokenKeyCache = new Map(); + +const RendererTokenKey = tokenKey(Renderer); +const ElementRefTokenKey = tokenKey(ElementRef); +const ViewContainerRefTokenKey = tokenKey(ViewContainerRef); +const TemplateRefTokenKey = tokenKey(TemplateRef); + +export function providerDef( + flags: NodeFlags, ctor: any, deps: ([DepFlags, any] | any)[], + props?: {[name: string]: [number, string]}, component?: () => ViewDefinition): NodeDef { + const bindings: BindingDef[] = []; + if (props) { + for (let prop in props) { + const [bindingIndex, nonMinifiedName] = props[prop]; + bindings[bindingIndex] = { + type: BindingType.ProviderProperty, + name: prop, nonMinifiedName, + securityContext: undefined, + suffix: undefined + }; + } + } + const depDefs: DepDef[] = deps.map(value => { + let token: any; + let flags: DepFlags; + if (Array.isArray(value)) { + [flags, token] = value; + } else { + flags = DepFlags.None; + token = value; + } + return {flags, token, tokenKey: tokenKey(token)}; + }); + if (component) { + flags = flags | NodeFlags.HasComponent; + } + return { + type: NodeType.Provider, + // will bet set by the view definition + index: undefined, + reverseChildIndex: undefined, + parent: undefined, + childFlags: undefined, + bindingIndex: undefined, + providerIndices: undefined, + // regular values + flags, + childCount: 0, bindings, + element: undefined, + provider: { + tokenKey: tokenKey(ctor), + ctor, + deps: depDefs, + }, + text: undefined, component, + template: undefined + }; +} + +export function tokenKey(token: any): string { + let key = _tokenKeyCache.get(token); + if (!key) { + key = stringify(token) + '_' + _tokenKeyCache.size; + _tokenKeyCache.set(token, key); + } + return key; +} + +export function createProvider(view: ViewData, def: NodeDef, componentView: ViewData): NodeData { + const providerDef = def.provider; + return { + renderNode: undefined, + provider: createInstance(view, def.parent, providerDef.ctor, providerDef.deps), + embeddedViews: undefined, componentView + }; +} + +export function checkAndUpdateProviderInline( + view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, + v7: any, v8: any, v9: any) { + const provider = view.nodes[def.index].provider; + let changes: SimpleChanges; + // Note: fallthrough is intended! + switch (def.bindings.length) { + case 10: + changes = checkAndUpdateProp(view, provider, def, 9, v9, changes); + case 9: + changes = checkAndUpdateProp(view, provider, def, 8, v8, changes); + case 8: + changes = checkAndUpdateProp(view, provider, def, 7, v7, changes); + case 7: + changes = checkAndUpdateProp(view, provider, def, 6, v6, changes); + case 6: + changes = checkAndUpdateProp(view, provider, def, 5, v5, changes); + case 5: + changes = checkAndUpdateProp(view, provider, def, 4, v4, changes); + case 4: + changes = checkAndUpdateProp(view, provider, def, 3, v3, changes); + case 3: + changes = checkAndUpdateProp(view, provider, def, 2, v2, changes); + case 2: + changes = checkAndUpdateProp(view, provider, def, 1, v1, changes); + case 1: + changes = checkAndUpdateProp(view, provider, def, 0, v0, changes); + } + if (changes) { + provider.ngOnChanges(changes); + } + if (view.firstChange && (def.flags & NodeFlags.OnInit)) { + provider.ngOnInit(); + } + if (def.flags & NodeFlags.DoCheck) { + provider.ngDoCheck(); + } +} + +export function checkAndUpdateProviderDynamic( + view: ViewData, index: number, def: NodeDef, values: any[]) { + const provider = view.nodes[def.index].provider; + let changes: SimpleChanges; + for (let i = 0; i < values.length; i++) { + changes = checkAndUpdateProp(view, provider, def, i, values[i], changes); + } + if (changes) { + provider.ngOnChanges(changes); + } + if (view.firstChange && (def.flags & NodeFlags.OnInit)) { + provider.ngOnInit(); + } + if (def.flags & NodeFlags.DoCheck) { + provider.ngDoCheck(); + } +} + +function createInstance(view: ViewData, elIndex: number, ctor: any, deps: DepDef[]): any { + const len = deps.length; + let injectable: any; + switch (len) { + case 0: + injectable = new ctor(); + break; + case 1: + injectable = new ctor(resolveDep(view, elIndex, deps[0])); + break; + case 2: + injectable = new ctor(resolveDep(view, elIndex, deps[0]), resolveDep(view, elIndex, deps[1])); + break; + case 3: + injectable = new ctor( + resolveDep(view, elIndex, deps[0]), resolveDep(view, elIndex, deps[1]), + resolveDep(view, elIndex, deps[2])); + break; + default: + const depValues = new Array(len); + for (let i = 0; i < len; i++) { + depValues[i] = resolveDep(view, elIndex, deps[i]); + } + injectable = new ctor(...depValues); + } + return injectable; +} + +export function resolveDep( + view: ViewData, elIndex: number, depDef: DepDef, + notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { + const tokenKey = depDef.tokenKey; + + if (depDef.flags & DepFlags.SkipSelf) { + const elDef = view.def.nodes[elIndex]; + if (elDef.parent != null) { + elIndex = elDef.parent; + } else { + elIndex = view.parentIndex; + view = view.parent; + } + } + + while (view) { + const elDef = view.def.nodes[elIndex]; + switch (tokenKey) { + case RendererTokenKey: + if (view.renderer) { + return view.renderer; + } else { + return Injector.NULL.get(depDef.token, notFoundValue); + } + case ElementRefTokenKey: + return new ElementRef(view.nodes[elIndex].renderNode); + case ViewContainerRefTokenKey: + return view.services.createViewContainerRef(view.nodes[elIndex]); + case TemplateRefTokenKey: + return view.services.createTemplateRef(view, elDef); + default: + const providerIndex = elDef.providerIndices[tokenKey]; + if (providerIndex != null) { + return view.nodes[providerIndex].provider; + } + } + elIndex = view.parentIndex; + view = view.parent; + } + return Injector.NULL.get(depDef.token, notFoundValue); +} + +function checkAndUpdateProp( + view: ViewData, provider: any, def: NodeDef, bindingIdx: number, value: any, + changes: SimpleChanges): SimpleChanges { + let change: SimpleChange; + let changed: boolean; + if (def.flags & NodeFlags.OnChanges) { + change = checkAndUpdateBindingWithChange(view, def, bindingIdx, value); + changed = !!change; + } else { + changed = checkAndUpdateBinding(view, def, bindingIdx, value); + } + if (changed) { + const binding = def.bindings[bindingIdx]; + const propName = binding.name; + // Note: This is still safe with Closure Compiler as + // the user passed in the property name as an object has to `providerDef`, + // so Closure Compiler will have renamed the property correctly already. + provider[propName] = value; + + if (view.def.flags & ViewFlags.LogBindingUpdate) { + setBindingDebugInfo(view.renderer, view.nodes[def.parent].renderNode, name, value); + } + if (change) { + changes = changes || {}; + changes[binding.nonMinifiedName] = change; + } + } + return changes; +} + +export function callLifecycleHooksChildrenFirst(view: ViewData, lifecycles: NodeFlags) { + if (!(view.def.nodeFlags & lifecycles)) { + return; + } + const len = view.def.nodes.length; + for (let i = 0; i < len; i++) { + // We use the provider post order to call providers of children first. + const nodeDef = view.def.reverseChildNodes[i]; + const nodeIndex = nodeDef.index; + if (nodeDef.flags & lifecycles) { + // a leaf + callProviderLifecycles(view.nodes[nodeIndex].provider, nodeDef.flags & lifecycles); + } else if ((nodeDef.childFlags & lifecycles) === 0) { + // a parent with leafs + // no child matches one of the lifecycles, + // then skip the children + i += nodeDef.childCount; + } + } +} + +function callProviderLifecycles(provider: any, lifecycles: NodeFlags) { + if (lifecycles & NodeFlags.AfterContentInit) { + provider.ngAfterContentInit(); + } + if (lifecycles & NodeFlags.AfterContentChecked) { + provider.ngAfterContentChecked(); + } + if (lifecycles & NodeFlags.AfterViewInit) { + provider.ngAfterViewInit(); + } + if (lifecycles & NodeFlags.AfterViewChecked) { + provider.ngAfterViewChecked(); + } + if (lifecycles & NodeFlags.OnDestroy) { + provider.ngOnDestroy(); + } +} diff --git a/modules/@angular/core/src/view/services.ts b/modules/@angular/core/src/view/services.ts new file mode 100644 index 0000000000..93664d98d1 --- /dev/null +++ b/modules/@angular/core/src/view/services.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {Injectable, Injector} from '../di'; +import {unimplemented} from '../facade/errors'; +import {ComponentFactory, ComponentRef} from '../linker/component_factory'; +import {ElementRef} from '../linker/element_ref'; +import {TemplateRef} from '../linker/template_ref'; +import {ViewContainerRef} from '../linker/view_container_ref'; +import {EmbeddedViewRef, ViewRef} from '../linker/view_ref'; +import {RenderComponentType, Renderer, RootRenderer} from '../render/api'; +import {Sanitizer, SecurityContext} from '../security'; + +import {NodeData, NodeDef, Services, ViewData, ViewDefinition} from './types'; +import {checkAndUpdateView, checkNoChangesView, createEmbeddedView, destroyView} from './view'; +import {attachEmbeddedView, detachEmbeddedView, rootRenderNodes} from './view_attach'; + +@Injectable() +export class DefaultServices implements Services { + constructor(private _rootRenderer: RootRenderer, private _sanitizer: Sanitizer) {} + + renderComponent(rcp: RenderComponentType): Renderer { + return this._rootRenderer.renderComponent(rcp); + } + sanitize(context: SecurityContext, value: string): string { + return this._sanitizer.sanitize(context, value); + } + // Note: This needs to be here to prevent a cycle in source files. + createViewContainerRef(data: NodeData): ViewContainerRef { return new ViewContainerRef_(data); } + // Note: This needs to be here to prevent a cycle in source files. + createTemplateRef(parentView: ViewData, def: NodeDef): TemplateRef { + return new TemplateRef_(parentView, def); + } +} + +class ViewContainerRef_ implements ViewContainerRef { + constructor(private _data: NodeData) {} + + get element(): ElementRef { return unimplemented(); } + + get injector(): Injector { return unimplemented(); } + + get parentInjector(): Injector { return unimplemented(); } + + clear(): void { + const len = this._data.embeddedViews.length; + for (let i = len - 1; i >= 0; i--) { + const view = detachEmbeddedView(this._data, i); + destroyView(view); + } + } + + get(index: number): ViewRef { return new ViewRef_(this._data.embeddedViews[index]); } + + get length(): number { return this._data.embeddedViews.length; }; + + createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): + EmbeddedViewRef { + const viewRef = templateRef.createEmbeddedView(context); + this.insert(viewRef, index); + return viewRef; + } + + createComponent( + componentFactory: ComponentFactory, index?: number, injector?: Injector, + projectableNodes?: any[][]): ComponentRef { + return unimplemented(); + } + + insert(viewRef: ViewRef, index?: number): ViewRef { + const viewData = (viewRef)._view; + attachEmbeddedView(this._data, index, viewData); + return viewRef; + } + + move(viewRef: ViewRef, currentIndex: number): ViewRef { return unimplemented(); } + + indexOf(viewRef: ViewRef): number { + return this._data.embeddedViews.indexOf((viewRef)._view); + } + + remove(index?: number): void { + const viewData = detachEmbeddedView(this._data, index); + destroyView(viewData); + } + + detach(index?: number): ViewRef { + const view = this.get(index); + detachEmbeddedView(this._data, index); + return view; + } +} + +class ViewRef_ implements EmbeddedViewRef { + /** @internal */ + _view: ViewData; + + constructor(_view: ViewData) { this._view = _view; } + + get rootNodes(): any[] { return rootRenderNodes(this._view); } + + get context() { return this._view.context; } + + get destroyed(): boolean { return unimplemented(); } + + markForCheck(): void { unimplemented(); } + detach(): void { unimplemented(); } + detectChanges(): void { checkAndUpdateView(this._view); } + checkNoChanges(): void { checkNoChangesView(this._view); } + reattach(): void { unimplemented(); } + onDestroy(callback: Function) { unimplemented(); } + + destroy() { unimplemented(); } +} + +class TemplateRef_ implements TemplateRef { + constructor(private _parentView: ViewData, private _def: NodeDef) {} + + createEmbeddedView(context: any): EmbeddedViewRef { + return new ViewRef_(createEmbeddedView(this._parentView, this._def, context)); + } + + get elementRef(): ElementRef { + return new ElementRef(this._parentView.nodes[this._def.index].renderNode); + } +} diff --git a/modules/@angular/core/src/view/text.ts b/modules/@angular/core/src/view/text.ts new file mode 100644 index 0000000000..0ef3c7b925 --- /dev/null +++ b/modules/@angular/core/src/view/text.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {looseIdentical} from '../facade/lang'; + +import {BindingDef, BindingType, NodeData, NodeDef, NodeFlags, NodeType, Services, ViewData} from './types'; +import {checkAndUpdateBinding} from './util'; + +export function textDef(constants: string[]): NodeDef { + const bindings: BindingDef[] = new Array(constants.length - 1); + for (let i = 1; i < constants.length; i++) { + bindings[i - 1] = { + type: BindingType.Interpolation, + name: undefined, + nonMinifiedName: undefined, + securityContext: undefined, + suffix: constants[i] + }; + } + return { + type: NodeType.Text, + // will bet set by the view definition + index: undefined, + reverseChildIndex: undefined, + parent: undefined, + childFlags: undefined, + bindingIndex: undefined, + providerIndices: undefined, + // regular values + flags: 0, + childCount: 0, bindings, + element: undefined, + provider: undefined, + text: {prefix: constants[0]}, + component: undefined, + template: undefined + }; +} + +export function createText(view: ViewData, renderHost: any, def: NodeDef): NodeData { + const parentNode = def.parent != null ? view.nodes[def.parent].renderNode : renderHost; + let renderNode: any; + if (view.renderer) { + renderNode = view.renderer.createText(parentNode, def.text.prefix); + } else { + renderNode = document.createTextNode(def.text.prefix); + if (parentNode) { + parentNode.appendChild(renderNode); + } + } + return {renderNode, provider: undefined, embeddedViews: undefined, componentView: undefined}; +} + +export function checkAndUpdateTextInline( + view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, + v7: any, v8: any, v9: any) { + const bindings = def.bindings; + let changed = false; + // Note: fallthrough is intended! + switch (bindings.length) { + case 10: + if (checkAndUpdateBinding(view, def, 9, v9)) changed = true; + case 9: + if (checkAndUpdateBinding(view, def, 8, v8)) changed = true; + case 8: + if (checkAndUpdateBinding(view, def, 7, v7)) changed = true; + case 7: + if (checkAndUpdateBinding(view, def, 6, v6)) changed = true; + case 6: + if (checkAndUpdateBinding(view, def, 5, v5)) changed = true; + case 5: + if (checkAndUpdateBinding(view, def, 4, v4)) changed = true; + case 4: + if (checkAndUpdateBinding(view, def, 3, v3)) changed = true; + case 3: + if (checkAndUpdateBinding(view, def, 2, v2)) changed = true; + case 2: + if (checkAndUpdateBinding(view, def, 1, v1)) changed = true; + case 1: + if (checkAndUpdateBinding(view, def, 0, v0)) changed = true; + } + + if (changed) { + let value = ''; + // Note: fallthrough is intended! + switch (bindings.length) { + case 10: + value = _addInterpolationPart(v9, bindings[9]); + case 9: + value = _addInterpolationPart(v8, bindings[8]) + value; + case 8: + value = _addInterpolationPart(v7, bindings[7]) + value; + case 7: + value = _addInterpolationPart(v6, bindings[6]) + value; + case 6: + value = _addInterpolationPart(v5, bindings[5]) + value; + case 5: + value = _addInterpolationPart(v4, bindings[4]) + value; + case 4: + value = _addInterpolationPart(v3, bindings[3]) + value; + case 3: + value = _addInterpolationPart(v2, bindings[2]) + value; + case 2: + value = _addInterpolationPart(v1, bindings[1]) + value; + case 1: + value = _addInterpolationPart(v0, bindings[0]) + value; + } + value = def.text.prefix + value; + const renderNode = view.nodes[def.index].renderNode; + if (view.renderer) { + view.renderer.setText(renderNode, value); + } else { + renderNode.nodeValue = value; + } + } +} + +export function checkAndUpdateTextDynamic(view: ViewData, def: NodeDef, values: any[]) { + const bindings = def.bindings; + let changed = view.firstChange; + for (let i = 0; i < values.length && !changed; i++) { + changed = changed || checkAndUpdateBinding(view, def, i, values[i]); + } + if (changed) { + let value = ''; + for (let i = 0; i < values.length; i++) { + value = value + _addInterpolationPart(values[i], bindings[i]); + } + value = def.text.prefix + value; + const renderNode = view.nodes[def.index].renderNode; + if (view.renderer) { + view.renderer.setText(renderNode, value); + } else { + renderNode.nodeValue = value; + } + } +} + +function _addInterpolationPart(value: any, binding: BindingDef): string { + const valueStr = value != null ? value.toString() : ''; + return valueStr + binding.suffix; +} diff --git a/modules/@angular/core/src/view/types.ts b/modules/@angular/core/src/view/types.ts new file mode 100644 index 0000000000..8bf3cc4f52 --- /dev/null +++ b/modules/@angular/core/src/view/types.ts @@ -0,0 +1,187 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {TemplateRef} from '../linker/template_ref'; +import {ViewContainerRef} from '../linker/view_container_ref'; +import {RenderComponentType, Renderer, RootRenderer} from '../render/api'; +import {Sanitizer, SecurityContext} from '../security'; + +// ------------------------------------- +// Defs +// ------------------------------------- + +export interface ViewDefinition { + flags: ViewFlags; + componentType: RenderComponentType; + update: ViewUpdateFn; + /** + * Order: Depth first. + * Especially providers are before elements / anchros. + */ + nodes: NodeDef[]; + /** aggregated NodeFlags for all nodes **/ + nodeFlags: NodeFlags; + /** + * Order: parents before children, but children in reverse order. + * Especially providers are after elements / anchros. + */ + reverseChildNodes: NodeDef[]; + lastRootNode: number; + bindingCount: number; +} + +export type ViewUpdateFn = (updater: NodeUpdater, view: ViewData, component: any, context: any) => + void; + +export interface NodeUpdater { + checkInline( + view: ViewData, nodeIndex: number, v0?: any, v1?: any, v2?: any, v3?: any, v4?: any, v5?: any, + v6?: any, v7?: any, v8?: any, v9?: any): void; + checkDynamic(view: ViewData, nodeIndex: number, values: any[]): void; +} + +/** + * Bitmask for ViewDefintion.flags. + */ +export enum ViewFlags { + None = 0, + LogBindingUpdate = 1 << 0, + DirectDom = 1 << 1 +} + +export interface NodeDef { + type: NodeType; + index: number; + reverseChildIndex: number; + flags: NodeFlags; + parent: number; + /** number of transitive children */ + childCount: number; + /** aggregated NodeFlags for all children **/ + childFlags: NodeFlags; + bindingIndex: number; + bindings: BindingDef[]; + element: ElementDef; + providerIndices: {[tokenKey: string]: number}; + provider: ProviderDef; + text: TextDef; + // closure to allow recursive components + component: () => ViewDefinition; + template: ViewDefinition; +} + +export enum NodeType { + Element, + Text, + Anchor, + Provider +} + +/** + * Bitmask for NodeDef.flags. + */ +export enum NodeFlags { + None = 0, + OnInit = 1 << 0, + OnDestroy = 1 << 1, + DoCheck = 1 << 2, + OnChanges = 1 << 3, + AfterContentInit = 1 << 4, + AfterContentChecked = 1 << 5, + AfterViewInit = 1 << 6, + AfterViewChecked = 1 << 7, + HasEmbeddedViews = 1 << 8, + HasComponent = 1 << 9, +} + +export interface ElementDef { + name: string; + attrs: {[name: string]: string}; +} + +/** + * Bitmask for DI flags + */ +export enum DepFlags { + None = 0, + SkipSelf = 1 << 0 +} + +export interface DepDef { + flags: DepFlags; + token: any; + tokenKey: string; +} + +export interface ProviderDef { + tokenKey: string; + ctor: any; + deps: DepDef[]; +} + +export interface TextDef { prefix: string; } + +export enum BindingType { + ElementAttribute, + ElementClass, + ElementStyle, + ElementProperty, + ProviderProperty, + Interpolation +} + +export interface BindingDef { + type: BindingType; + name: string; + nonMinifiedName: string; + securityContext: SecurityContext; + suffix: string; +} + +// ------------------------------------- +// Data +// ------------------------------------- + +/** + * View instance data. + * Attention: Adding fields to this is performance sensitive! + */ +export interface ViewData { + def: ViewDefinition; + renderer: Renderer; + services: Services; + // index of parent element / anchor. Not the index + // of the provider with the component view. + parentIndex: number; + parent: ViewData; + component: any; + context: any; + nodes: NodeData[]; + firstChange: boolean; + oldValues: any[]; +} + +/** + * Node instance data. + * Attention: Adding fields to this is performance sensitive! + */ +export interface NodeData { + renderNode: any; + provider: any; + componentView: ViewData; + embeddedViews: ViewData[]; +} + +export interface Services { + renderComponent(rcp: RenderComponentType): Renderer; + sanitize(context: SecurityContext, value: string): string; + // Note: This needs to be here to prevent a cycle in source files. + createViewContainerRef(data: NodeData): ViewContainerRef; + // Note: This needs to be here to prevent a cycle in source files. + createTemplateRef(parentView: ViewData, def: NodeDef): TemplateRef; +} diff --git a/modules/@angular/core/src/view/util.ts b/modules/@angular/core/src/view/util.ts new file mode 100644 index 0000000000..ebc3b33c20 --- /dev/null +++ b/modules/@angular/core/src/view/util.ts @@ -0,0 +1,62 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {devModeEqual} from '../change_detection/change_detection'; +import {SimpleChange} from '../change_detection/change_detection_util'; +import {looseIdentical} from '../facade/lang'; +import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; +import {Renderer} from '../render/api'; + +import {NodeData, NodeDef, NodeFlags, NodeType, ViewData, ViewDefinition} from './types'; + +export function setBindingDebugInfo( + renderer: Renderer, renderNode: any, propName: string, value: any) { + try { + renderer.setBindingDebugInfo( + renderNode, `ng-reflect-${camelCaseToDashCase(propName)}`, value ? value.toString() : null); + } catch (e) { + renderer.setBindingDebugInfo( + renderNode, `ng-reflect-${camelCaseToDashCase(propName)}`, + '[ERROR] Exception while trying to serialize the value'); + } +} + +const CAMEL_CASE_REGEXP = /([A-Z])/g; + +function camelCaseToDashCase(input: string): string { + return input.replace(CAMEL_CASE_REGEXP, (...m: any[]) => '-' + m[1].toLowerCase()); +} + +export function checkBindingNoChanges( + view: ViewData, def: NodeDef, bindingIdx: number, value: any) { + const oldValue = view.oldValues[def.bindingIndex + bindingIdx]; + if (view.firstChange || !devModeEqual(oldValue, value)) { + throw new ExpressionChangedAfterItHasBeenCheckedError(oldValue, value, view.firstChange); + } +} + +export function checkAndUpdateBinding( + view: ViewData, def: NodeDef, bindingIdx: number, value: any): boolean { + const oldValues = view.oldValues; + if (view.firstChange || !looseIdentical(oldValues[def.bindingIndex + bindingIdx], value)) { + oldValues[def.bindingIndex + bindingIdx] = value; + return true; + } + return false; +} + +export function checkAndUpdateBindingWithChange( + view: ViewData, def: NodeDef, bindingIdx: number, value: any): SimpleChange { + const oldValues = view.oldValues; + const oldValue = oldValues[def.bindingIndex + bindingIdx]; + if (view.firstChange || !looseIdentical(oldValue, value)) { + oldValues[def.bindingIndex + bindingIdx] = value; + return new SimpleChange(oldValue, value, view.firstChange); + } + return null; +} diff --git a/modules/@angular/core/src/view/view.ts b/modules/@angular/core/src/view/view.ts new file mode 100644 index 0000000000..b62c5b882c --- /dev/null +++ b/modules/@angular/core/src/view/view.ts @@ -0,0 +1,399 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ExpressionChangedAfterItHasBeenCheckedError} from '../linker/errors'; +import {RenderComponentType, Renderer} from '../render/api'; + +import {createAnchor} from './anchor'; +import {checkAndUpdateElementDynamic, checkAndUpdateElementInline, createElement} from './element'; +import {callLifecycleHooksChildrenFirst, checkAndUpdateProviderDynamic, checkAndUpdateProviderInline, createProvider} from './provider'; +import {checkAndUpdateTextDynamic, checkAndUpdateTextInline, createText} from './text'; +import {ElementDef, NodeData, NodeDef, NodeFlags, NodeType, NodeUpdater, ProviderDef, Services, TextDef, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn} from './types'; +import {checkBindingNoChanges} from './util'; + +const NOOP_UPDATE = (): any => undefined; + +export function viewDef( + flags: ViewFlags, nodesWithoutIndices: NodeDef[], update?: ViewUpdateFn, + componentType?: RenderComponentType): ViewDefinition { + // clone nodes and set auto calculated values + if (nodesWithoutIndices.length === 0) { + throw new Error(`Illegal State: Views without nodes are not allowed!`); + } + const nodes: NodeDef[] = new Array(nodesWithoutIndices.length); + const reverseChildNodes: NodeDef[] = new Array(nodesWithoutIndices.length); + let viewBindingCount = 0; + let viewFlags = 0; + let currentParent: NodeDef = null; + let lastRootNode: NodeDef = null; + for (let i = 0; i < nodesWithoutIndices.length; i++) { + while (currentParent && i > currentParent.index + currentParent.childCount) { + const newParent = nodes[currentParent.parent]; + if (newParent) { + newParent.childFlags |= currentParent.childFlags; + } + currentParent = newParent; + } + const reverseChildIndex = calculateReverseChildIndex( + currentParent, i, nodesWithoutIndices[i].childCount, nodesWithoutIndices.length); + const node = cloneAndModifyNode(nodesWithoutIndices[i], { + index: i, + parent: currentParent ? currentParent.index : undefined, + bindingIndex: viewBindingCount, reverseChildIndex, + providerIndices: Object.create(currentParent ? currentParent.providerIndices : null) + }); + nodes[i] = node; + reverseChildNodes[reverseChildIndex] = node; + validateNode(currentParent, node); + + viewFlags |= node.flags; + viewBindingCount += node.bindings.length; + if (currentParent) { + currentParent.childFlags |= node.flags; + } + + if (!currentParent) { + lastRootNode = node; + } + if (node.provider) { + currentParent.providerIndices[node.provider.tokenKey] = i; + } + if (node.childCount) { + currentParent = node; + } + } + + return { + nodeFlags: viewFlags, + flags, + nodes: nodes, reverseChildNodes, + update: update || NOOP_UPDATE, componentType, + bindingCount: viewBindingCount, + lastRootNode: lastRootNode.index + }; +} + +function calculateReverseChildIndex( + currentParent: NodeDef, i: number, childCount: number, nodeCount: number) { + // Notes about reverse child order: + // - Every node is directly before its children, in dfs and reverse child order. + // - node.childCount contains all children, in dfs and reverse child order. + // - In dfs order, every node is before its first child + // - In reverse child order, every node is before its last child + + // Algorithm, main idea: + // - In reverse child order, the ranges for each child + its transitive children are mirrored + // regarding their position inside of their parent + + // Visualization: + // Given the following tree: + // Nodes: n0 + // n1 n2 + // n11 n12 n21 n22 + // dfs: 0 1 2 3 4 5 6 + // result: 0 4 6 5 1 3 2 + // + // Example: + // Current node = 1 + // 1) lastChildIndex = 3 + // 2) lastChildOffsetRelativeToParentInDfsOrder = 2 + // 3) parentEndIndexInReverseChildOrder = 6 + // 4) result = 4 + let lastChildOffsetRelativeToParentInDfsOrder: number; + let parentEndIndexInReverseChildOrder: number; + if (currentParent) { + const lastChildIndex = i + childCount; + lastChildOffsetRelativeToParentInDfsOrder = lastChildIndex - currentParent.index - 1; + parentEndIndexInReverseChildOrder = currentParent.reverseChildIndex + currentParent.childCount; + } else { + lastChildOffsetRelativeToParentInDfsOrder = i + childCount; + parentEndIndexInReverseChildOrder = nodeCount - 1; + } + return parentEndIndexInReverseChildOrder - lastChildOffsetRelativeToParentInDfsOrder; +} + +function validateNode(parent: NodeDef, node: NodeDef) { + if (node.template) { + if (node.template.lastRootNode != null && + node.template.nodes[node.template.lastRootNode].flags & NodeFlags.HasEmbeddedViews) { + throw new Error( + `Illegal State: Last root node of a template can't have embedded views, at index ${node.index}!`); + } + } + if (node.provider) { + const parentType = parent ? parent.type : null; + if (parentType !== NodeType.Element && parentType !== NodeType.Anchor) { + throw new Error( + `Illegal State: Provider nodes need to be children of elements or anchors, at index ${node.index}!`); + } + } + if (node.childCount) { + if (parent) { + const parentEnd = parent.index + parent.childCount; + if (node.index <= parentEnd && node.index + node.childCount > parentEnd) { + throw new Error( + `Illegal State: childCount of node leads outside of parent, at index ${node.index}!`); + } + } + } +} + +function cloneAndModifyNode(nodeDef: NodeDef, values: { + index: number, + reverseChildIndex: number, + parent: number, + bindingIndex: number, + providerIndices: {[tokenKey: string]: number} +}): NodeDef { + const clonedNode: NodeDef = {}; + for (let prop in nodeDef) { + (clonedNode)[prop] = (nodeDef)[prop]; + } + + clonedNode.index = values.index; + clonedNode.bindingIndex = values.bindingIndex; + clonedNode.parent = values.parent; + clonedNode.reverseChildIndex = values.reverseChildIndex; + clonedNode.providerIndices = values.providerIndices; + // Note: We can't set the value immediately, as we need to walk the children first. + clonedNode.childFlags = 0; + return clonedNode; +} + +export function createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData { + // embedded views are seen as siblings to the anchor, so we need + // to get the parent of the anchor and use it as parentIndex. + const view = createView(parent.services, parent, anchorDef.parent, anchorDef.template); + initView(view, null, parent.component, context); + return view; +} + +export function createRootView(services: Services, def: ViewDefinition, context?: any): ViewData { + const view = createView(services, null, null, def); + initView(view, null, context, context); + return view; +} + +function createView( + services: Services, parent: ViewData, parentIndex: number, def: ViewDefinition): ViewData { + const nodes: NodeData[] = new Array(def.nodes.length); + let renderer: Renderer; + if (def.flags != null && (def.flags & ViewFlags.DirectDom)) { + renderer = null; + } else { + renderer = def.componentType ? services.renderComponent(def.componentType) : parent.renderer; + } + const view: ViewData = { + def, + parent, + parentIndex, + context: undefined, + component: undefined, nodes, + firstChange: true, renderer, services, + oldValues: new Array(def.bindingCount) + }; + return view; +} + +function initView(view: ViewData, renderHost: any, component: any, context: any) { + view.component = component; + view.context = context; + const def = view.def; + const nodes = view.nodes; + for (let i = 0; i < def.nodes.length; i++) { + const nodeDef = def.nodes[i]; + let nodeData: any; + switch (nodeDef.type) { + case NodeType.Element: + nodeData = createElement(view, renderHost, nodeDef); + break; + case NodeType.Text: + nodeData = createText(view, renderHost, nodeDef); + break; + case NodeType.Anchor: + nodeData = createAnchor(view, renderHost, nodeDef); + break; + case NodeType.Provider: + let componentView: ViewData; + if (nodeDef.component) { + componentView = createView(view.services, view, i, nodeDef.component()); + } + nodeData = createProvider(view, nodeDef, componentView); + break; + } + nodes[i] = nodeData; + } + execComponentViewsAction(view, ViewAction.InitComponent); +} + +export function checkNoChangesView(view: ViewData) { + view.def.update(CheckNoChanges, view, view.component, view.context); + execEmbeddedViewsAction(view, ViewAction.CheckNoChanges); + execComponentViewsAction(view, ViewAction.CheckNoChanges); +} + +const CheckNoChanges: NodeUpdater = { + checkInline: (view: ViewData, index: number, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, + v6: any, v7: any, v8: any, v9: any): void => { + const nodeDef = view.def.nodes[index]; + // Note: fallthrough is intended! + switch (nodeDef.bindings.length) { + case 10: + checkBindingNoChanges(view, nodeDef, 9, v9); + case 9: + checkBindingNoChanges(view, nodeDef, 8, v8); + case 8: + checkBindingNoChanges(view, nodeDef, 7, v7); + case 7: + checkBindingNoChanges(view, nodeDef, 6, v6); + case 6: + checkBindingNoChanges(view, nodeDef, 5, v5); + case 5: + checkBindingNoChanges(view, nodeDef, 4, v4); + case 4: + checkBindingNoChanges(view, nodeDef, 3, v3); + case 3: + checkBindingNoChanges(view, nodeDef, 2, v2); + case 2: + checkBindingNoChanges(view, nodeDef, 1, v1); + case 1: + checkBindingNoChanges(view, nodeDef, 0, v0); + } + }, + checkDynamic: (view: ViewData, index: number, values: any[]): void => { + const oldValues = view.oldValues; + for (let i = 0; i < values.length; i++) { + checkBindingNoChanges(view, view.def.nodes[index], i, values[i]); + } + } +}; + +export function checkAndUpdateView(view: ViewData) { + view.def.update(CheckAndUpdate, view, view.component, view.context); + execEmbeddedViewsAction(view, ViewAction.CheckAndUpdate); + + callLifecycleHooksChildrenFirst( + view, NodeFlags.AfterContentChecked | (view.firstChange ? NodeFlags.AfterContentInit : 0)); + execComponentViewsAction(view, ViewAction.CheckAndUpdate); + + callLifecycleHooksChildrenFirst( + view, NodeFlags.AfterViewChecked | (view.firstChange ? NodeFlags.AfterViewInit : 0)); + view.firstChange = false; +} + +const CheckAndUpdate: NodeUpdater = { + checkInline: (view: ViewData, index: number, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, + v6: any, v7: any, v8: any, v9: any): void => { + const nodeDef = view.def.nodes[index]; + switch (nodeDef.type) { + case NodeType.Element: + checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + case NodeType.Text: + checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + case NodeType.Provider: + checkAndUpdateProviderInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9); + break; + } + }, + checkDynamic: (view: ViewData, index: number, values: any[]): void => { + const nodeDef = view.def.nodes[index]; + switch (nodeDef.type) { + case NodeType.Element: + checkAndUpdateElementDynamic(view, nodeDef, values); + break; + case NodeType.Text: + checkAndUpdateTextDynamic(view, nodeDef, values); + break; + case NodeType.Provider: + checkAndUpdateProviderDynamic(view, index, nodeDef, values); + break; + } + } +}; + +export function destroyView(view: ViewData) { + callLifecycleHooksChildrenFirst(view, NodeFlags.OnDestroy); + execComponentViewsAction(view, ViewAction.Destroy); + execEmbeddedViewsAction(view, ViewAction.Destroy); +} + +enum ViewAction { + InitComponent, + CheckNoChanges, + CheckAndUpdate, + Destroy +} + +function execComponentViewsAction(view: ViewData, action: ViewAction) { + const def = view.def; + if (!(def.nodeFlags & NodeFlags.HasComponent)) { + return; + } + for (let i = 0; i < def.nodes.length; i++) { + const nodeDef = def.nodes[i]; + if (nodeDef.flags & NodeFlags.HasComponent) { + // a leaf + const nodeData = view.nodes[i]; + if (action === ViewAction.InitComponent) { + let renderHost = view.nodes[nodeDef.parent].renderNode; + if (view.renderer) { + renderHost = view.renderer.createViewRoot(renderHost); + } + initView(nodeData.componentView, renderHost, nodeData.provider, nodeData.provider); + } else { + callViewAction(nodeData.componentView, action); + } + } else if ((nodeDef.childFlags & NodeFlags.HasComponent) === 0) { + // a parent with leafs + // no child is a component, + // then skip the children + i += nodeDef.childCount; + } + } +} + +function execEmbeddedViewsAction(view: ViewData, action: ViewAction) { + const def = view.def; + if (!(def.nodeFlags & NodeFlags.HasEmbeddedViews)) { + return; + } + for (let i = 0; i < def.nodes.length; i++) { + const nodeDef = def.nodes[i]; + if (nodeDef.flags & NodeFlags.HasEmbeddedViews) { + // a leaf + const nodeData = view.nodes[i]; + const embeddedViews = nodeData.embeddedViews; + if (embeddedViews) { + for (let k = 0; k < embeddedViews.length; k++) { + callViewAction(embeddedViews[k], action); + } + } + } else if ((nodeDef.childFlags & NodeFlags.HasEmbeddedViews) === 0) { + // a parent with leafs + // no child is a component, + // then skip the children + i += nodeDef.childCount; + } + } +} + +function callViewAction(view: ViewData, action: ViewAction) { + switch (action) { + case ViewAction.CheckNoChanges: + checkNoChangesView(view); + break; + case ViewAction.CheckAndUpdate: + checkAndUpdateView(view); + break; + case ViewAction.Destroy: + destroyView(view); + break; + } +} diff --git a/modules/@angular/core/src/view/view_attach.ts b/modules/@angular/core/src/view/view_attach.ts new file mode 100644 index 0000000000..66e1833178 --- /dev/null +++ b/modules/@angular/core/src/view/view_attach.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NodeData, NodeFlags, ViewData} from './types'; + +export function attachEmbeddedView(node: NodeData, viewIndex: number, view: ViewData) { + let embeddedViews = node.embeddedViews; + if (viewIndex == null) { + viewIndex = embeddedViews.length; + } + // perf: array.push is faster than array.splice! + if (viewIndex >= embeddedViews.length) { + embeddedViews.push(view); + } else { + embeddedViews.splice(viewIndex, 0, view); + } + const prevView = viewIndex > 0 ? embeddedViews[viewIndex - 1] : null; + const prevNode = prevView ? prevView.nodes[prevView.def.lastRootNode] : node; + const prevRenderNode = prevNode.renderNode; + if (view.renderer) { + view.renderer.attachViewAfter(prevRenderNode, rootRenderNodes(view)); + } else { + const parentNode = prevRenderNode.parentNode; + const nextSibling = prevRenderNode.nextSibling; + if (parentNode) { + const action = nextSibling ? DirectDomAction.InsertBefore : DirectDomAction.AppendChild; + directDomAttachDetachSiblingRenderNodes(view, 0, action, parentNode, nextSibling); + } + } +} + +export function detachEmbeddedView(node: NodeData, viewIndex: number): ViewData { + const embeddedViews = node.embeddedViews; + if (viewIndex == null) { + viewIndex = embeddedViews.length; + } + const view = embeddedViews[viewIndex]; + // perf: array.pop is faster than array.splice! + if (viewIndex >= embeddedViews.length - 1) { + embeddedViews.pop(); + } else { + embeddedViews.splice(viewIndex, 1); + } + if (view.renderer) { + view.renderer.detachView(rootRenderNodes(view)); + } else { + const parentNode = node.renderNode.parentNode; + if (parentNode) { + directDomAttachDetachSiblingRenderNodes( + view, 0, DirectDomAction.RemoveChild, parentNode, null); + } + } + return view; +} + +export function rootRenderNodes(view: ViewData): any[] { + const renderNodes: any[] = []; + collectSiblingRenderNodes(view, 0, renderNodes); + return renderNodes; +} + +function collectSiblingRenderNodes(view: ViewData, startIndex: number, target: any[]) { + for (let i = startIndex; i < view.nodes.length; i++) { + const nodeDef = view.def.nodes[i]; + const nodeData = view.nodes[i]; + target.push(nodeData.renderNode); + if (nodeDef.flags & NodeFlags.HasEmbeddedViews) { + const embeddedViews = nodeData.embeddedViews; + if (embeddedViews) { + for (let k = 0; k < embeddedViews.length; k++) { + collectSiblingRenderNodes(embeddedViews[k], 0, target); + } + } + } + // jump to next sibling + i += nodeDef.childCount; + } +} + +enum DirectDomAction { + AppendChild, + InsertBefore, + RemoveChild +} + +function directDomAttachDetachSiblingRenderNodes( + view: ViewData, startIndex: number, action: DirectDomAction, parentNode: any, + nextSibling: any) { + for (let i = startIndex; i < view.nodes.length; i++) { + const nodeDef = view.def.nodes[i]; + const nodeData = view.nodes[i]; + switch (action) { + case DirectDomAction.AppendChild: + parentNode.appendChild(nodeData.renderNode); + break; + case DirectDomAction.InsertBefore: + parentNode.insertBefore(nodeData.renderNode, nextSibling); + break; + case DirectDomAction.RemoveChild: + parentNode.removeChild(nodeData.renderNode); + break; + } + if (nodeDef.flags & NodeFlags.HasEmbeddedViews) { + const embeddedViews = nodeData.embeddedViews; + if (embeddedViews) { + for (let k = 0; k < embeddedViews.length; k++) { + directDomAttachDetachSiblingRenderNodes( + embeddedViews[k], 0, action, parentNode, nextSibling); + } + } + } + // jump to next sibling + i += nodeDef.childCount; + } +} \ No newline at end of file diff --git a/modules/@angular/core/test/view/anchor_spec.ts b/modules/@angular/core/test/view/anchor_spec.ts new file mode 100644 index 0000000000..8c90b787e0 --- /dev/null +++ b/modules/@angular/core/test/view/anchor_spec.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; +import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +import {isBrowser, setupAndCheckRenderer} from './helper'; + +export function main() { + if (isBrowser()) { + defineTests({directDom: true, viewFlags: ViewFlags.DirectDom}); + } + defineTests({directDom: false, viewFlags: 0}); +} + +function defineTests(config: {directDom: boolean, viewFlags: number}) { + describe(`View Anchor, directDom: ${config.directDom}`, () => { + setupAndCheckRenderer(config); + + let services: Services; + let renderComponentType: RenderComponentType; + + beforeEach( + inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => { + services = new DefaultServices(rootRenderer, sanitizer); + renderComponentType = + new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); + })); + + function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater, renderComponentType); + } + + function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, viewDef); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + describe('create', () => { + it('should create anchor nodes without parents', () => { + const rootNodes = + createAndGetRootNodes(compViewDef([anchorDef(NodeFlags.None, 0)])).rootNodes; + expect(rootNodes.length).toBe(1); + }); + + it('should create views with multiple root anchor nodes', () => { + const rootNodes = createAndGetRootNodes(compViewDef([ + anchorDef(NodeFlags.None, 0), anchorDef(NodeFlags.None, 0) + ])).rootNodes; + expect(rootNodes.length).toBe(2); + }); + + it('should create anchor nodes with parents', () => { + const rootNodes = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + anchorDef(NodeFlags.None, 0), + ])).rootNodes; + expect(getDOM().childNodes(rootNodes[0]).length).toBe(1); + }); + }); + }); +} diff --git a/modules/@angular/core/test/view/component_view_spec.ts b/modules/@angular/core/test/view/component_view_spec.ts new file mode 100644 index 0000000000..34105bfae5 --- /dev/null +++ b/modules/@angular/core/test/view/component_view_spec.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; +import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +import {isBrowser, setupAndCheckRenderer} from './helper'; + +export function main() { + if (isBrowser()) { + defineTests({directDom: true, viewFlags: ViewFlags.DirectDom}); + } + defineTests({directDom: false, viewFlags: 0}); +} + +function defineTests(config: {directDom: boolean, viewFlags: number}) { + describe(`Component Views, directDom: ${config.directDom}`, () => { + setupAndCheckRenderer(config); + + let services: Services; + let renderComponentType: RenderComponentType; + + beforeEach( + inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => { + services = new DefaultServices(rootRenderer, sanitizer); + renderComponentType = + new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); + })); + + function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater, renderComponentType); + } + + function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, viewDef); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + it('should create and attach component views', () => { + class AComp {} + + const {view, rootNodes} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + providerDef(NodeFlags.None, AComp, [], null, () => compViewDef([ + elementDef(NodeFlags.None, 0, 'span'), + ])), + ])); + + const compRootEl = getDOM().childNodes(rootNodes[0])[0]; + expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span'); + }); + + it('should dirty check component views', () => { + let value = 'v1'; + let instance: AComp; + class AComp { + a: any; + constructor() { instance = this; } + } + + const updater = jasmine.createSpy('updater').and.callFake( + (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, value)); + + const {view, rootNodes} = createAndGetRootNodes( + compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + providerDef(NodeFlags.None, AComp, [], null, () => compViewDef( + [ + elementDef(NodeFlags.None, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]), + ], updater + )), + ], jasmine.createSpy('parentUpdater'))); + + checkAndUpdateView(view); + + expect(updater).toHaveBeenCalled(); + // component + expect(updater.calls.mostRecent().args[2]).toBe(instance); + // view context + expect(updater.calls.mostRecent().args[3]).toBe(instance); + + updater.calls.reset(); + checkNoChangesView(view); + + expect(updater).toHaveBeenCalled(); + // component + expect(updater.calls.mostRecent().args[2]).toBe(instance); + // view context + expect(updater.calls.mostRecent().args[3]).toBe(instance); + + value = 'v2'; + expect(() => checkNoChangesView(view)) + .toThrowError( + `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); + }); + + it('should destroy component views', () => { + const log: string[] = []; + + class AComp {} + + class ChildProvider { + ngOnDestroy() { log.push('ngOnDestroy'); }; + } + + const {view, rootNodes} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + providerDef( + NodeFlags.None, AComp, [], null, () => compViewDef([ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.OnDestroy, ChildProvider, []) + ])), + ])); + + destroyView(view); + + expect(log).toEqual(['ngOnDestroy']); + }); + }); +} \ No newline at end of file diff --git a/modules/@angular/core/test/view/element_spec.ts b/modules/@angular/core/test/view/element_spec.ts new file mode 100644 index 0000000000..7176f76471 --- /dev/null +++ b/modules/@angular/core/test/view/element_spec.ts @@ -0,0 +1,223 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; +import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +import {isBrowser, setupAndCheckRenderer} from './helper'; + +export function main() { + if (isBrowser()) { + defineTests({directDom: true, viewFlags: ViewFlags.DirectDom}); + } + defineTests({directDom: false, viewFlags: 0}); +} + +function defineTests(config: {directDom: boolean, viewFlags: number}) { + describe(`View Elements, directDom: ${config.directDom}`, () => { + setupAndCheckRenderer(config); + + let services: Services; + let renderComponentType: RenderComponentType; + + beforeEach( + inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => { + services = new DefaultServices(rootRenderer, sanitizer); + renderComponentType = + new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); + })); + + function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater, renderComponentType); + } + + function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, viewDef); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + describe('create', () => { + it('should create elements without parents', () => { + const rootNodes = + createAndGetRootNodes(compViewDef([elementDef(NodeFlags.None, 0, 'span')])).rootNodes; + expect(rootNodes.length).toBe(1); + expect(getDOM().nodeName(rootNodes[0]).toLowerCase()).toBe('span'); + }); + + it('should create views with multiple root elements', () => { + const rootNodes = + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 0, 'span'), elementDef(NodeFlags.None, 0, 'span') + ])).rootNodes; + expect(rootNodes.length).toBe(2); + }); + + it('should create elements with parents', () => { + const rootNodes = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + elementDef(NodeFlags.None, 0, 'span'), + ])).rootNodes; + expect(rootNodes.length).toBe(1); + const spanEl = getDOM().childNodes(rootNodes[0])[0]; + expect(getDOM().nodeName(spanEl).toLowerCase()).toBe('span'); + }); + + it('should set fixed attributes', () => { + const rootNodes = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 0, 'div', {'title': 'a'}), + ])).rootNodes; + expect(rootNodes.length).toBe(1); + expect(getDOM().getAttribute(rootNodes[0], 'title')).toBe('a'); + }); + }); + + it('should checkNoChanges', () => { + let attrValue = 'v1'; + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef( + NodeFlags.None, 0, 'div', null, + [[BindingType.ElementAttribute, 'a1', SecurityContext.NONE]]), + ], + (updater, view) => updater.checkInline(view, 0, attrValue))); + + checkAndUpdateView(view); + checkNoChangesView(view); + + attrValue = 'v2'; + expect(() => checkNoChangesView(view)) + .toThrowError( + `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); + }); + + describe('change properties', () => { + [{ + name: 'inline', + updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2') + }, + { + name: 'dynamic', + updater: (updater: NodeUpdater, view: ViewData) => + updater.checkDynamic(view, 0, ['v1', 'v2']) + }].forEach((config) => { + it(`should update ${config.name}`, () => { + + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef( + NodeFlags.None, 0, 'input', null, + [ + [BindingType.ElementProperty, 'title', SecurityContext.NONE], + [BindingType.ElementProperty, 'value', SecurityContext.NONE] + ]), + ], + config.updater)); + + checkAndUpdateView(view); + + const el = rootNodes[0]; + expect(getDOM().getProperty(el, 'title')).toBe('v1'); + expect(getDOM().getProperty(el, 'value')).toBe('v2'); + }); + }); + }); + + describe('change attributes', () => { + [{ + name: 'inline', + updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'v1', 'v2') + }, + { + name: 'dynamic', + updater: (updater: NodeUpdater, view: ViewData) => + updater.checkDynamic(view, 0, ['v1', 'v2']) + }].forEach((config) => { + it(`should update ${config.name}`, () => { + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef( + NodeFlags.None, 0, 'div', null, + [ + [BindingType.ElementAttribute, 'a1', SecurityContext.NONE], + [BindingType.ElementAttribute, 'a2', SecurityContext.NONE] + ]), + ], + config.updater)); + + checkAndUpdateView(view); + + const el = rootNodes[0]; + expect(getDOM().getAttribute(el, 'a1')).toBe('v1'); + expect(getDOM().getAttribute(el, 'a2')).toBe('v2'); + }); + }); + }); + + describe('change classes', () => { + [{ + name: 'inline', + updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, true, true) + }, + { + name: 'dynamic', + updater: (updater: NodeUpdater, view: ViewData) => + updater.checkDynamic(view, 0, [true, true]) + }].forEach((config) => { + it(`should update ${config.name}`, () => { + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef( + NodeFlags.None, 0, 'div', null, + [[BindingType.ElementClass, 'c1'], [BindingType.ElementClass, 'c2']]), + ], + config.updater)); + + checkAndUpdateView(view); + + const el = rootNodes[0]; + expect(getDOM().hasClass(el, 'c1')).toBeTruthy(); + expect(getDOM().hasClass(el, 'c2')).toBeTruthy(); + }); + }); + }); + + describe('change styles', () => { + [{ + name: 'inline', + updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 10, 'red') + }, + { + name: 'dynamic', + updater: (updater: NodeUpdater, view: ViewData) => + updater.checkDynamic(view, 0, [10, 'red']) + }].forEach((config) => { + it(`should update ${config.name}`, () => { + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef( + NodeFlags.None, 0, 'div', null, + [ + [BindingType.ElementStyle, 'width', 'px'], + [BindingType.ElementStyle, 'color', null] + ]), + ], + config.updater)); + + checkAndUpdateView(view); + + const el = rootNodes[0]; + expect(getDOM().getStyle(el, 'width')).toBe('10px'); + expect(getDOM().getStyle(el, 'color')).toBe('red'); + }); + }); + }); + }); +} diff --git a/modules/@angular/core/test/view/embedded_view_spec.ts b/modules/@angular/core/test/view/embedded_view_spec.ts new file mode 100644 index 0000000000..56b0176ac6 --- /dev/null +++ b/modules/@angular/core/test/view/embedded_view_spec.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; +import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, attachEmbeddedView, checkAndUpdateView, checkNoChangesView, createEmbeddedView, createRootView, destroyView, detachEmbeddedView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +import {isBrowser, setupAndCheckRenderer} from './helper'; + +export function main() { + if (isBrowser()) { + defineTests({directDom: true, viewFlags: ViewFlags.DirectDom}); + } + defineTests({directDom: false, viewFlags: 0}); +} + +function defineTests(config: {directDom: boolean, viewFlags: number}) { + describe(`Embedded Views, directDom: ${config.directDom}`, () => { + setupAndCheckRenderer(config); + + let services: Services; + let renderComponentType: RenderComponentType; + + beforeEach( + inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => { + services = new DefaultServices(rootRenderer, sanitizer); + renderComponentType = + new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); + })); + + function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater, renderComponentType); + } + + function embeddedViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater); + } + + function createAndGetRootNodes( + viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, viewDef, context); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + it('should attach and detach embedded views', () => { + const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 2, 'div'), + anchorDef( + NodeFlags.HasEmbeddedViews, 0, + embeddedViewDef([elementDef(NodeFlags.None, 0, 'span', {'name': 'child0'})])), + anchorDef(NodeFlags.None, 0, embeddedViewDef([elementDef( + NodeFlags.None, 0, 'span', {'name': 'child1'})])) + ])); + + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]); + + const childView1 = createEmbeddedView(parentView, parentView.def.nodes[2]); + + const rootChildren = getDOM().childNodes(rootNodes[0]); + attachEmbeddedView(parentView.nodes[1], 0, childView0); + attachEmbeddedView(parentView.nodes[1], 1, childView1); + + // 2 anchors + 2 elements + expect(rootChildren.length).toBe(4); + expect(getDOM().getAttribute(rootChildren[1], 'name')).toBe('child0'); + expect(getDOM().getAttribute(rootChildren[2], 'name')).toBe('child1'); + + detachEmbeddedView(parentView.nodes[1], 1); + detachEmbeddedView(parentView.nodes[1], 0); + + expect(getDOM().childNodes(rootNodes[0]).length).toBe(2); + }); + + it('should include embedded views in root nodes', () => { + const {view: parentView} = createAndGetRootNodes(compViewDef([ + anchorDef( + NodeFlags.HasEmbeddedViews, 0, + embeddedViewDef([elementDef(NodeFlags.None, 0, 'span', {'name': 'child0'})])), + elementDef(NodeFlags.None, 0, 'span', {'name': 'after'}) + ])); + + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[0]); + attachEmbeddedView(parentView.nodes[0], 0, childView0); + + const rootNodes = rootRenderNodes(parentView); + expect(rootNodes.length).toBe(3); + expect(getDOM().getAttribute(rootNodes[1], 'name')).toBe('child0'); + expect(getDOM().getAttribute(rootNodes[2], 'name')).toBe('after'); + }); + + it('should dirty check embedded views', () => { + let childValue = 'v1'; + const parentContext = new Object(); + const childContext = new Object(); + const updater = jasmine.createSpy('updater').and.callFake( + (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, childValue)); + + const {view: parentView, rootNodes} = createAndGetRootNodes( + compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + anchorDef( + NodeFlags.HasEmbeddedViews, 0, + embeddedViewDef( + [elementDef( + NodeFlags.None, 0, 'span', null, + [[BindingType.ElementAttribute, 'name', SecurityContext.NONE]])], + updater)) + ]), + parentContext); + + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1], childContext); + + const rootEl = rootNodes[0]; + attachEmbeddedView(parentView.nodes[1], 0, childView0); + + checkAndUpdateView(parentView); + + expect(updater).toHaveBeenCalled(); + // component + expect(updater.calls.mostRecent().args[2]).toBe(parentContext); + // view context + expect(updater.calls.mostRecent().args[3]).toBe(childContext); + + updater.calls.reset(); + checkNoChangesView(parentView); + + expect(updater).toHaveBeenCalled(); + // component + expect(updater.calls.mostRecent().args[2]).toBe(parentContext); + // view context + expect(updater.calls.mostRecent().args[3]).toBe(childContext); + + childValue = 'v2'; + expect(() => checkNoChangesView(parentView)) + .toThrowError( + `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); + }); + + it('should destroy embedded views', () => { + const log: string[] = []; + + class ChildProvider { + ngOnDestroy() { log.push('ngOnDestroy'); }; + } + + const {view: parentView, rootNodes} = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + anchorDef(NodeFlags.HasEmbeddedViews, 0, embeddedViewDef([ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.OnDestroy, ChildProvider, []) + ])) + ])); + + const childView0 = createEmbeddedView(parentView, parentView.def.nodes[1]); + + attachEmbeddedView(parentView.nodes[1], 0, childView0); + destroyView(parentView); + + expect(log).toEqual(['ngOnDestroy']); + }); + }); +} \ No newline at end of file diff --git a/modules/@angular/core/test/view/helper.ts b/modules/@angular/core/test/view/helper.ts new file mode 100644 index 0000000000..1a77d32205 --- /dev/null +++ b/modules/@angular/core/test/view/helper.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {RootRenderer} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +export function isBrowser() { + return getDOM().supportsDOMEvents(); +} + +export function setupAndCheckRenderer(config: {directDom: boolean}) { + let rootRenderer: any; + if (config.directDom) { + beforeEach(() => { + rootRenderer = { + renderComponent: jasmine.createSpy('renderComponent') + .and.throwError('Renderer should not have been called!') + }; + TestBed.configureTestingModule( + {providers: [{provide: RootRenderer, useValue: rootRenderer}]}); + }); + afterEach(() => { expect(rootRenderer.renderComponent).not.toHaveBeenCalled(); }); + } else { + beforeEach(() => { + rootRenderer = TestBed.get(RootRenderer); + spyOn(rootRenderer, 'renderComponent').and.callThrough(); + }); + afterEach(() => { expect(rootRenderer.renderComponent).toHaveBeenCalled(); }); + } +} diff --git a/modules/@angular/core/test/view/provider_spec.ts b/modules/@angular/core/test/view/provider_spec.ts new file mode 100644 index 0000000000..c8fea5611a --- /dev/null +++ b/modules/@angular/core/test/view/provider_spec.ts @@ -0,0 +1,328 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, DoCheck, ElementRef, OnChanges, OnDestroy, OnInit, RenderComponentType, Renderer, RootRenderer, Sanitizer, SecurityContext, SimpleChange, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; +import {BindingType, DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, destroyView, elementDef, providerDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +import {isBrowser, setupAndCheckRenderer} from './helper'; + +export function main() { + if (isBrowser()) { + defineTests({directDom: true, viewFlags: ViewFlags.DirectDom}); + } + defineTests({directDom: false, viewFlags: 0}); +} + +function defineTests(config: {directDom: boolean, viewFlags: number}) { + describe(`View Providers, directDom: ${config.directDom}`, () => { + setupAndCheckRenderer(config); + + let services: Services; + let renderComponentType: RenderComponentType; + + beforeEach( + inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => { + services = new DefaultServices(rootRenderer, sanitizer); + renderComponentType = + new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); + })); + + function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater, renderComponentType); + } + + function embeddedViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater); + } + + function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, viewDef); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + describe('create', () => { + it('should create providers eagerly', () => { + let instances: SomeService[] = []; + class SomeService { + constructor() { instances.push(this); } + } + + createAndGetRootNodes(compViewDef( + [elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [])])); + + expect(instances.length).toBe(1); + }); + + describe('deps', () => { + let instance: SomeService; + class Dep {} + + class SomeService { + constructor(public dep: any) { instance = this; } + } + + beforeEach(() => { instance = null; }); + + it('should inject deps from the same element', () => { + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 2, 'span'), providerDef(NodeFlags.None, Dep, []), + providerDef(NodeFlags.None, SomeService, [Dep]) + ])); + + expect(instance.dep instanceof Dep).toBeTruthy(); + }); + + it('should inject deps from a parent element', () => { + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 3, 'span'), providerDef(NodeFlags.None, Dep, []), + elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [Dep]) + ])); + + expect(instance.dep instanceof Dep).toBeTruthy(); + }); + + it('should not inject deps from sibling root elements', () => { + const nodes = [ + elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, Dep, []), + elementDef(NodeFlags.None, 1, 'span'), providerDef(NodeFlags.None, SomeService, [Dep]) + ]; + + // root elements + expect(() => createAndGetRootNodes(compViewDef(nodes))) + .toThrowError('No provider for Dep!'); + + // non root elements + expect( + () => createAndGetRootNodes( + compViewDef([elementDef(NodeFlags.None, 4, 'span')].concat(nodes)))) + .toThrowError('No provider for Dep!'); + }); + + it('should inject from a parent elment in a parent view', () => { + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + providerDef( + NodeFlags.None, Dep, [], null, () => compViewDef([ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.None, SomeService, [Dep]) + ])), + ])); + + expect(instance.dep instanceof Dep).toBeTruthy(); + }); + + describe('builtin tokens', () => { + it('should inject ViewContainerRef', () => { + createAndGetRootNodes(compViewDef([ + anchorDef(NodeFlags.HasEmbeddedViews, 1), + providerDef(NodeFlags.None, SomeService, [ViewContainerRef]) + ])); + + expect(instance.dep.createEmbeddedView).toBeTruthy(); + }); + + it('should inject TemplateRef', () => { + createAndGetRootNodes(compViewDef([ + anchorDef(NodeFlags.None, 1, embeddedViewDef([anchorDef(NodeFlags.None, 0)])), + providerDef(NodeFlags.None, SomeService, [TemplateRef]) + ])); + + expect(instance.dep.createEmbeddedView).toBeTruthy(); + }); + + it('should inject ElementRef', () => { + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.None, SomeService, [ElementRef]) + ])); + + expect(getDOM().nodeName(instance.dep.nativeElement).toLowerCase()).toBe('span'); + }); + + if (config.directDom) { + it('should not inject Renderer when using directDom', () => { + expect(() => createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.None, SomeService, [Renderer]) + ]))) + .toThrowError('No provider for Renderer!'); + }); + } else { + it('should inject Renderer when not using directDom', () => { + createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.None, SomeService, [Renderer]) + ])); + + expect(instance.dep.createElement).toBeTruthy(); + }); + } + }); + + }); + }); + + describe('data binding', () => { + [{ + name: 'inline', + updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 1, 'v1', 'v2') + }, + { + name: 'dynamic', + updater: (updater: NodeUpdater, view: ViewData) => + updater.checkDynamic(view, 1, ['v1', 'v2']) + }].forEach((config) => { + it(`should update ${config.name}`, () => { + let instance: SomeService; + + class SomeService { + a: any; + b: any; + constructor() { instance = this; } + } + + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a'], b: [1, 'b']}) + ], + config.updater)); + + checkAndUpdateView(view); + + expect(instance.a).toBe('v1'); + expect(instance.b).toBe('v2'); + }); + }); + + it('should checkNoChanges', () => { + class SomeService { + a: any; + } + + let propValue = 'v1'; + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.None, SomeService, [], {a: [0, 'a']}) + ], + (updater, view) => updater.checkInline(view, 1, propValue))); + + checkAndUpdateView(view); + checkNoChangesView(view); + + propValue = 'v2'; + expect(() => checkNoChangesView(view)) + .toThrowError( + `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); + }); + }); + + describe('lifecycle hooks', () => { + it('should call the lifecycle hooks in the right order', () => { + let instanceCount = 0; + let log: string[] = []; + + class SomeService implements OnInit, DoCheck, OnChanges, AfterContentInit, + AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy { + id: number; + a: any; + ngOnInit() { log.push(`${this.id}_ngOnInit`); } + ngDoCheck() { log.push(`${this.id}_ngDoCheck`); } + ngOnChanges() { log.push(`${this.id}_ngOnChanges`); } + ngAfterContentInit() { log.push(`${this.id}_ngAfterContentInit`); } + ngAfterContentChecked() { log.push(`${this.id}_ngAfterContentChecked`); } + ngAfterViewInit() { log.push(`${this.id}_ngAfterViewInit`); } + ngAfterViewChecked() { log.push(`${this.id}_ngAfterViewChecked`); } + ngOnDestroy() { log.push(`${this.id}_ngOnDestroy`); } + constructor() { this.id = instanceCount++; } + } + + const allFlags = NodeFlags.OnInit | NodeFlags.DoCheck | NodeFlags.OnChanges | + NodeFlags.AfterContentInit | NodeFlags.AfterContentChecked | NodeFlags.AfterViewInit | + NodeFlags.AfterViewChecked | NodeFlags.OnDestroy; + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef(NodeFlags.None, 3, 'span'), + providerDef(allFlags, SomeService, [], {a: [0, 'a']}), + elementDef(NodeFlags.None, 1, 'span'), + providerDef(allFlags, SomeService, [], {a: [0, 'a']}) + ], + (updater) => { + updater.checkInline(view, 1, 'someValue'); + updater.checkInline(view, 3, 'someValue'); + })); + + checkAndUpdateView(view); + + // Note: After... hooks are called bottom up. + expect(log).toEqual([ + '0_ngOnChanges', + '0_ngOnInit', + '0_ngDoCheck', + '1_ngOnChanges', + '1_ngOnInit', + '1_ngDoCheck', + '1_ngAfterContentInit', + '1_ngAfterContentChecked', + '0_ngAfterContentInit', + '0_ngAfterContentChecked', + '1_ngAfterViewInit', + '1_ngAfterViewChecked', + '0_ngAfterViewInit', + '0_ngAfterViewChecked', + ]); + + log = []; + checkAndUpdateView(view); + + // Note: After... hooks are called bottom up. + expect(log).toEqual([ + '0_ngDoCheck', '1_ngDoCheck', '1_ngAfterContentChecked', '0_ngAfterContentChecked', + '1_ngAfterViewChecked', '0_ngAfterViewChecked' + ]); + + log = []; + destroyView(view); + + // Note: ngOnDestroy ist called bottom up. + expect(log).toEqual(['1_ngOnDestroy', '0_ngOnDestroy']); + }); + + it('should call ngOnChanges with the changed values and the non minified names', () => { + let changesLog: SimpleChange[] = []; + let currValue = 'v1'; + + class SomeService implements OnChanges { + a: any; + ngOnChanges(changes: {[name: string]: SimpleChange}) { + changesLog.push(changes['nonMinifiedA']); + } + } + + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.OnChanges, SomeService, [], {a: [0, 'nonMinifiedA']}) + ], + (updater) => updater.checkInline(view, 1, currValue))); + + checkAndUpdateView(view); + expect(changesLog).toEqual([new SimpleChange(undefined, 'v1', true)]); + + currValue = 'v2'; + changesLog = []; + checkAndUpdateView(view); + expect(changesLog).toEqual([new SimpleChange('v1', 'v2', false)]); + }); + }); + }); +} diff --git a/modules/@angular/core/test/view/text_spec.ts b/modules/@angular/core/test/view/text_spec.ts new file mode 100644 index 0000000000..b67ee7ea56 --- /dev/null +++ b/modules/@angular/core/test/view/text_spec.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core'; +import {DefaultServices, NodeDef, NodeFlags, NodeUpdater, Services, ViewData, ViewDefinition, ViewFlags, ViewUpdateFn, anchorDef, checkAndUpdateView, checkNoChangesView, createRootView, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index'; +import {inject} from '@angular/core/testing'; +import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; + +import {isBrowser, setupAndCheckRenderer} from './helper'; + +export function main() { + if (isBrowser()) { + defineTests({directDom: true, viewFlags: ViewFlags.DirectDom}); + } + defineTests({directDom: false, viewFlags: 0}); +} + +function defineTests(config: {directDom: boolean, viewFlags: number}) { + describe(`View Text, directDom: ${config.directDom}`, () => { + setupAndCheckRenderer(config); + + let services: Services; + let renderComponentType: RenderComponentType; + + beforeEach( + inject([RootRenderer, Sanitizer], (rootRenderer: RootRenderer, sanitizer: Sanitizer) => { + services = new DefaultServices(rootRenderer, sanitizer); + renderComponentType = + new RenderComponentType('1', 'someUrl', 0, ViewEncapsulation.None, [], {}); + })); + + function compViewDef(nodes: NodeDef[], updater?: ViewUpdateFn): ViewDefinition { + return viewDef(config.viewFlags, nodes, updater, renderComponentType); + } + + function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} { + const view = createRootView(services, viewDef); + const rootNodes = rootRenderNodes(view); + return {rootNodes, view}; + } + + describe('create', () => { + it('should create text nodes without parents', () => { + const rootNodes = createAndGetRootNodes(compViewDef([textDef(['a'])])).rootNodes; + expect(rootNodes.length).toBe(1); + expect(getDOM().getText(rootNodes[0])).toBe('a'); + }); + + it('should create views with multiple root text nodes', () => { + const rootNodes = + createAndGetRootNodes(compViewDef([textDef(['a']), textDef(['b'])])).rootNodes; + expect(rootNodes.length).toBe(2); + }); + + it('should create text nodes with parents', () => { + const rootNodes = createAndGetRootNodes(compViewDef([ + elementDef(NodeFlags.None, 1, 'div'), + textDef(['a']), + ])).rootNodes; + expect(rootNodes.length).toBe(1); + const textNode = getDOM().firstChild(rootNodes[0]); + expect(getDOM().getText(textNode)).toBe('a'); + }); + }); + + it('should checkNoChanges', () => { + let textValue = 'v1'; + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + textDef(['', '']), + ], + (updater, view) => updater.checkInline(view, 0, textValue))); + + checkAndUpdateView(view); + checkNoChangesView(view); + + textValue = 'v2'; + expect(() => checkNoChangesView(view)) + .toThrowError( + `Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`); + }); + + describe('change text', () => { + [{ + name: 'inline', + updater: (updater: NodeUpdater, view: ViewData) => updater.checkInline(view, 0, 'a', 'b') + }, + { + name: 'dynamic', + updater: (updater: NodeUpdater, view: ViewData) => + updater.checkDynamic(view, 0, ['a', 'b']) + }].forEach((config) => { + it(`should update ${config.name}`, () => { + const {view, rootNodes} = createAndGetRootNodes(compViewDef( + [ + textDef(['0', '1', '2']), + ], + config.updater)); + + checkAndUpdateView(view); + + const node = rootNodes[0]; + expect(getDOM().getText(rootNodes[0])).toBe('0a1b2'); + }); + }); + }); + + }); +} diff --git a/modules/@angular/core/test/view/view_def_spec.ts b/modules/@angular/core/test/view/view_def_spec.ts new file mode 100644 index 0000000000..1df1104613 --- /dev/null +++ b/modules/@angular/core/test/view/view_def_spec.ts @@ -0,0 +1,167 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NodeFlags, NodeUpdater, ViewData, ViewDefinition, ViewFlags, anchorDef, checkAndUpdateView, checkNoChangesView, elementDef, providerDef, textDef, viewDef} from '@angular/core/src/view/index'; + +export function main() { + describe('viewDef', () => { + describe('reverseChild order', () => { + function reverseChildOrder(viewDef: ViewDefinition): number[] { + return viewDef.reverseChildNodes.map(node => node.index); + } + + it('should reverse child order for root nodes', () => { + const vd = viewDef(ViewFlags.None, [ + textDef(['a']), // level 0, index 0 + textDef(['a']), // level 0, index 0 + ]); + + expect(reverseChildOrder(vd)).toEqual([1, 0]); + }); + + it('should reverse child order for one level, one root', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 2, 'span'), // level 0, index 0 + textDef(['a']), // level 1, index 1 + textDef(['a']), // level 1, index 2 + ]); + + expect(reverseChildOrder(vd)).toEqual([0, 2, 1]); + }); + + it('should reverse child order for 1 level, 2 roots', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 2, 'span'), // level 0, index 0 + textDef(['a']), // level 1, index 1 + textDef(['a']), // level 1, index 2 + elementDef(NodeFlags.None, 1, 'span'), // level 0, index 3 + textDef(['a']), // level 1, index 4 + ]); + + expect(reverseChildOrder(vd)).toEqual([3, 4, 0, 2, 1]); + }); + + it('should reverse child order for 2 levels', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 4, 'span'), // level 0, index 0 + elementDef(NodeFlags.None, 1, 'span'), // level 1, index 1 + textDef(['a']), // level 2, index 2 + elementDef(NodeFlags.None, 1, 'span'), // level 1, index 3 + textDef(['a']), // level 2, index 4 + ]); + + expect(reverseChildOrder(vd)).toEqual([0, 3, 4, 1, 2]); + }); + + it('should reverse child order for mixed levels', () => { + const vd = viewDef(ViewFlags.None, [ + textDef(['a']), // level 0, index 0 + elementDef(NodeFlags.None, 5, 'span'), // level 0, index 1 + textDef(['a']), // level 1, index 2 + elementDef(NodeFlags.None, 1, 'span'), // level 1, index 3 + textDef(['a']), // level 2, index 4 + elementDef(NodeFlags.None, 1, 'span'), // level 1, index 5 + textDef(['a']), // level 2, index 6 + textDef(['a']), // level 0, index 7 + ]); + + expect(reverseChildOrder(vd)).toEqual([7, 1, 5, 6, 3, 4, 2, 0]); + }); + }); + + describe('parent', () => { + function parents(viewDef: ViewDefinition): number[] { + return viewDef.nodes.map(node => node.parent); + } + + it('should calculate parents for one level', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 2, 'span'), + textDef(['a']), + textDef(['a']), + ]); + + expect(parents(vd)).toEqual([undefined, 0, 0]); + }); + + it('should calculate parents for one level, multiple roots', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 1, 'span'), + textDef(['a']), + elementDef(NodeFlags.None, 1, 'span'), + textDef(['a']), + textDef(['a']), + ]); + + expect(parents(vd)).toEqual([undefined, 0, undefined, 2, undefined]); + }); + + it('should calculate parents for multiple levels', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 2, 'span'), + elementDef(NodeFlags.None, 1, 'span'), + textDef(['a']), + elementDef(NodeFlags.None, 1, 'span'), + textDef(['a']), + textDef(['a']), + ]); + + expect(parents(vd)).toEqual([undefined, 0, 1, undefined, 3, undefined]); + }); + }); + + describe('childFlags', () => { + + function childFlags(viewDef: ViewDefinition): number[] { + return viewDef.nodes.map(node => node.childFlags); + } + + it('should calculate childFlags for one level', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, AService, []) + ]); + + expect(childFlags(vd)).toEqual([NodeFlags.AfterContentChecked, NodeFlags.None]); + }); + + it('should calculate childFlags for one level, multiple roots', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, AService, []), + elementDef(NodeFlags.None, 2, 'span'), + providerDef(NodeFlags.AfterContentInit, AService, []), + providerDef(NodeFlags.AfterViewChecked, AService, []), + ]); + + expect(childFlags(vd)).toEqual([ + NodeFlags.AfterContentChecked, NodeFlags.None, + NodeFlags.AfterContentInit | NodeFlags.AfterViewChecked, NodeFlags.None, NodeFlags.None + ]); + }); + + it('should calculate childFlags for multiple levels', () => { + const vd = viewDef(ViewFlags.None, [ + elementDef(NodeFlags.None, 2, 'span'), + elementDef(NodeFlags.None, 1, 'span'), + providerDef(NodeFlags.AfterContentChecked, AService, []), + elementDef(NodeFlags.None, 2, 'span'), + providerDef(NodeFlags.AfterContentInit, AService, []), + providerDef(NodeFlags.AfterViewInit, AService, []), + ]); + + expect(childFlags(vd)).toEqual([ + NodeFlags.AfterContentChecked, NodeFlags.AfterContentChecked, NodeFlags.None, + NodeFlags.AfterContentInit | NodeFlags.AfterViewInit, NodeFlags.None, NodeFlags.None + ]); + }); + }); + }); +} + +class AService {} diff --git a/modules/benchmarks/e2e_test/tree_perf.ts b/modules/benchmarks/e2e_test/tree_perf.ts index 872247d46d..d261881ddb 100644 --- a/modules/benchmarks/e2e_test/tree_perf.ts +++ b/modules/benchmarks/e2e_test/tree_perf.ts @@ -50,6 +50,18 @@ describe('tree benchmark perf', () => { }).then(done, done.fail); }); + it('should run for ng2 next', (done) => { + runTreeBenchmark({ + id: `deepTree.ng2.next.${worker.id}`, + url: 'all/benchmarks/src/tree/ng2_next/index.html', + ignoreBrowserSynchronization: true, + work: worker.work, + prepare: worker.prepare, + // Can't use bundles as we use non exported code + extraParams: [{name: 'bundles', value: false}] + }).then(done, done.fail); + }); + it('should run for ng2 ftl', (done) => { runTreeBenchmark({ id: `deepTree.ng2.ftl.${worker.id}`, @@ -132,16 +144,27 @@ describe('tree benchmark perf', () => { }).then(done, done.fail); }); }); + }); - it('should run ng2 changedetection', (done) => { - runTreeBenchmark({ - id: `deepTree.ng2.changedetection`, - url: 'all/benchmarks/src/tree/ng2/index.html', - work: () => $('#detectChanges').click(), - setup: () => $('#createDom').click(), - }).then(done, done.fail); - }); + it('should run ng2 changedetection', (done) => { + runTreeBenchmark({ + id: `deepTree.ng2.changedetection`, + url: 'all/benchmarks/src/tree/ng2/index.html', + work: () => $('#detectChanges').click(), + setup: () => $('#createDom').click(), + }).then(done, done.fail); + }); + it('should run ng2 next changedetection', (done) => { + runTreeBenchmark({ + id: `deepTree.ng2.next.changedetection`, + url: 'all/benchmarks/src/tree/ng2_next/index.html', + work: () => $('#detectChanges').click(), + setup: () => $('#createDom').click(), + ignoreBrowserSynchronization: true, + // Can't use bundles as we use non exported code + extraParams: [{name: 'bundles', value: false}] + }).then(done, done.fail); }); function runTreeBenchmark(config: { diff --git a/modules/benchmarks/e2e_test/tree_spec.ts b/modules/benchmarks/e2e_test/tree_spec.ts index 50713e4b8a..483dfc8c6e 100644 --- a/modules/benchmarks/e2e_test/tree_spec.ts +++ b/modules/benchmarks/e2e_test/tree_spec.ts @@ -21,7 +21,30 @@ describe('tree benchmark spec', () => { it('should work for ng2 detect changes', () => { let params = [{name: 'depth', value: 4}]; - openBrowser({url: 'all/benchmarks/src/tree/ng2/index.html'}); + openBrowser({url: 'all/benchmarks/src/tree/ng2/index.html', params}); + $('#detectChanges').click(); + expect($('#numberOfChecks').getText()).toContain('10'); + }); + + it('should work for ng2 next', () => { + testTreeBenchmark({ + url: 'all/benchmarks/src/tree/ng2_next/index.html', + ignoreBrowserSynchronization: true, + // Can't use bundles as we use non exported code + extraParams: [{name: 'bundles', value: false}] + }); + }); + + it('should work for ng2 next detect changes', () => { + let params = [ + {name: 'depth', value: 4}, + // Can't use bundles as we use non exported code + {name: 'bundles', value: false} + ]; + openBrowser({ + url: 'all/benchmarks/src/tree/ng2_next/index.html', + ignoreBrowserSynchronization: true, params + }); $('#detectChanges').click(); expect($('#numberOfChecks').getText()).toContain('10'); }); diff --git a/modules/benchmarks/src/tree/ng2_next/README.md b/modules/benchmarks/src/tree/ng2_next/README.md new file mode 100644 index 0000000000..67a126705c --- /dev/null +++ b/modules/benchmarks/src/tree/ng2_next/README.md @@ -0,0 +1,46 @@ +# Ng2 Next Benchmark + +This benchmark uses the upcoming view engine for Angular 2, which moves +more functionality from codegen into runtime to reduce generated code size. + +As we introduce more runtime code, we need to be very careful to not +regress in performance, compared to the pure codegen solution. + +## Initial resuls: size of Deep Tree Benchmark + +File size for Tree benchmark template, +view class of the component + the 2 embedded view classes (without imports nor host view factory): + + | bytes | ratio | bytes (gzip) | ratio (gzip) +------------------------------ | ----- | ----- | ------------ | ------------ +Source template + annotation | 245 | 1x | 159 | 1x +Gen code (Closure minified) | 2693 | 11.9x | 746 | 4.7x +New View Engine (minified) | 868 | 3.5x | 436 | 2.7x + +## Initial results: performance of Deep Tree Benchmark + +Measured locally on a MacBook Pro. + +BENCHMARK deepTree.... +Description: +- bundles: false +- depth: 11 +- forceGc: false +- regressionSlopeMetric: scriptTime +- sampleSize: 20 +- userAgent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36 + +...createOnly | gcAmount | gcTime | majorGcTime | pureScriptTime | renderTime | scriptTime +--------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ +ng2 | 11461.24+-21% | 12.35+-42% | 1.15+-429% | 72.49+-4% | 49.61+-4% | 82.69+-6% +ng2 next | 6207.77+-93% | 9.84+-84% | 3.35+-238% | 73.95+-4% | 49.86+-4% | 77.53+-10% + +...update | gcAmount | gcTime | majorGcTime | pureScriptTime | renderTime | scriptTime +--------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ +ng2 | 0.00 | 0.00+-435% | 0.00+-435% | 13.34+-8% | 28.55+-8% | 13.34+-8% +ng2 next | 175.02+-435% | 0.74+-435% | 0.00+-302% | 20.55+-12% | 28.00+-6% | 20.55+-12% + +...pure cd (10x) | gcAmount | gcTime | majorGcTime | pureScriptTime | renderTime | scriptTime +--------------- | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | ------------------ +ng2 | 2155.57+-238% | 0.24+-238% | 0.00+-238% | 19.32+-9% | 2.54+-6% | 19.32+-9% +ng2 next | 908.12+-366% | 1.62+-325% | 0.49+-435% | 30.66+-6% | 2.62+-19% | 30.66+-6% diff --git a/modules/benchmarks/src/tree/ng2_next/index.html b/modules/benchmarks/src/tree/ng2_next/index.html new file mode 100644 index 0000000000..69fcee27da --- /dev/null +++ b/modules/benchmarks/src/tree/ng2_next/index.html @@ -0,0 +1,36 @@ + + + + +

Params

+
+ Depth: + +
+ +
+ +

Ng2 Next Tree Benchmark

+

+ + + + + + +

+ +
+ Change detection runs: +
+
+ Loading... +
+ + + + diff --git a/modules/benchmarks/src/tree/ng2_next/index.ts b/modules/benchmarks/src/tree/ng2_next/index.ts new file mode 100644 index 0000000000..6bb0b5693d --- /dev/null +++ b/modules/benchmarks/src/tree/ng2_next/index.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ApplicationRef, NgModuleRef} from '@angular/core'; + +import {bindAction, profile} from '../../util'; +import {buildTree, emptyTree} from '../util'; + +import {AppModule, TreeComponent} from './tree'; + +export function main() { + let tree: TreeComponent; + let appMod: AppModule; + let detectChangesRuns = 0; + + function destroyDom() { + tree.data = emptyTree; + appMod.tick(); + } + + function createDom() { + tree.data = buildTree(); + appMod.tick(); + } + + function detectChanges() { + for (let i = 0; i < 10; i++) { + appMod.tick(); + } + detectChangesRuns += 10; + numberOfChecksEl.textContent = `${detectChangesRuns}`; + } + + function noop() {} + + const numberOfChecksEl = document.getElementById('numberOfChecks'); + + appMod = new AppModule(); + appMod.bootstrap(); + tree = appMod.rootComp; + const rootEl = document.querySelector('#root'); + rootEl.textContent = ''; + rootEl.appendChild(appMod.rootEl); + + bindAction('#destroyDom', destroyDom); + bindAction('#createDom', createDom); + bindAction('#detectChanges', detectChanges); + bindAction('#detectChangesProfile', profile(detectChanges, noop, 'detectChanges')); + bindAction('#updateDomProfile', profile(createDom, noop, 'update')); + bindAction('#createDomProfile', profile(createDom, destroyDom, 'create')); +} diff --git a/modules/benchmarks/src/tree/ng2_next/tree.ts b/modules/benchmarks/src/tree/ng2_next/tree.ts new file mode 100644 index 0000000000..ad5e35f67f --- /dev/null +++ b/modules/benchmarks/src/tree/ng2_next/tree.ts @@ -0,0 +1,86 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {NgIf} from '@angular/common'; +import {Component, NgModule, TemplateRef, ViewContainerRef, ViewEncapsulation} from '@angular/core'; +import {BindingType, DefaultServices, NodeFlags, NodeUpdater, ViewData, ViewDefinition, ViewFlags, anchorDef, checkAndUpdateView, createRootView, elementDef, providerDef, textDef, viewDef} from '@angular/core/src/view/index'; +import {DomSanitizer, DomSanitizerImpl, SafeStyle} from '@angular/platform-browser/src/security/dom_sanitization_service'; + +import {TreeNode, emptyTree} from '../util'; + +let trustedEmptyColor: SafeStyle; +let trustedGreyColor: SafeStyle; + +export class TreeComponent { + data: TreeNode = emptyTree; + get bgColor() { return this.data.depth % 2 ? trustedEmptyColor : trustedGreyColor; } +} + +let viewFlags = ViewFlags.DirectDom; + +const TreeComponent_Host: ViewDefinition = viewDef(viewFlags, [ + elementDef(NodeFlags.None, 1, 'tree'), + providerDef(NodeFlags.None, TreeComponent, [], null, () => TreeComponent_0), +]); + +const TreeComponent_1: ViewDefinition = viewDef( + viewFlags, + [ + elementDef(NodeFlags.None, 1, 'tree'), + providerDef(NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, () => TreeComponent_0), + ], + (updater: NodeUpdater, view: ViewData, cmp: TreeComponent) => { + updater.checkInline(view, 1, cmp.data.left); + }); + +const TreeComponent_2: ViewDefinition = viewDef( + viewFlags, + [ + elementDef(NodeFlags.None, 1, 'tree'), + providerDef(NodeFlags.None, TreeComponent, [], {data: [0, 'data']}, () => TreeComponent_0), + ], + (updater: NodeUpdater, view: ViewData, cmp: TreeComponent) => { + updater.checkInline(view, 1, cmp.data.right); + }); + +const TreeComponent_0: ViewDefinition = viewDef( + viewFlags, + [ + elementDef( + NodeFlags.None, 1, 'span', null, [[BindingType.ElementStyle, 'backgroundColor', null]]), + textDef([' ', ' ']), + anchorDef(NodeFlags.HasEmbeddedViews, 1, TreeComponent_1), + providerDef(NodeFlags.None, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), + anchorDef(NodeFlags.HasEmbeddedViews, 1, TreeComponent_2), + providerDef(NodeFlags.None, NgIf, [ViewContainerRef, TemplateRef], {ngIf: [0, 'ngIf']}), + ], + (updater: NodeUpdater, view: ViewData, cmp: TreeComponent) => { + updater.checkInline(view, 0, cmp.bgColor); + updater.checkInline(view, 1, cmp.data.value); + updater.checkInline(view, 3, cmp.data.left != null); + updater.checkInline(view, 5, cmp.data.right != null); + }); + +export class AppModule { + public rootComp: TreeComponent; + public rootEl: any; + private rootView: ViewData; + private sanitizer: DomSanitizer; + + constructor() { + this.sanitizer = new DomSanitizerImpl(); + trustedEmptyColor = this.sanitizer.bypassSecurityTrustStyle(''); + trustedGreyColor = this.sanitizer.bypassSecurityTrustStyle('grey'); + } + bootstrap() { + this.rootView = createRootView(new DefaultServices(null, this.sanitizer), TreeComponent_Host); + this.rootComp = this.rootView.nodes[1].provider; + this.rootEl = this.rootView.nodes[0].renderNode; + } + tick() { checkAndUpdateView(this.rootView); } +} diff --git a/modules/e2e_util/e2e_util.ts b/modules/e2e_util/e2e_util.ts index 6a5023e114..1defd9b5df 100644 --- a/modules/e2e_util/e2e_util.ts +++ b/modules/e2e_util/e2e_util.ts @@ -46,7 +46,7 @@ export function openBrowser(config: { const url = encodeURI(config.url + '?' + urlParams.join('&')); browser.get(url); if (config.ignoreBrowserSynchronization) { - browser.sleep(500); + browser.sleep(2000); } } diff --git a/modules/tsconfig.json b/modules/tsconfig.json index a7acf27b4b..1f3549c865 100644 --- a/modules/tsconfig.json +++ b/modules/tsconfig.json @@ -8,7 +8,8 @@ "moduleResolution": "node", "outDir": "../dist/all/", "noImplicitAny": true, - "noFallthroughCasesInSwitch": true, + // Attention: This is only set to false for @angular/core. + "noFallthroughCasesInSwitch": false, "paths": { "selenium-webdriver": ["../node_modules/@types/selenium-webdriver/index.d.ts"], "rxjs/*": ["../node_modules/rxjs/*"],