From 2f87eb52fe8afdb02f709ececee6942f8c44abe1 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Fri, 20 Jan 2017 13:10:57 -0800 Subject: [PATCH] feat(core): add initial view engine (#14014) The new view engine allows our codegen to produce less code, as it can interpret view definitions during runtime. The view engine is not feature complete yet, but already allows to implement a tree benchmark based on it. Part of #14013 --- modules/@angular/core/src/view/anchor.ts | 50 +++ modules/@angular/core/src/view/element.ts | 212 ++++++++++ modules/@angular/core/src/view/index.ts | 17 + modules/@angular/core/src/view/provider.ts | 291 +++++++++++++ modules/@angular/core/src/view/services.ts | 131 ++++++ modules/@angular/core/src/view/text.ts | 147 +++++++ modules/@angular/core/src/view/types.ts | 187 ++++++++ modules/@angular/core/src/view/util.ts | 62 +++ modules/@angular/core/src/view/view.ts | 399 ++++++++++++++++++ modules/@angular/core/src/view/view_attach.ts | 120 ++++++ .../@angular/core/test/view/anchor_spec.ts | 70 +++ .../core/test/view/component_view_spec.ts | 128 ++++++ .../@angular/core/test/view/element_spec.ts | 223 ++++++++++ .../core/test/view/embedded_view_spec.ts | 169 ++++++++ modules/@angular/core/test/view/helper.ts | 36 ++ .../@angular/core/test/view/provider_spec.ts | 328 ++++++++++++++ modules/@angular/core/test/view/text_spec.ts | 114 +++++ .../@angular/core/test/view/view_def_spec.ts | 167 ++++++++ modules/benchmarks/e2e_test/tree_perf.ts | 39 +- modules/benchmarks/e2e_test/tree_spec.ts | 25 +- .../benchmarks/src/tree/ng2_next/README.md | 46 ++ .../benchmarks/src/tree/ng2_next/index.html | 36 ++ modules/benchmarks/src/tree/ng2_next/index.ts | 56 +++ modules/benchmarks/src/tree/ng2_next/tree.ts | 86 ++++ modules/e2e_util/e2e_util.ts | 2 +- modules/tsconfig.json | 3 +- 26 files changed, 3133 insertions(+), 11 deletions(-) create mode 100644 modules/@angular/core/src/view/anchor.ts create mode 100644 modules/@angular/core/src/view/element.ts create mode 100644 modules/@angular/core/src/view/index.ts create mode 100644 modules/@angular/core/src/view/provider.ts create mode 100644 modules/@angular/core/src/view/services.ts create mode 100644 modules/@angular/core/src/view/text.ts create mode 100644 modules/@angular/core/src/view/types.ts create mode 100644 modules/@angular/core/src/view/util.ts create mode 100644 modules/@angular/core/src/view/view.ts create mode 100644 modules/@angular/core/src/view/view_attach.ts create mode 100644 modules/@angular/core/test/view/anchor_spec.ts create mode 100644 modules/@angular/core/test/view/component_view_spec.ts create mode 100644 modules/@angular/core/test/view/element_spec.ts create mode 100644 modules/@angular/core/test/view/embedded_view_spec.ts create mode 100644 modules/@angular/core/test/view/helper.ts create mode 100644 modules/@angular/core/test/view/provider_spec.ts create mode 100644 modules/@angular/core/test/view/text_spec.ts create mode 100644 modules/@angular/core/test/view/view_def_spec.ts create mode 100644 modules/benchmarks/src/tree/ng2_next/README.md create mode 100644 modules/benchmarks/src/tree/ng2_next/index.html create mode 100644 modules/benchmarks/src/tree/ng2_next/index.ts create mode 100644 modules/benchmarks/src/tree/ng2_next/tree.ts 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/*"],