diff --git a/karma-js.conf.js b/karma-js.conf.js index 1f9429f186..c7c559a491 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -57,6 +57,7 @@ module.exports = function(config) { 'dist/all/@angular/**/*node_only_spec.js', 'dist/all/@angular/benchpress/**', 'dist/all/@angular/compiler-cli/**', + 'dist/all/@angular/core/test/render3/**', 'dist/all/@angular/compiler/test/aot/**', 'dist/all/@angular/examples/**/e2e_test/*', 'dist/all/@angular/language-service/**', diff --git a/packages/core/BUILD.bazel b/packages/core/BUILD.bazel index eebf4873e6..7409716f8e 100644 --- a/packages/core/BUILD.bazel +++ b/packages/core/BUILD.bazel @@ -14,6 +14,7 @@ ng_module( module_name = "@angular/core", tsconfig = "//packages:tsconfig", deps = [ + "//packages:types", "@rxjs", ], ) diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts new file mode 100644 index 0000000000..1227347bea --- /dev/null +++ b/packages/core/src/render3/assert.ts @@ -0,0 +1,39 @@ +/** + * @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 + */ + +function stringify(value: any) { + return typeof value === 'string' ? `"${value}"` : '' + value; +} + +export function assertNumber(actual: any, condition: string) { + (typeof actual != 'number') && assertThrow(actual, 'number', condition, 'typeof =='); +} + +export function assertEqual( + actual: T, expected: T, condition: string, serializer?: ((v: T) => string)) { + (actual != expected) && assertThrow(actual, expected, condition, '==', serializer); +} + +export function assertLessThan(actual: T, expected: T, condition: string) { + (actual < expected) && assertThrow(actual, expected, condition, '>'); +} + +export function assertNotNull(actual: T, condition: string) { + assertNotEqual(actual, null, condition); +} + +export function assertNotEqual(actual: T, expected: T, condition: string) { + (actual == expected) && assertThrow(actual, expected, condition, '!='); +} + +export function assertThrow( + actual: T, expected: T, condition: string, operator: string, + serializer: ((v: T) => string) = stringify) { + throw new Error( + `ASSERT: expected ${condition} ${operator} ${serializer(expected)} but was ${serializer(actual)}!`); +} diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts new file mode 100644 index 0000000000..f5f3cdfdff --- /dev/null +++ b/packages/core/src/render3/component.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 {ComponentRef, EmbeddedViewRef, Injector} from '../core'; +import {assertNotNull} from './assert'; +import {NG_HOST_SYMBOL, createError, createViewState, directiveCreate, elementHost, enterView, leaveView} from './instructions'; +import {LElement} from './interfaces'; +import {ComponentDef, ComponentType} from './public_interfaces'; +import {RElement, Renderer3, RendererFactory3} from './renderer'; +import {stringify} from './util'; + + + +/** + * Options which control how the component should be bootstrapped. + */ +export interface CreateComponentOptionArgs { + /** + * Which renderer to use. + */ + renderer?: Renderer3; + + rendererFactory?: RendererFactory3; + + /** + * Which host element should the component be bootstrapped on. If not specified + * the component definition's `tag` is used to query the existing DOM for the + * element to bootstrap. + */ + host?: RElement|string; + + /** + * Optional Injector which is the Module Injector for the component. + */ + injector?: Injector; + + /** + * a set of features which should be applied to this component. + */ + features?: ((component: T, componentDef: ComponentDef) => void)[]; +} + + +/** + * Bootstrap a Component into an existing host element and return `ComponentRef`. + * + * @param componentType Component to bootstrap + * @param options Optional parameters which control bootstrapping + */ +export function createComponentRef( + componentType: ComponentType, opts: CreateComponentOptionArgs): ComponentRef { + const component = renderComponent(componentType, opts); + const hostView = createViewRef(detectChanges.bind(component), component); + return { + location: {nativeElement: getHostElement(component)}, + injector: opts.injector || NULL_INJECTOR, + instance: component, + hostView: hostView, + changeDetectorRef: hostView, + componentType: componentType, + destroy: function() {}, + onDestroy: function(cb: Function): void {} + }; +} + +function createViewRef(detectChanges: () => void, context: T): EmbeddedViewRef { + return addDestroyable( + { + rootNodes: null !, + // inherited from core/ChangeDetectorRef + markForCheck: () => { + if (ngDevMode) { + implement(); + } + }, + detach: () => { + if (ngDevMode) { + implement(); + } + }, + detectChanges: detectChanges, + checkNoChanges: () => { + if (ngDevMode) { + implement(); + } + }, + reattach: () => { + if (ngDevMode) { + implement(); + } + }, + }, + context); +} + +interface DestroyRef { + context: T; + destroyed: boolean; + destroy(): void; + onDestroy(cb: Function): void; +} + +function implement() { + throw new Error('NotImplemented'); +} + +function addDestroyable(obj: any, context: C): T&DestroyRef { + let destroyFn: Function[]|null = null; + obj.destroyed = false; + obj.destroy = function() { + destroyFn && destroyFn.forEach((fn) => fn()); + this.destroyed = true; + }; + obj.onDestroy = (fn: Function) => (destroyFn || (destroyFn = [])).push(fn); + return obj; +} + + +// TODO: A hack to not pull in the NullInjector from @angular/core. +export const NULL_INJECTOR: Injector = { + get: function(token: any, notFoundValue?: any) { + throw new Error('NullInjector: Not found: ' + stringify(token)); + } +}; + + +/** + * Bootstrap a Component into an existing host element and return `NgComponent`. + * + * NgComponent is a light weight Custom Elements inspired API for bootstrapping and + * interacting with bootstrapped component. + * + * @param componentType Component to bootstrap + * @param options Optional parameters which control bootstrapping + */ +export function renderComponent( + componentType: ComponentType, opts: CreateComponentOptionArgs = {}): T { + const renderer = opts.renderer || document; + const componentDef = componentType.ngComponentDef; + let component: T; + const oldView = enterView(createViewState(-1, renderer), null); + try { + elementHost(opts.host || componentDef.tag); + component = directiveCreate(0, componentDef.n(), componentDef); + } finally { + leaveView(oldView); + } + + opts.features && opts.features.forEach((feature) => feature(component, componentDef)); + detectChanges(component); + return component; +} + +export function detectChanges(component: T) { + ngDevMode && assertNotNull(component, 'component'); + const hostNode = (component as any)[NG_HOST_SYMBOL] as LElement; + if (ngDevMode && !hostNode) { + createError('Not a directive instance', component); + } + ngDevMode && assertNotNull(hostNode.data, 'hostNode.data'); + const oldView = enterView(hostNode.view !, hostNode); + try { + (component.constructor as ComponentType).ngComponentDef.r(0, 0); + isDirty = false; + } finally { + leaveView(oldView); + } +} + +let isDirty = false; +export function markDirty( + component: T, scheduler: (fn: () => void) => void = requestAnimationFrame) { + ngDevMode && assertNotNull(component, 'component'); + if (!isDirty) { + isDirty = true; + scheduler(detectChanges.bind(null, component)); + } +} + +export function getHostElement(component: T): RElement { + return ((component as any)[NG_HOST_SYMBOL] as LElement).native; +} diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts new file mode 100644 index 0000000000..10972715ff --- /dev/null +++ b/packages/core/src/render3/di.ts @@ -0,0 +1,156 @@ +/** + * @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 {ComponentFactory, ComponentRef as IComponentRef, ElementRef as IElementRef, EmbeddedViewRef as IEmbeddedViewRef, Injector, NgModuleRef as INgModuleRef, TemplateRef as ITemplateRef, Type, ViewContainerRef as IViewContainerRef, ViewRef as IViewRef} from '../core'; +import {BLOOM_SIZE, NG_ELEMENT_ID, getOrCreateNodeInjector} from './instructions'; +import {LContainer, LNodeFlags, LNodeInjector} from './interfaces'; +import {ComponentTemplate} from './public_interfaces'; +import {stringify} from './util'; + +export const enum InjectFlags { + Optional = 1 << 0, + CheckSelf = 1 << 1, + CheckParent = 1 << 2, + Default = CheckSelf | CheckParent +} + +function createError(text: string, token: any) { + return new Error(`ElementInjector: ${text} [${stringify(token)}]`); +} + +export function inject(token: Type, flags?: InjectFlags): T { + const di = getOrCreateNodeInjector(); + const bloomHash = bloomHashBit(token); + if (bloomHash === null) { + const moduleInjector = di.injector; + if (!moduleInjector) { + throw createError('NotFound', token); + } + moduleInjector.get(token); + } else { + let injector: LNodeInjector|null = di; + while (injector) { + injector = bloomFindPossibleInjector(injector, bloomHash); + if (injector) { + const node = injector.node; + const flags = node.flags; + let size = flags & LNodeFlags.SIZE_MASK; + if (size !== 0) { + size = size >> LNodeFlags.SIZE_SHIFT; + const start = flags >> LNodeFlags.INDX_SHIFT; + const directives = node.view.directives; + if (directives) { + for (let i = start, ii = start + size; i < ii; i++) { + const def = directives[(i << 1) | 1]; + if (def.diPublic && def.type == token) { + return directives[i << 1]; + } + } + } + } + injector = injector.parent; + } + } + } + throw createError('Not found', token); +} + +function bloomHashBit(type: Type): number|null { + let id: number|undefined = (type as any)[NG_ELEMENT_ID]; + return typeof id === 'number' ? id % BLOOM_SIZE : null; +} + +export function bloomFindPossibleInjector(injector: LNodeInjector, bloomBit: number): LNodeInjector| + null { + const mask = 1 << bloomBit; + let di: LNodeInjector|null = injector; + while (di) { + // See if the current injector may have the value. + let value: number = + bloomBit < 64 ? (bloomBit < 32 ? di.bf0 : di.bf1) : (bloomBit < 96 ? di.bf2 : di.bf3); + if ((value & mask) === mask) { + return di; + } + // See if the parent injectors may have the value + value = + bloomBit < 64 ? (bloomBit < 32 ? di.cbf0 : di.cbf1) : (bloomBit < 96 ? di.cbf2 : di.cbf3); + // Only go to parent if parent may have value otherwise exit. + di = (value & mask) ? di.parent : null; + } + return null; +} + + +export function injectElementRef(): IElementRef { + let di = getOrCreateNodeInjector(); + return di.elementRef || (di.elementRef = new ElementRef(di.node.native)); +} + +class ElementRef implements IElementRef { + readonly nativeElement: any; + constructor(nativeElement: any) { this.nativeElement = nativeElement; } +} + + +export function injectTemplateRef(): ITemplateRef { + let di = getOrCreateNodeInjector(); + const data = (di.node as LContainer).data; + if (data === null || data.template === null) { + throw createError('Directive not used in structural way.', null); + } + return di.templateRef || + (di.templateRef = new TemplateRef(injectElementRef(), data.template)); +} + +class TemplateRef implements ITemplateRef { + readonly elementRef: IElementRef; + + constructor(elementRef: IElementRef, template: ComponentTemplate) { + this.elementRef = elementRef; + } + + createEmbeddedView(context: T): IEmbeddedViewRef { throw notImplemented(); } +} + +export function injectViewContainerRef(): IViewContainerRef { + let di = getOrCreateNodeInjector(); + return di.viewContainerRef || (di.viewContainerRef = new ViewContainerRef(di.node as LContainer)); +} + +class ViewContainerRef implements IViewContainerRef { + element: IElementRef; + injector: Injector; + parentInjector: Injector; + + constructor(node: LContainer) {} + + clear(): void { throw notImplemented(); } + get(index: number): IViewRef|null { throw notImplemented(); } + length: number; + createEmbeddedView( + templateRef: ITemplateRef, context?: C|undefined, + index?: number|undefined): IEmbeddedViewRef { + throw notImplemented(); + } + createComponent( + componentFactory: ComponentFactory, index?: number|undefined, + injector?: Injector|undefined, projectableNodes?: any[][]|undefined, + ngModule?: INgModuleRef|undefined): IComponentRef { + throw notImplemented(); + } + insert(viewRef: IViewRef, index?: number|undefined): IViewRef { throw notImplemented(); } + move(viewRef: IViewRef, currentIndex: number): IViewRef { throw notImplemented(); } + indexOf(viewRef: IViewRef): number { throw notImplemented(); } + remove(index?: number|undefined): void { throw notImplemented(); } + detach(index?: number|undefined): IViewRef|null { throw notImplemented(); } +} + + +function notImplemented() { + return new Error('Method not implemented.'); +} diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts new file mode 100644 index 0000000000..29c71f5e85 --- /dev/null +++ b/packages/core/src/render3/index.ts @@ -0,0 +1,81 @@ +/** + * @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 {createComponentRef, detectChanges, getHostElement, markDirty, renderComponent} from './component'; +import {inject, injectElementRef, injectTemplateRef, injectViewContainerRef} from './di'; +import {ComponentDef, ComponentTemplate, ComponentType, DirectiveDef, DirectiveDefFlags, NgOnChangesFeature, PublicFeature, defineComponent, defineDirective} from './public_interfaces'; + +// Naming scheme: +// - Capital letters are for creating things: T(Text), E(Element), D(Directive), V(View), +// C(Container), L(Listener) +// - lower case letters are for binding: b(bind) +// - lower case letters are for binding target: p(property), a(attribute), k(class), s(style), +// i(input) +// - lower case letters for guarding life cycle hooks: l(lifeCycle) +// - lower case for closing: c(containerEnd), e(elementEnd), v(viewEnd) +// clang-format off +export { + LifeCycleGuard, + + NO_CHANGE as NC, + + bind as b, + bind1 as b1, + bind2 as b2, + bind3 as b3, + bind4 as b4, + bind5 as b5, + bind6 as b6, + bind7 as b7, + bind8 as b8, + bindV as bV, + + containerCreate as C, + containerEnd as c, + contentProjection as P, + + directiveCreate as D, + directiveLifeCycle as l, + distributeProjectedNodes as dP, + + elementAttribute as a, + elementClass as k, + elementCreate as E, + elementEnd as e, + elementProperty as p, + elementStyle as s, + + listenerCreate as L, + memory as m, + queryCreate as Q, + + refreshComponent as r, + refreshContainer as rC, + refreshContainerEnd as rc, + refreshQuery as rQ, + textCreate as T, + textCreateBound as t, + + viewCreate as V, + viewEnd as v, +} from './instructions'; +// clang-format on +export {QueryList} from './query'; +export {inject, injectElementRef, injectTemplateRef, injectViewContainerRef}; +export { + ComponentDef, + ComponentTemplate, + ComponentType, + DirectiveDef, + DirectiveDefFlags, + NgOnChangesFeature, + PublicFeature, + defineComponent, + defineDirective, +}; +export {createComponentRef, detectChanges, getHostElement, markDirty, renderComponent}; diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts new file mode 100644 index 0000000000..8ec317a768 --- /dev/null +++ b/packages/core/src/render3/instructions.ts @@ -0,0 +1,1578 @@ +/** + * @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 './ng_dev_mode'; + +import {Type} from '../core'; +import {assertEqual, assertLessThan, assertNotEqual, assertNotNull} from './assert'; +import {CSSSelector, ContainerState, InitialInputData, InitialInputs, LContainer, LElement, LNode, LNodeFlags, LNodeInjector, LProjection, LText, LView, MinificationData, MinificationDataValue, NodeBindings, ProjectionState, QueryState, ViewState} from './interfaces'; +import {assertNodeType} from './node_assert'; +import {appendChild, insertChild, insertView, processProjectedNode, removeView} from './node_manipulation'; +import {isNodeMatchingSelector} from './node_selector_matcher'; +import {ComponentDef, ComponentTemplate, DirectiveDef} from './public_interfaces'; +import {QueryList, QueryState_} from './query'; +import {RComment, RElement, RText, Renderer3, Renderer3Fn, Renderer3oo, RendererStyleFlags3} from './renderer'; +import {isDifferent, stringify} from './util'; + +export {refreshQuery} from './query'; + +export const enum LifeCycleGuard {ON_INIT = 1, ON_DESTROY = 2, ON_CHANGES = 4} + +export const NG_HOST_SYMBOL = '__ngHostLNode__'; + +/** + * This property gets set before entering a template. + */ +let renderer: Renderer3; + +/** Used to set the parent property when nodes are created. */ +let previousOrParentNode: LNode; + +/** + * If `isParent` is: + * - `true`: then `previousOrParentNode` points to a parent node. + * - `false`: then `previousOrParentNode` points to previous node (sibling). + */ +let isParent: boolean; + +/** + * The current template's static data (shared between all templates of a + * given type). + * + * Each node's static data is stored at the same index that it's stored + * in the nodes array. Any nodes that do not have static data store a null + * value to avoid a sparse array. + */ +let ngData: (NodeBindings | null)[]; + +/** + * State of the current view being processed. + */ +let currentView: ViewState = createViewState(null !, null !); + +let currentQuery: QueryState|null; + +/** + * This property gets set before entering a template. + */ +let creationMode: boolean; + +/** + * An array of nodes (text, element, container, etc) and their bindings + * in the current view + */ +let nodesAndBindings: any[]; + +/** + * At times it is necessary for template to store information between invocations. + * `locals` is the storage mechanism along with `memory` instruction. + * For Example: storing queries between template invocations. + */ +let locals: any[]|null; + +/** + * An array of directives in the current view + * + * even indices: contain the directive instance. + * odd indices: contain the directive def + * + * We must store the directive def (rather than token | null) + * because we need to be able to access the inputs and outputs + * of directives that aren't diPublic. + */ +let directives: any[]; + +/** + * Points to the next binding index to read or write to. + */ +let bindingIndex: number; + +/** + * When a view is destroyed, listeners need to be released + * and onDestroy callbacks need to be called. This cleanup array + * stores both listener data (in chunks of 4) and onDestroy data + * (in chunks of 2), as they'll be processed at the same time. + * + * If it's a listener being stored: + * 1st index is: event name to remove + * 2nd index is: native element + * 3rd index is: listener function + * 4th index is: useCapture boolean + * + * If it's an onDestroy function: + * 1st index is: onDestroy function + * 2nd index is: context for function + */ +let cleanup: any[]|null; + +/** + * Swap the current state with a new state. + * + * For performance reasons we store the state in the top level of the module. + * This way we minimize the number of properties to read. Whenever a new view + * is entered we have to store the state for later, and when the view is + * exited the state has to be restored + * + * @param newViewState New state to become active + * @param host Element to which the View is a child of + * @returns the previous state; + */ +export function enterView(newViewState: ViewState, host: LElement | LView | null): ViewState { + const oldViewState = currentView; + directives = newViewState.directives; + nodesAndBindings = newViewState.nodesAndBindings; + bindingIndex = newViewState.bindingStartIndex || 0; + + if (creationMode = !nodesAndBindings) { + // Absence of nodes implies creationMode. + (newViewState as{nodesAndBindings: LNode[]}).nodesAndBindings = nodesAndBindings = []; + } + cleanup = newViewState.cleanup; + renderer = newViewState.renderer; + locals = newViewState.locals; + + if (host != null) { + previousOrParentNode = host; + isParent = true; + } + + currentView = newViewState; + return oldViewState !; +} + +export const leaveView: (newViewState: ViewState) => void = enterView as any; + +export function createViewState(viewId: number, renderer: Renderer3): ViewState { + const newView = { + parent: currentView, + id: viewId, // -1 for component views + node: null !, // until we initialize it in createNode. + nodesAndBindings: null !, // Hack use as a marker for creationMode + directives: [], + cleanup: null, + renderer: renderer, + locals: null, + child: null, + tail: null, + next: null, + bindingStartIndex: null + }; + + return newView; +} + +/** + * A common way of creating the LNode to make sure that all of them have same shape to + * keep the execution code monomorphic and fast. + */ +export function createNode( + index: number | null, type: LNodeFlags.Element, native: RElement | RText | null, + viewState?: ViewState | null): LElement; +export function createNode( + index: null, type: LNodeFlags.View, native: null, viewState: ViewState): LView; +export function createNode( + index: number, type: LNodeFlags.Container, native: RComment, + containerState: ContainerState): LContainer; +export function createNode( + index: number, type: LNodeFlags.Projection, native: null, + projectionState: ProjectionState): LProjection; +export function createNode( + index: number | null, type: LNodeFlags, native: RText | RElement | RComment | null, + state?: null | ViewState | ContainerState | ProjectionState): LElement<ext&LView&LContainer& + LProjection { + const parent = isParent ? previousOrParentNode : + previousOrParentNode && previousOrParentNode.parent as LNode; + let query = (isParent ? currentQuery : previousOrParentNode && previousOrParentNode.query) || + parent && parent.query && parent.query.child(); + const isState = state != null; + const node: LElement<ext&LView&LContainer&LProjection = { + flags: type, + native: native as any, + view: currentView, + parent: parent as any, + child: null, + next: null, + nodeInjector: parent ? parent.nodeInjector : null, + data: isState ? state as any : null, + query: query, + nodeBindings: null + }; + + if ((type & LNodeFlags.ViewOrElement) === LNodeFlags.ViewOrElement && isState) { + // Bit of a hack to bust through the readonly because there is a circular dep between + // ViewState and LNode. + ngDevMode && assertEqual((state as ViewState).node, null, 'viewState.node'); + (state as ViewState as{node: LNode}).node = node; + } + if (index != null) { + // We are Element or Container + ngDevMode && + assertEqual(nodesAndBindings.length, index, 'nodesAndBindings.length not in sequence'); + nodesAndBindings[index] = node; + + // Every node adds a value to the data array to avoid a sparse array + if (ngData && index >= ngData.length) { + ngData[index] = null; + } else if (ngData) { + node.nodeBindings = ngData[index]; + } + + // Now link ourselves into the tree. + if (isParent) { + currentQuery = null; + if (previousOrParentNode.view === currentView) { + // We are in the same view, which means we are adding content node to the parent View. + ngDevMode && assertEqual(previousOrParentNode.child, null, 'previousNode.child'); + previousOrParentNode.child = node; + } else { + // We are adding component view, so we don't link parent node child to this node. + } + } else if (previousOrParentNode) { + ngDevMode && assertEqual(previousOrParentNode.next, null, 'previousNode.next'); + previousOrParentNode.next = node; + } + } + previousOrParentNode = node; + isParent = true; + return node; +} + + +////////////////////////// +//// Render +////////////////////////// + +/** + * + * @param host Existing node to render into. + * @param renderer Renderer to use. + * @param template Template function with the instructions. + * @param context to pass into the template. + */ +export function renderTemplate(host: LElement, template: ComponentTemplate, context: T) { + const hostView = host.data !; + ngDevMode && assertNotEqual(hostView, null, 'hostView'); + const oldView = enterView(hostView, host); + try { + ngData = template.ngData || (template.ngData = [] as never); + template(context, creationMode); + } finally { + leaveView(oldView); + } +} + +export const NG_ELEMENT_ID = '__NG_ELEMENT_ID__'; +export const BLOOM_SIZE = 128; +let nextNgElementId = 0; + +export function bloomAdd(di: LNodeInjector, type: Type): void { + let id: number|undefined = (type as any)[NG_ELEMENT_ID]; + if (id == null) { + id = (type as any)[NG_ELEMENT_ID] = nextNgElementId++; + } + const bloomBit = id % BLOOM_SIZE; + // JS bit operations are 32 bits + const mask = 1 << bloomBit; + if (bloomBit < 64) { + if (bloomBit < 32) { + di.bf0 |= mask; + } else { + di.bf1 |= mask; + } + } else { + if (bloomBit < 96) { + di.bf2 |= mask; + } else { + di.bf3 |= mask; + } + } +} + +export function getOrCreateNodeInjector(): LNodeInjector { + ngDevMode && assertPreviousIsParent(); + const node = previousOrParentNode as LElement | LContainer; + const nodeInjector = node.nodeInjector; + const parentInjector = node.parent && node.parent.nodeInjector; + if (nodeInjector != parentInjector) { + return nodeInjector !; + } + return node.nodeInjector = { + parent: parentInjector, + node: node, + bf0: 0, + bf1: 0, + bf2: 0, + bf3: 0, + cbf0: parentInjector == null ? 0 : parentInjector.cbf0 | parentInjector.bf0, + cbf1: parentInjector == null ? 0 : parentInjector.cbf1 | parentInjector.bf1, + cbf2: parentInjector == null ? 0 : parentInjector.cbf2 | parentInjector.bf2, + cbf3: parentInjector == null ? 0 : parentInjector.cbf3 | parentInjector.bf3, + injector: null, + templateRef: null, + viewContainerRef: null, + elementRef: null + }; +} + + +////////////////////////// +//// ELEMENT +////////////////////////// + +/** + * Create DOM element. The instruction must later be followed by `elementEnd()` call. + * + * @param index Index of the element in the nodes array + * @param nameOrComponentDef Name of the DOM Node or `ComponentDef`. + * @param attrs Statically bound set of attributes to be written into the DOM element on creation. + * + * Attributes are passed as an array of strings where elements with an even index hold an attribute + * name and elements with an odd index hold an attribute value, ex.: + * ['id', 'warning5', 'class', 'alert'] + */ +export function elementCreate( + index: number, nameOrComponentDef?: string | ComponentDef, attrs?: string[]): RElement { + let node: LElement; + let native: RElement; + + if (nameOrComponentDef == null) { + // native node retrieval - used for exporting elements as tpl local variables (
) + const node = nodesAndBindings[index] !; + native = node && (node as LElement).native; + } else { + ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex'); + const isHostElement = typeof nameOrComponentDef !== 'string'; + const name = isHostElement ? (nameOrComponentDef as ComponentDef).tag : + nameOrComponentDef as string; + if (name === null) { + // TODO: future support for nameless components. + throw 'for now name is required'; + } else { + native = renderer.createElement(name); + // Only component views should be added to the view tree directly. Embedded views are + // accessed through their containers because they may be removed / re-added later. + node = createNode( + index, LNodeFlags.Element, native, + isHostElement ? addToViewTree(createViewState(-1, renderer)) : null); + + if (node.nodeBindings == null) { + node.nodeBindings = ngData[index] = createNodeBindings(name, attrs || null); + } + + if (attrs) setUpAttributes(native, attrs); + appendChild(node.parent !, native, currentView); + } + } + return native; +} + +function setUpAttributes(native: RElement, attrs: string[]): void { + ngDevMode && assertEqual(attrs.length % 2, 0, 'attrs.length % 2'); + const isFnRenderer = (renderer as Renderer3Fn).setAttribute; + for (let i = 0; i < attrs.length; i += 2) { + isFnRenderer ? (renderer as Renderer3Fn).setAttribute !(native, attrs[i], attrs[i | 1]) : + native.setAttribute(attrs[i], attrs[i | 1]); + } +} + +export function createError(text: string, token: any) { + return new Error(`Renderer: ${text} [${stringify(token)}]`); +} + + +/** + * Used for bootstrapping existing nodes into rendering pipeline. + * + * @param elementOrSelector Render element or CSS selector to locate the element. + */ +export function elementHost(elementOrSelector: RElement | string) { + ngDevMode && assertNodesInRange(-1); + const rNode = typeof elementOrSelector === 'string' ? + ((renderer as Renderer3Fn).selectRootElement ? + (renderer as Renderer3Fn).selectRootElement(elementOrSelector) : + (renderer as Renderer3oo).querySelector !(elementOrSelector)) : + elementOrSelector; + if (ngDevMode && !rNode) { + if (typeof elementOrSelector === 'string') { + throw createError('Host node with selector not found:', elementOrSelector); + } else { + throw createError('Host node is required:', elementOrSelector); + } + } + createNode(0, LNodeFlags.Element, rNode, createViewState(-1, renderer)); +} + + +/** + * Adds an event listener to the current node. + * + * If an output exists on one of the node's directives, it also subscribes to the output + * and saves the subscription for later cleanup. + * + * @param eventName Name of the event + * @param listener The function to be called when event emits + * @param useCapture Whether or not to use capture in event listener. + */ +export function listenerCreate( + eventName: string, listener: EventListener, useCapture = false): void { + ngDevMode && assertPreviousIsParent(); + const node = previousOrParentNode; + const native = node.native as RElement; + + // In order to match current behavior, event listeners must be added for all events (including + // outputs). + if ((renderer as Renderer3Fn).listen) { + const cleanupFn = (renderer as Renderer3Fn).listen(native, eventName, listener); + (cleanup || (cleanup = currentView.cleanup = [])).push(cleanupFn, null); + } else { + native.addEventListener(eventName, listener, useCapture); + (cleanup || (cleanup = currentView.cleanup = [])).push(eventName, native, listener, useCapture); + } + + let mergeData: NodeBindings|null = node.nodeBindings !; + if (mergeData.outputs === undefined) { + // if we createNodeBindings here, inputs must be undefined so we know they still need to be + // checked + mergeData.outputs = null; + mergeData = generateMinifiedData(node.flags, mergeData); + } + + const outputs = mergeData.outputs; + let outputData: (number | string)[]|undefined; + if (outputs && (outputData = outputs[eventName])) { + outputCreate(outputData, listener); + } +} + +/** + * Iterates through the outputs associated with a particular event name and subscribes to + * each output. + */ +function outputCreate(outputs: (number | string)[], listener: Function): void { + for (let i = 0; i < outputs.length; i += 2) { + ngDevMode && assertDirectivesInRange((outputs[i] as number) << 1); + const subscription = + directives[(outputs[i] as number) << 1][outputs[i | 1]].subscribe(listener); + cleanup !.push(subscription.unsubscribe, subscription); + } +} + +/** + * Mark the end of the element. + */ +export function elementEnd() { + if (isParent) { + isParent = false; + } else { + ngDevMode && assertHasParent(); + previousOrParentNode = previousOrParentNode.parent !; + } + ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Element); + const query = previousOrParentNode.query; + query && query.add(previousOrParentNode); +} + +/** + * Update an attribute on an Element. This is used with a `bind` instruction. + * + * @param index The index of the element to update in the nodes array + * @param attrName Name of attribute. Because it is going to DOM, this is not subject to + * renaming as port of minification. + * @param value Value to write. This value will go through stringification. + */ +export function elementAttribute(index: number, attrName: string, value: any): void { + if (value !== NO_CHANGE) { + const lElement = nodesAndBindings[index] as LElement; + if (value == null) { + (renderer as Renderer3Fn).removeAttribute ? + (renderer as Renderer3Fn).removeAttribute(lElement.native, attrName) : + lElement.native.removeAttribute(attrName); + } else { + (renderer as Renderer3Fn).setAttribute ? + (renderer as Renderer3Fn).setAttribute(lElement.native, attrName, value) : + lElement.native.setAttribute(attrName, value); + } + } +} + +/** + * Update a property on an Element. + * + * If the property name also exists as an input property on one of the element's directives, + * the component property will be set instead of the element property. This check is also + * done at compile time to determine whether to generate an i() or p() instruction, but must + * be conducted at runtime as well so child components that add new @Inputs don't have to be + * re-compiled. + * + * @param index The index of the element to update in the nodes array + * @param propName Name of property. Because it is going to DOM, this is not subject to + * renaming as part of minification. + * @param value New value to write. + */ + +export function elementProperty(index: number, propName: string, value: T | NO_CHANGE): void { + if (value === NO_CHANGE) return; + const node = nodesAndBindings[index] as LElement; + + let data: NodeBindings|null = node.nodeBindings !; + // if data.inputs is undefined, a listener has created output data, but inputs haven't yet been + // checked + if (data.inputs === undefined) { + // mark inputs as checked + data.inputs = null; + data = generateMinifiedData(node.flags, data, true); + } + + const inputData = data.inputs; + let dataValue: MinificationDataValue|null; + if (inputData && (dataValue = inputData[propName])) { + setInputsForProperty(dataValue, value); + } else { + const native = node.native; + (renderer as Renderer3Fn).setProperty ? + (renderer as Renderer3Fn).setProperty(native, propName, value) : + native.setProperty ? native.setProperty(propName, value) : + (native as any)[propName] = value; + } +} + +function createNodeBindings(tagName: string, attrs: string[] | null): NodeBindings { + return {tagName, attrs, initialInputs: undefined, inputs: undefined, outputs: undefined}; +} + +/** + * Given a list of directive indices and minified input names, sets the + * input properties on the corresponding directives. + */ +function setInputsForProperty(inputs: (number | string)[], value: any): void { + for (let i = 0; i < inputs.length; i += 2) { + ngDevMode && assertDirectivesInRange(inputs[i] as number << 1); + directives[(inputs[i] as number) << 1][inputs[i | 1]] = value; + } +} + +/** + * This function consolidates all the inputs or outputs defined by directives + * on this node into one object and stores it in ngData so it can + * be shared between all templates of this type. + * + * @param index Index where data should be stored in ngData + */ +function generateMinifiedData( + flags: number, data: NodeBindings, isInputData = false): NodeBindings { + const start = flags >> LNodeFlags.INDX_SHIFT; + const size = (flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT; + + for (let i = start, ii = start + size; i < ii; i++) { + const directiveDef: DirectiveDef = directives[(i << 1) | 1]; + const minifiedPropertyMap: {[minifiedKey: string]: string} = + isInputData ? directiveDef.inputs : directiveDef.outputs; + for (let unminifiedKey in minifiedPropertyMap) { + if (minifiedPropertyMap.hasOwnProperty(unminifiedKey)) { + const minifiedKey = minifiedPropertyMap[unminifiedKey]; + const staticDirData: MinificationData = isInputData ? (data.inputs || (data.inputs = {})) : + (data.outputs || (data.outputs = {})); + const hasProperty: boolean = staticDirData.hasOwnProperty(unminifiedKey); + hasProperty ? staticDirData[unminifiedKey].push(i, minifiedKey) : + (staticDirData[unminifiedKey] = [i, minifiedKey]); + } + } + } + return data; +} + +/** + * Add or remove a class in a classList. + * + * This instruction is meant to handle the [class.foo]="exp" case + * + * @param index The index of the element to update in the nodes array + * @param className Name of class to toggle. Because it is going to DOM, this is not subject to + * renaming as part of minification. + * @param value A value indicating if a given class should be added or removed. + */ +export function elementClass(index: number, className: string, value: T | NO_CHANGE): void { + if (value !== NO_CHANGE) { + const lElement = nodesAndBindings[index] as LElement; + if (value) { + (renderer as Renderer3Fn).addClass ? + (renderer as Renderer3Fn).addClass(lElement.native, className) : + lElement.native.classList.add(className); + + } else { + (renderer as Renderer3Fn).removeClass ? + (renderer as Renderer3Fn).removeClass(lElement.native, className) : + lElement.native.classList.remove(className); + } + } +} + +/** + * Update a given style on an Element. + * + * @param index Index of the element to change in the nodes array + * @param styleName Name of property. Because it is going to DOM this is not subject to + * renaming as part of minification. + * @param value New value to write (null to remove). + * @param suffix Suffix to add to style's value (optional). + */ +export function elementStyle( + index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void { + if (value !== NO_CHANGE) { + const lElement = nodesAndBindings[index] as LElement; + if (value == null) { + (renderer as Renderer3Fn).removeStyle ? + (renderer as Renderer3Fn) + .removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) : + lElement.native.style.removeProperty(styleName); + } else { + (renderer as Renderer3Fn).setStyle ? + (renderer as Renderer3Fn) + .setStyle( + lElement.native, styleName, suffix ? stringify(value) + suffix : stringify(value), + RendererStyleFlags3.DashCase) : + lElement.native.style.setProperty( + styleName, suffix ? stringify(value) + suffix : stringify(value)); + } + } +} + + + +////////////////////////// +//// TEXT +////////////////////////// + +/** + * Create static text node + * + * @param index Index of the node in the nodes array. + * @param value Value to write. This value will be stringified. + * If value is not provided than the actual creation of the text node is delayed. + */ +export function textCreate(index: number, value?: any): void { + ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex'); + const textNode = value != null ? + ((renderer as Renderer3Fn).createText ? + (renderer as Renderer3Fn).createText(stringify(value)) : + (renderer as Renderer3oo).createTextNode !(stringify(value))) : + null; + const node = createNode(index, LNodeFlags.Element, textNode); + // Text nodes are self closing. + isParent = false; + appendChild(node.parent !, textNode, currentView); +} + +/** + * Create text node with binding + * Bindings should be handled externally with the proper bind(1-8) method + * + * @param index Index of the node in the nodes array. + * @param value Stringified value to write. + */ +export function textCreateBound(index: number, value: T | NO_CHANGE): void { + // TODO(misko): I don't think index < nodes.length check is needed here. + let existingNode = index < nodesAndBindings.length && nodesAndBindings[index] as LText; + if (existingNode && existingNode.native) { + // If DOM node exists and value changed, update textContent + value !== NO_CHANGE && + ((renderer as Renderer3Fn).setValue ? + (renderer as Renderer3Fn).setValue(existingNode.native, stringify(value)) : + existingNode.native.textContent = stringify(value)); + } else if (existingNode) { + // Node was created but DOM node creation was delayed. Create and append now. + existingNode.native = + ((renderer as Renderer3Fn).createText ? + (renderer as Renderer3Fn).createText(stringify(value)) : + (renderer as Renderer3oo).createTextNode !(stringify(value))); + insertChild(existingNode, currentView); + } else { + textCreate(index, value); + } +} + + +////////////////////////// +//// Directive +////////////////////////// + +/** + * Create or retrieve the directive. + * + * NOTE: directives can be created in order other than the index order. They can also + * be retrieved before they are created in which case the value will be null. + * + * @param index Each directive in a `View` will have a unique index. Directives can + * be created or retrieved out of order. + * @param directive The directive instance. + * @param directiveDef DirectiveDef object which contains information about the template. + */ +export function directiveCreate(index: number): T; +export function directiveCreate(index: number, directive: T, directiveDef: DirectiveDef): T; +export function directiveCreate( + index: number, directive?: T, directiveDef?: DirectiveDef): T { + let instance; + const index2 = index << 1; + if (directive == null) { + // return existing + ngDevMode && assertDirectivesInRange(index2); + instance = directives[index2]; + } else { + ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex'); + ngDevMode && assertPreviousIsParent(); + let flags = previousOrParentNode !.flags; + let size = flags & LNodeFlags.SIZE_MASK; + if (size === 0) { + flags = + (index << LNodeFlags.INDX_SHIFT) | LNodeFlags.SIZE_SKIP | flags & LNodeFlags.TYPE_MASK; + } else { + flags += LNodeFlags.SIZE_SKIP; + } + previousOrParentNode !.flags = flags; + + ngDevMode && assertDirectivesInRange(index2 - 1); + Object.defineProperty( + directive, NG_HOST_SYMBOL, {enumerable: false, value: previousOrParentNode}); + directives[index2] = instance = directive; + directives[index2 | 1] = directiveDef; + const diPublic = directiveDef !.diPublic; + if (diPublic) { + diPublic(directiveDef !); + } + const nodeBindings: NodeBindings|null = ngData && ngData[nodesAndBindings.length - 1]; + if (nodeBindings && nodeBindings.attrs) + setInputsFromAttrs(instance, directiveDef !.inputs, nodeBindings); + } + return instance; +} + +/** + * Sets initial input properties on directive instances from attribute data + * + * @param instance Instance of the directive on which to set the initial inputs + * @param inputs The list of inputs from the directive def + * @param nodeBindings The static data for this node + */ +function setInputsFromAttrs( + instance: T, inputs: {[key: string]: string}, nodeBindings: NodeBindings): void { + const directiveIndex = + ((previousOrParentNode.flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT) - 1; + + let initialInputData = nodeBindings.initialInputs as InitialInputData | undefined; + if (initialInputData === undefined || directiveIndex >= initialInputData.length) { + initialInputData = generateInitialInputs(directiveIndex, inputs, nodeBindings); + } + + const initialInputs: InitialInputs|null = initialInputData[directiveIndex]; + if (initialInputs) { + for (let i = 0; i < initialInputs.length; i += 2) { + (instance as any)[initialInputs[i]] = initialInputs[i | 1]; + } + } +} + +/** + * Generates the initialInputData for the template's static storage. + * + * @param directiveIndex Index to store the initial input data + * @param inputs The list of inputs from the directive def + * @param staticData The static data on this node + */ +function generateInitialInputs( + directiveIndex: number, inputs: {[key: string]: string}, + nodeBindings: NodeBindings): InitialInputData { + const initialInputData: InitialInputData = + nodeBindings.initialInputs || (nodeBindings.initialInputs = []); + initialInputData[directiveIndex] = null; + + const attrs = nodeBindings.attrs !; + for (let i = 0; i < attrs.length; i += 2) { + const attrName = attrs[i]; + const minifiedInputName = inputs[attrName]; + if (minifiedInputName !== undefined) { + const inputsToStore: InitialInputs = + initialInputData[directiveIndex] || (initialInputData[directiveIndex] = []); + inputsToStore.push(minifiedInputName, attrs[i | 1]); + } + } + return initialInputData; +} + +export function diPublic(def: DirectiveDef): void { + bloomAdd(getOrCreateNodeInjector(), def.type); +} + +export function directiveLifeCycle( + lifeCycle: LifeCycleGuard.ON_DESTROY, self: any, method: Function): void; +export function directiveLifeCycle(lifeCycle: LifeCycleGuard): boolean; +export function directiveLifeCycle( + lifeCycle: LifeCycleGuard, self?: any, method?: Function): boolean { + if (lifeCycle === LifeCycleGuard.ON_INIT) { + return creationMode; + } else if (lifeCycle === LifeCycleGuard.ON_DESTROY) { + (cleanup || (currentView.cleanup = cleanup = [])).push(method, self); + } + return false; +} + + +////////////////////////// +//// ViewContainer & View +////////////////////////// + +/** + * Creates an LContainer. + * + * Only `LView`s can go into `LContainer`. + * + * @param index The index of the container in the nodes array + * @param template Optional inline template + */ +export function containerCreate( + index: number, template?: ComponentTemplate, tagName?: string, attrs?: string[]): void { + ngDevMode && assertEqual(currentView.bindingStartIndex, null, 'bindingStartIndex'); + + // If the direct parent of the container is a view, its children (including its comment) + // will need to be added through insertView() when its parent view is being inserted. + // For now, it is marked "headless" so we know to append its children later. + let comment = renderer.createComment(ngDevMode ? 'container' : ''); + let renderParent: LElement|null = null; + const currentParent = isParent ? previousOrParentNode : previousOrParentNode.parent !; + ngDevMode && assertNotEqual(currentParent, null, 'currentParent'); + if (appendChild(currentParent, comment, currentView)) { + // we are adding to an Element which is either: + // - Not a component (will not be re-projected, just added) + // - View of the Component + renderParent = currentParent as LElement; + } + + const node = createNode(index, LNodeFlags.Container, comment, { + children: [], + nextIndex: 0, renderParent, + template: template == null ? null : template, + next: null, + parent: currentView + }); + + if (tagName && node.nodeBindings == null) { + node.nodeBindings = ngData[index] = createNodeBindings(tagName, attrs || null); + } + + // Containers are added to the current view tree instead of their embedded views + // because views can be removed and re-inserted. + addToViewTree(node.data); +} + +export function containerEnd() { + if (isParent) { + isParent = false; + } else { + ngDevMode && assertHasParent(); + previousOrParentNode = previousOrParentNode.parent !; + } + ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container); +} + +/** + * Sets a container up to receive views. + * + * @param index The index of the container in the nodes array + */ +export function refreshContainer(index: number): void { + ngDevMode && assertNodesInRange(index); + previousOrParentNode = nodesAndBindings[index] as LNode; + ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container); + isParent = true; + (previousOrParentNode as LContainer).data.nextIndex = 0; +} + +/** + * Marks the end of the LContainer. + * + * Marking the end of ViewContainer is the time when to child Views get inserted or removed. + */ +export function refreshContainerEnd(): void { + if (isParent) { + isParent = false; + } else { + ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.View); + ngDevMode && assertHasParent(); + previousOrParentNode = previousOrParentNode.parent !; + } + ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.Container); + const container = previousOrParentNode as LContainer; + ngDevMode && assertNodeType(container, LNodeFlags.Container); + const nextIndex = container.data.nextIndex; + while (nextIndex < container.data.children.length) { + // remove extra view. + removeView(container, nextIndex); + } +} + +/** + * Creates an LView. + * + * @param viewBlockId The ID of this view + * @return Whether or not this view is in creation mode + */ +export function viewCreate(viewBlockId: number): boolean { + const container = (isParent ? previousOrParentNode : previousOrParentNode.parent !) as LContainer; + ngDevMode && assertNodeType(container, LNodeFlags.Container); + const containerState = container.data; + const children = containerState.children; + + const existingView: LView|false = !creationMode && containerState.nextIndex < children.length && + children[containerState.nextIndex]; + let viewUpdateMode = existingView && viewBlockId === (existingView as LView).data.id; + + if (viewUpdateMode) { + previousOrParentNode = children[containerState.nextIndex++]; + ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.View); + isParent = true; + enterView((existingView as LView).data, previousOrParentNode as LView); + } else { + // When we create a new View, we always reset the state of the instructions. + const newViewState = createViewState(viewBlockId, renderer); + enterView(newViewState, createNode(null, LNodeFlags.View, null, newViewState)); + containerState.nextIndex++; + } + return !viewUpdateMode; +} + +/** + * Marks the end of the LView. + */ +export function viewEnd(): void { + isParent = false; + const viewNode = previousOrParentNode = currentView.node as LView; + const container = previousOrParentNode.parent as LContainer; + ngDevMode && assertNodeType(viewNode, LNodeFlags.View); + ngDevMode && assertNodeType(container, LNodeFlags.Container); + const containerState = container.data; + const previousView = containerState.nextIndex <= containerState.children.length ? + containerState.children[containerState.nextIndex - 1] as LView : + null; + const viewIdChanged = previousView == null ? true : previousView.data.id !== viewNode.data.id; + + if (viewIdChanged) { + insertView(container, viewNode, containerState.nextIndex - 1); + creationMode = false; + } + leaveView(currentView !.parent !); + ngDevMode && assertEqual(isParent, false, 'isParent'); + ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.View); +} +///////////// + + +export const refreshComponent: + (directiveIndex: number, elementIndex: number, template: ComponentTemplate) => + void = function( + this: undefined | {template: ComponentTemplate}, directiveIndex: number, + elementIndex: number, template: ComponentTemplate) { + ngDevMode && assertNodesInRange(elementIndex); + const element = nodesAndBindings ![elementIndex] as LElement; + ngDevMode && assertNodeType(element, LNodeFlags.Element); + ngDevMode && assertNotEqual(element.data, null, 'isComponent'); + ngDevMode && assertDirectivesInRange(directiveIndex << 1); + const hostView = element.data !; + ngDevMode && assertNotEqual(hostView, null, 'hostView'); + const directive = directives[directiveIndex << 1]; + const oldView = enterView(hostView, element); + const oldNgData = ngData; + try { + const _template = template || this !.template; + ngData = _template.ngData || (_template.ngData = [] as never); + _template(directive, creationMode); + } finally { + ngData = oldNgData; + leaveView(oldView); + } +}; + +/** + * Instruction to distribute projectable nodes among occurrences in a given template. + * It takes all the selectors from the entire component's template and decides where + * each projected node belongs (it re-distributes nodes among "buckets" where each "bucket" is + * backed by a selector). + * + * @param {CSSSelector[]} selectors + */ +export function distributeProjectedNodes(selectors?: CSSSelector[]): LNode[][] { + const noOfNodeBuckets = selectors ? selectors.length + 1 : 1; + const distributedNodes = new Array(noOfNodeBuckets); + for (let i = 0; i < noOfNodeBuckets; i++) { + distributedNodes[i] = []; + } + + const componentNode = findComponentHost(currentView); + let componentChild = componentNode.child; + + while (componentChild !== null) { + if (!selectors) { + distributedNodes[0].push(componentChild); + } else if ( + (componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element || + (componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container) { + // Only trying to match selectors against: + // - elements, excluding text nodes; + // - containers that have tagName and attributes associated. + + if (componentChild.nodeBindings) { + for (let i = 0; i < selectors !.length; i++) { + if (isNodeMatchingSelector(componentChild.nodeBindings, selectors ![i])) { + distributedNodes[i + 1].push(componentChild); + break; // first matching selector "captures" a given node + } else { + distributedNodes[0].push(componentChild); + } + } + } else { + distributedNodes[0].push(componentChild); + } + + } else if ((componentChild.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Projection) { + // we don't descent into nodes to re-project (not trying to match selectors against nodes to + // re-project) + distributedNodes[0].push(componentChild); + } + componentChild = componentChild.next; + } + + return distributedNodes; +} + +/** + * Inserts previously re-distributed projected nodes. This instruction must be preceded by a call + * to the distributeProjectedNodes instruction. + * + * @param {number} nodeIndex + * @param {number} localIndex - index under which distribution of projected nodes was memorized + * @param {number} selectorIndex - 0 means without any selector + */ +export function contentProjection( + nodeIndex: number, localIndex: number, selectorIndex: number = 0): void { + const projectedNodes: ProjectionState = []; + const node = createNode(nodeIndex, LNodeFlags.Projection, null, projectedNodes); + isParent = false; // self closing + const currentParent = node.parent; + + // re-distribution of projectable nodes is memorized on a component's view level + const componentNode = findComponentHost(currentView); + + // make sure that nodes to project were memorized + ngDevMode && assertNotNull(componentNode.data !.locals, 'componentNode.data.locals'); + const nodesForSelector = + valueInLocals(componentNode.data !.locals !, localIndex)[selectorIndex]; + + for (let i = 0; i < nodesForSelector.length; i++) { + const nodeToProject = nodesForSelector[i]; + if ((nodeToProject.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Projection) { + const previouslyProjectedNodes = (nodeToProject as LProjection).data; + for (let j = 0; j < previouslyProjectedNodes.length; j++) { + processProjectedNode( + projectedNodes, previouslyProjectedNodes[j], currentParent, currentView); + } + } else { + processProjectedNode( + projectedNodes, nodeToProject as LElement | LText | LContainer, currentParent, + currentView); + } + } +} + +/** + * Given a current view, finds the nearest component's host (LElement). + * + * @param {ViewState} viewState + * @returns {LElement} + */ +function findComponentHost(viewState: ViewState): LElement { + let viewRootLNode = viewState.node; + while ((viewRootLNode.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.View) { + ngDevMode && assertNotNull(viewState.parent, 'viewState.parent'); + viewState = viewState.parent !; + viewRootLNode = viewState.node; + } + + ngDevMode && assertNodeType(viewRootLNode, LNodeFlags.Element); + ngDevMode && assertNotNull(viewRootLNode.data, 'node.data'); + + return viewRootLNode as LElement; +} + +/** + * Adds a ViewState or a ContainerState to the end of the current + * view tree. + * + * This structure will be used to traverse through nested views + * to remove listeners and call onDestroy callbacks. + * + * @param {ViewState | ContainerState} state + */ +export function addToViewTree(state: T): T { + currentView.tail ? (currentView.tail.next = state) : (currentView.child = state); + currentView.tail = state; + return state; +} + +/** The type of the NO_CHANGE constant. */ +export type NO_CHANGE = { + brand: 'no change detected' +}; + +/** + * A special value which designates that a value has not changed. + */ +export const NO_CHANGE: NO_CHANGE = { + brand: 'no change detected' +}; + + +/** + * Create interpolation bindings with variable number of arguments. + * + * If any of the arguments change that the interpolation is concatenated + * and causes an update. + * + * @param values an array of values to diff. + */ +export function bindV(values: any[]): string|NO_CHANGE { + let different: boolean; + let parts: any[]; + if (different = creationMode) { + // make a copy of the array. + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = parts = values.slice(); + } else { + parts = nodesAndBindings[bindingIndex++]; + different = false; + for (let i = 0; i < values.length; i++) { + different = different || values[i] !== NO_CHANGE && isDifferent(values[i], parts[i]); + if (different && values[i] !== NO_CHANGE) { + parts[i] = values[i]; + } + } + } + if (different) { + let str = stringify(parts[0]); + for (let i = 1; i < parts.length; i++) { + str += stringify(parts[i]); + } + return str; + } else { + return NO_CHANGE; + } +} + +/** + * Create a single value binding without interpolation. + * + * @param value Value to diff + */ +export function bind(value: T | NO_CHANGE): T|NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = value; + } else { + if (different = value !== NO_CHANGE && isDifferent(nodesAndBindings[bindingIndex], value)) { + nodesAndBindings[bindingIndex] = value; + } + bindingIndex++; + } + return different ? value : NO_CHANGE; +} + +/** + * Create an interpolation bindings with 1 arguments. + * + * @param prefix static value used for concatenation only. + * @param value value checked for change. + * @param suffix static value used for concatenation only. + */ +export function bind1(prefix: string, value: any, suffix: string): string|NO_CHANGE { + return bind(value) === NO_CHANGE ? NO_CHANGE : prefix + stringify(value) + suffix; +} + +/** + * Create an interpolation bindings with 2 arguments. + * + * @param prefix + * @param v0 value checked for change + * @param i0 + * @param v1 value checked for change + * @param suffix + */ +export function bind2(prefix: string, v0: any, i0: string, v1: any, suffix: string): string| + NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1}; + } else { + const parts: {v0: any, v1: any} = nodesAndBindings[bindingIndex++]; + if (v0 === NO_CHANGE) v0 = parts.v0; + if (v1 === NO_CHANGE) v1 = parts.v1; + if (different = (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1))) { + parts.v0 = v0; + parts.v1 = v1; + } + } + return different ? prefix + stringify(v0) + i0 + stringify(v1) + suffix : NO_CHANGE; +} + +/** + * Create an interpolation bindings with 3 arguments. + * + * @param prefix + * @param v0 + * @param i0 + * @param v1 + * @param i1 + * @param v2 + * @param suffix + */ +export function bind3( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): string| + NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2}; + } else { + const parts: {v0: any, v1: any, v2: any} = nodesAndBindings[bindingIndex++]; + if (v0 === NO_CHANGE) v0 = parts.v0; + if (v1 === NO_CHANGE) v1 = parts.v1; + if (v2 === NO_CHANGE) v2 = parts.v2; + if (different = + (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1) || isDifferent(parts.v2, v2))) { + parts.v0 = v0; + parts.v1 = v1; + parts.v2 = v2; + } + } + return different ? prefix + stringify(v0) + i0 + stringify(v1) + i1 + stringify(v2) + suffix : + NO_CHANGE; +} + +/** + * Create an interpolation binding with 4 arguments. + * + * @param prefix + * @param v0 + * @param i0 + * @param v1 + * @param i1 + * @param v2 + * @param i2 + * @param v3 + * @param suffix + */ +export function bind4( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + suffix: string): string|NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3}; + } else { + const parts: {v0: any, v1: any, v2: any, v3: any} = nodesAndBindings[bindingIndex++]; + if (v0 === NO_CHANGE) v0 = parts.v0; + if (v1 === NO_CHANGE) v1 = parts.v1; + if (v2 === NO_CHANGE) v2 = parts.v2; + if (v3 === NO_CHANGE) v3 = parts.v3; + if (different = + (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1) || isDifferent(parts.v2, v2) || + isDifferent(parts.v3, v3))) { + parts.v0 = v0; + parts.v1 = v1; + parts.v2 = v2; + parts.v3 = v3; + } + } + return different ? + prefix + stringify(v0) + i0 + stringify(v1) + i1 + stringify(v2) + i2 + stringify(v3) + + suffix : + NO_CHANGE; +} + +/** + * Create an interpolation binding with 5 arguments. + * + * @param prefix + * @param v0 + * @param i0 + * @param v1 + * @param i1 + * @param v2 + * @param i2 + * @param v3 + * @param i3 + * @param v4 + * @param suffix + */ +export function bind5( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, suffix: string): string|NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3, v4}; + } else { + const parts: {v0: any, v1: any, v2: any, v3: any, v4: any} = nodesAndBindings[bindingIndex++]; + if (v0 === NO_CHANGE) v0 = parts.v0; + if (v1 === NO_CHANGE) v1 = parts.v1; + if (v2 === NO_CHANGE) v2 = parts.v2; + if (v3 === NO_CHANGE) v3 = parts.v3; + if (v4 === NO_CHANGE) v4 = parts.v4; + if (different = + (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1) || isDifferent(parts.v2, v2) || + isDifferent(parts.v3, v3) || isDifferent(parts.v4, v4))) { + parts.v0 = v0; + parts.v1 = v1; + parts.v2 = v2; + parts.v3 = v3; + parts.v4 = v4; + } + } + return different ? + prefix + stringify(v0) + i0 + stringify(v1) + i1 + stringify(v2) + i2 + stringify(v3) + i3 + + stringify(v4) + suffix : + NO_CHANGE; +} + +/** + * Create an interpolation binding with 6 arguments. + * + * @param prefix + * @param v0 + * @param i0 + * @param v1 + * @param i1 + * @param v2 + * @param i2 + * @param v3 + * @param i3 + * @param v4 + * @param i4 + * @param v5 + * @param suffix + */ +export function bind6( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, suffix: string): string|NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5}; + } else { + const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any} = + nodesAndBindings[bindingIndex++]; + if (v0 === NO_CHANGE) v0 = parts.v0; + if (v1 === NO_CHANGE) v1 = parts.v1; + if (v2 === NO_CHANGE) v2 = parts.v2; + if (v3 === NO_CHANGE) v3 = parts.v3; + if (v4 === NO_CHANGE) v4 = parts.v4; + if (v5 === NO_CHANGE) v5 = parts.v5; + if (different = + (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1) || isDifferent(parts.v2, v2) || + isDifferent(parts.v3, v3) || isDifferent(parts.v4, v4) || isDifferent(parts.v5, v5))) { + parts.v0 = v0; + parts.v1 = v1; + parts.v2 = v2; + parts.v3 = v3; + parts.v4 = v4; + parts.v5 = v5; + } + } + return different ? + prefix + stringify(v0) + i0 + stringify(v1) + i1 + stringify(v2) + i2 + stringify(v3) + i3 + + stringify(v4) + i4 + stringify(v5) + suffix : + NO_CHANGE; +} + +/** + * Create an interpolation binding with 7 arguments. + * + * @param prefix + * @param v0 + * @param i0 + * @param v1 + * @param i1 + * @param v2 + * @param i2 + * @param v3 + * @param i3 + * @param v4 + * @param i4 + * @param v5 + * @param i5 + * @param v6 + * @param suffix + */ +export function bind7( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, suffix: string): string| + NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6}; + } else { + const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any} = + nodesAndBindings[bindingIndex++]; + if (v0 === NO_CHANGE) v0 = parts.v0; + if (v1 === NO_CHANGE) v1 = parts.v1; + if (v2 === NO_CHANGE) v2 = parts.v2; + if (v3 === NO_CHANGE) v3 = parts.v3; + if (v4 === NO_CHANGE) v4 = parts.v4; + if (v5 === NO_CHANGE) v5 = parts.v5; + if (v6 === NO_CHANGE) v6 = parts.v6; + if (different = + (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1) || isDifferent(parts.v2, v2) || + isDifferent(parts.v3, v3) || isDifferent(parts.v4, v4) || isDifferent(parts.v5, v5) || + isDifferent(parts.v6, v6))) { + parts.v0 = v0; + parts.v1 = v1; + parts.v2 = v2; + parts.v3 = v3; + parts.v4 = v4; + parts.v5 = v5; + parts.v6 = v6; + } + } + return different ? + prefix + stringify(v0) + i0 + stringify(v1) + i1 + stringify(v2) + i2 + stringify(v3) + i3 + + stringify(v4) + i4 + stringify(v5) + i5 + stringify(v6) + suffix : + NO_CHANGE; +} + +/** + * Create an interpolation binding with 8 arguments. + * + * @param prefix + * @param v0 + * @param i0 + * @param v1 + * @param i1 + * @param v2 + * @param i2 + * @param v3 + * @param i3 + * @param v4 + * @param i4 + * @param v5 + * @param i5 + * @param v6 + * @param i6 + * @param v7 + * @param suffix + */ +export function bind8( + prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, i2: string, v3: any, + i3: string, v4: any, i4: string, v5: any, i5: string, v6: any, i6: string, v7: any, + suffix: string): string|NO_CHANGE { + let different: boolean; + if (different = creationMode) { + if (typeof currentView.bindingStartIndex !== 'number') { + bindingIndex = currentView.bindingStartIndex = nodesAndBindings.length; + } + nodesAndBindings[bindingIndex++] = + {v0: v0, v1: v1, v2: v2, v3: v3, v4: v4, v5: v5, v6: v6, v7: v7}; + } else { + const parts: {v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any, v7: any} = + nodesAndBindings[bindingIndex++]; + if (v0 === NO_CHANGE) v0 = parts.v0; + if (v1 === NO_CHANGE) v1 = parts.v1; + if (v2 === NO_CHANGE) v2 = parts.v2; + if (v3 === NO_CHANGE) v3 = parts.v3; + if (v4 === NO_CHANGE) v4 = parts.v4; + if (v5 === NO_CHANGE) v5 = parts.v5; + if (v6 === NO_CHANGE) v6 = parts.v6; + if (v7 === NO_CHANGE) v7 = parts.v7; + if (different = + (isDifferent(parts.v0, v0) || isDifferent(parts.v1, v1) || isDifferent(parts.v2, v2) || + isDifferent(parts.v3, v3) || isDifferent(parts.v4, v4) || isDifferent(parts.v5, v5) || + isDifferent(parts.v6, v6))) { + parts.v0 = v0; + parts.v1 = v1; + parts.v2 = v2; + parts.v3 = v3; + parts.v4 = v4; + parts.v5 = v5; + parts.v6 = v6; + parts.v7 = v7; + } + } + return different ? + prefix + stringify(v0) + i0 + stringify(v1) + i1 + stringify(v2) + i2 + stringify(v3) + i3 + + stringify(v4) + i4 + stringify(v5) + i5 + stringify(v6) + i6 + stringify(v7) + suffix : + NO_CHANGE; +} + +export function memory(index: number, value?: T): T { + const _locals = locals || (locals = currentView.locals = []); + return valueInLocals(_locals, index, value); +} + +function valueInLocals(locals: any[], index: number, value?: T): T { + ngDevMode && assertLocalsInRange(locals, index); + if (value === undefined) { + value = locals[index]; + } else { + locals[index] = value; + } + return value !; +} + +export function queryCreate(predicate: Type| any[], descend?: boolean): QueryList { + ngDevMode && assertPreviousIsParent(); + const queryList = new QueryList(); + const query = currentQuery || (currentQuery = new QueryState_()); + query.track(queryList, predicate, descend); + return queryList; +} + + + +function assertPreviousIsParent() { + assertEqual(isParent, true, 'isParent'); +} + +function assertHasParent() { + assertNotEqual(previousOrParentNode.parent, null, 'isParent'); +} + +function assertLocalsInRange(locals: any[], index: number) { + assertLessThan(locals ? locals.length : 0, index, 'locals.length'); +} + +function assertNodesInRange(index: number) { + assertLessThan(nodesAndBindings ? nodesAndBindings.length : 0, index, 'nodes.length'); +} + +function assertDirectivesInRange(index: number) { + assertLessThan(directives ? directives.length : 0, index, 'directives.length'); +} diff --git a/packages/core/src/render3/interfaces.ts b/packages/core/src/render3/interfaces.ts new file mode 100644 index 0000000000..09ec73492c --- /dev/null +++ b/packages/core/src/render3/interfaces.ts @@ -0,0 +1,603 @@ +/** + * @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 {ElementRef, Injector, QueryList, TemplateRef, Type, ViewContainerRef} from '../core'; +import {ComponentTemplate} from './public_interfaces'; +import {RComment, RElement, RText, Renderer3} from './renderer'; + +declare global { + const ngDevMode: boolean; +} + +export const enum LNodeFlags { + Container = 0b00, + Projection = 0b01, + View = 0b10, + Element = 0b11, + ViewOrElement = 0b10, + SIZE_SKIP = 0b100, + SIZE_SHIFT = 2, + INDX_SHIFT = 12, + TYPE_MASK = 0b00000000000000000000000000000011, + SIZE_MASK = 0b00000000000000000000111111111100, + INDX_MASK = 0b11111111111111111111000000000000, +} + +/** + * NOTES: + * + * Each Array costs 70 bytes and is composed of `Array` and `(array)` object + * - `Array` javascript visible object: 32 bytes + * - `(array)` VM object where the array is actually stored in: 38 bytes + * + * Each Object cost is 24 bytes plus 8 bytes per property. + * + * For small arrays, it is more efficient to store the data as a linked list + * of items rather than small arrays. However, the array access is faster as + * shown here: https://jsperf.com/small-arrays-vs-linked-objects + */ + + +/** + * `ViewState` stores all of the information needed to process the instructions as + * they are invoked from the template. `ViewState` is saved when a child `View` is + * being processed and restored when the child `View` is done. + * + * Keeping separate state for each view facilities view insertion / deletion, so we + * don't have to edit the nodes array or directives array based on which views + * are present. + */ +export interface ViewState { + /** + * The parent view is needed when we exit the view and must restore the previous + * `ViewState`. Without this, the render method would have to keep a stack of + * views as it is recursively rendering templates. + */ + readonly parent: ViewState|null; + + /** + * Pointer to the `LView` node which represents the root of the view. We + * need this to be able to efficiently find the `LView` when inserting the + * view into an anchor. + */ + readonly node: LView|LElement; + + /** + * ID to determine whether this view is the same as the previous view + * in this position. If it's not, we know this view needs to be inserted + * and the one that exists needs to be removed (e.g. if/else statements) + */ + readonly id: number; + + /** + * Renderer to be used for this view. + */ + readonly renderer: Renderer3; + + /** + * This array stores all element/text/container nodes created inside this view + * and their bindings. Stored as an array rather than a linked list so we can + * look up nodes directly in the case of forward declaration or bindings + * (e.g. E(1)).. + * + * All bindings for a given view are stored in the order in which they + * appear in the template, starting with `bindingStartIndex`. + * We use `bindingIndex` to internally keep track of which binding + * is currently active. + * + * NOTE: We also use nodes == null as a marker for creationMode. We + * do this by creating ViewState in incomplete state with nodes == null + * and we initialize it on first run. + */ + readonly nodesAndBindings: any[]; + + /** + * All directives created inside this view. Stored as an array + * rather than a linked list so we can look up directives directly + * in the case of forward declaration or DI. + * + * The array alternates between instances and directive tokens. + * - even indices: contain the directive token (type) + * - odd indices: contain the directive def + * + * We must store the directive def (rather than token | null) + * because we need to be able to access the inputs and outputs + * of directives that aren't diPublic. + */ + readonly directives: any[]; + + /** + * The binding start index is the index at which the nodes array + * starts to store bindings only. Saving this value ensures that we + * will begin reading bindings at the correct point in the array when + * we are in update mode. + */ + bindingStartIndex: number|null; + + /** + * When a view is destroyed, listeners need to be released + * and onDestroy callbacks need to be called. This cleanup array + * stores both listener data (in chunks of 4) and onDestroy data + * (in chunks of 2), as they'll be processed at the same time. + * + * If it's a listener being stored: + * 1st index is: event name to remove + * 2nd index is: native element + * 3rd index is: listener function + * 4th index is: useCapture boolean + * + * If it's an onDestroy function: + * 1st index is: onDestroy function + * 2nd index is; context for function + */ + cleanup: any[]|null; + + /** + * Necessary so views can traverse through their nested views + * to remove listeners and call onDestroy callbacks. + * + * For embedded views, we store the container rather than the + * first view to avoid managing splicing when views are added/removed. + */ + child: ViewState|ContainerState|null; + + /** + * The tail allows us to quickly add a new state to the end of the + * view list without having to propagate starting from the first child. + */ + tail: ViewState|ContainerState|null; + + /** + * Allows us to propagate between view states. + * + * Embedded views already have a node.next, but it is only set for views + * in the same container. We need a way to link component views as well. + */ + next: ViewState|ContainerState|null; + + locals: any[]|null; +} + +export interface LNodeInjector { + /** + * We need to store a reference to the injector's parent so DI can keep looking up + * the injector tree until it finds the dependency it's looking for. + */ + readonly parent: LNodeInjector|null; + + /** + * Allows access to the directives array in that node's view and to + * the node's flags (for starting directive index and directive size). Necessary + * for DI to retrieve a directive from the directives array if injector indicates + * it is there. + */ + readonly node: LElement|LContainer; + + /** + * The following bloom filter determines whether a directive is available + * on the associated node or not. This prevents us from searching the directives + * array at this level unless it's probable the directive is in it. + * + * - bf0: Check directive IDs 0-31 (IDs are % 128) + * - bf1: Check directive IDs 33-63 + * - bf2: Check directive IDs 64-95 + * - bf3: Check directive IDs 96-127 + */ + bf0: number; + bf1: number; + bf2: number; + bf3: number; + + /** + * cbf0 - cbf3 properties determine whether a directive is available through a + * parent injector. They refer to the merged values of parent bloom filters. This + * allows us to skip looking up the chain unless it's probable that directive exists + * up the chain. + */ + cbf0: number; + cbf1: number; + cbf2: number; + cbf3: number; + injector: Injector|null; + + /** Stores the TemplateRef so subsequent injections of the TemplateRef get the same instance. */ + templateRef: TemplateRef|null; + + /** Stores the ViewContainerRef so subsequent injections of the ViewContainerRef get the same + * instance. */ + viewContainerRef: ViewContainerRef|null; + + /** Stores the ElementRef so subsequent injections of the ElementRef get the same instance. */ + elementRef: ElementRef|null; +} + +/** + * LNode is an internal data structure which is used for the incremental DOM algorithm. + * + * The data structure is optimized for speed and size. + * + * In order to be fast, all subtypes of `LNode` should have the same shape. + * Because size of the `LNode` matters, many fields have multiple roles depending + * on the `LNode` subtype. + * + * NOTE: This is a private data structure and should not be exported by any of the + * instructions. + */ +export interface LNode { + /** + * This number stores three values using its bits: + * + * - the type of the node (first 2 bits) + * - the number of directives on that node (next 10 bits) + * - the starting index of the node's directives in the directives array (last 20 bits). + * + * The latter two values are necessary so DI can effectively search the directives associated + * with a node without searching the whole directives array. + */ + flags: LNodeFlags; + + /** + * The associated DOM node. Storing this allows us to: + * - append children to their element parents in the DOM (e.g. `parent.native.appendChild(...)`) + * - retrieve the sibling elements of text nodes whose creation / insertion has been delayed + * - mark locations where child views should be inserted (for containers) + */ + readonly native: RElement|RText|RComment|null; + + /** + * We need a reference to a node's parent so we can append the node to its parent's native + * element at the appropriate time. + */ + readonly parent: LNode|null; + + /** + * First child of the current node. + */ + child: LNode|null; + + /** + * The next sibling node. Necessary so we can propagate through the root nodes of a view + * to insert them or remove them from the DOM. + */ + next: LNode|null; + + /** + * If ViewState, then `data` contains lightDOM. + * If LContainer, then `data` contains ContainerState + */ + readonly data: ViewState|ContainerState|ProjectionState|null; + + + /** + * Each node belongs to a view. + * + * When the injector is walking up a tree, it needs access to the `directives` (part of view). + */ + readonly view: ViewState; + + /** The injector associated with this node. Necessary for DI. */ + nodeInjector: LNodeInjector|null; + + /** + * Optional `QueryState` used for tracking queries. + * + * If present the node creation/updates are reported to the `QueryState`. + */ + query: QueryState|null; + + /** + * Pointer to the corresponding NodeBindings object, which stores static + * data about this node. + */ + nodeBindings: NodeBindings|null; +} + +/** + * Used for tracking queries. + */ +export interface QueryState { + /** + * Used to ask query if it should be cloned to the child element. + * + * For example in the case of deep queries the `child()` returns + * query for the child node. In case of shallow queries it returns + * `null`. + */ + child(): QueryState|null; + + /** + * Notify `QueryState` that a `LNode` has been created. + */ + add(node: LNode): void; + + /** + * Notify `QueryState` that a `LView` has been added to `LContainer`. + */ + insert(container: LContainer, view: LView, insertIndex: number): void; + + /** + * Notify `QueryState` that a `LView` has been removed from `LContainer`. + */ + remove(container: LContainer, view: LView, removeIndex: number): void; + + /** + * Add additional `QueryList` to track. + * + * @param queryList `QueryList` to update with changes. + * @param predicate Either `Type` or selector array of [key, value] predicates. + * @param descend If true the query will recursively apply to the children. + */ + track(queryList: QueryList, predicate: Type|any[], descend?: boolean): void; +} + +/** The state associated with an LContainer */ +export interface ContainerState { + /** + * The next active index in the children array to read or write to. This helps us + * keep track of where we are in the children array. + */ + nextIndex: number; + + /** + * This allows us to jump from a container to a sibling container or + * component view with the same parent, so we can remove listeners efficiently. + */ + next: ViewState|ContainerState|null; + + /** + * Access to the parent view is necessary so we can propagate back + * up from inside a container to parent.next. + */ + parent: ViewState|null; + + /** + * A list of the container's currently active child views. Views will be inserted + * here as they are added and spliced from here when they are removed. We need + * to keep a record of current views so we know which views are already in the DOM + * (and don't need to be re-added) and so we can remove views from the DOM when they + * are no longer required. + */ + readonly children: LView[]; + + /** + * Parent Element which will contain the location where all of the Views will be + * inserted into to. + * + * If `renderParent` is `null` it is headless. This means that it is contained + * in another `LView` which in turn is contained in another `LContainer` and therefore + * it does not yet have its own parent. + * + * If `renderParent` is not `null` than it may be: + * - same as `LContainer.parent` in which case it is just a normal container. + * - different from `LContainer.parent` in which case it has been re-projected. + * In other words `LContainer.parent` is logical parent where as + * `ContainerState.projectedParent` is render parent. + * + * When views are inserted into `LContainer` than `renderParent` is: + * - `null`, we are in `LView` keep going up a hierarchy until actual + * `renderParent` is found. + * - not `null`, than use the `projectedParent.native` as the `RElement` to insert + * `LView`s into. + */ + renderParent: LElement|null; + + /** + * The template extracted from the location of the Container. + */ + readonly template: ComponentTemplate|null; +} + +/** + * This mapping is necessary so we can set input properties and output listeners + * properly at runtime when property names are minified. + * + * Key: original unminified input or output name + * Value: array containing minified name and related directive index + * + * The value must be an array to support inputs and outputs with the same name + * on the same node. + */ +export type MinificationData = { + [key: string]: MinificationDataValue +}; + +/** + * The value in MinificationData objects. + * + * In each array: + * Even indices: directive index + * Odd indices: minified name + * + * e.g. [0, 'change-minified'] + */ +export type MinificationDataValue = (number | string)[]; + + +/** + * This array contains information about input properties that + * need to be set once from attribute data. It's ordered by + * directive index (relative to element) so it's simple to + * look up a specific directive's initial input data. + * + * Within each sub-array: + * + * Even indices: minified input name + * Odd indices: initial value + * + * If a directive on a node does not have any input properties + * that should be set from attributes, its index is set to null + * to avoid a sparse array. + * + * e.g. [null, ['role-min', 'button']] + */ +export type InitialInputData = (InitialInputs | null)[]; + +/** + * Used by InitialInputData to store input properties + * that should be set once from attributes. + * + * Even indices: minified input name + * Odd indices: initial value + * + * e.g. ['role-min', 'button'] + */ +export type InitialInputs = string[]; + +/** + * LNode binding data for a particular node that is shared between all templates + * of a specific type. + * + * If a property is: + * - Minification Data: that property's data was generated and this is it + * - Null: that property's data was already generated and nothing was found. + * - Undefined: that property's data has not yet been generated + */ +export interface NodeBindings { + /** The tag name associated with this node. */ + tagName: string|null; + + /** + * Static attributes associated with an element. We need to store + * static attributes to support content projection with selectors. + * Attributes are stored statically because reading them from the DOM + * would be way too slow for content projection and queries. + * + * Since attrs will always be calculated first, they will never need + * to be marked undefined by other instructions. + */ + attrs: string[]|null; + + /** + * This property contains information about input properties that + * need to be set once from attribute data. + */ + initialInputs: InitialInputData|null|undefined; + + /** Input data for all directives on this node. */ + inputs: MinificationData|null|undefined; + + /** Output data for all directives on this node. */ + outputs: MinificationData|null|undefined; +} + +/** Interface necessary to work with view tree traversal */ +export interface ViewOrContainerState { + next: ViewState|ContainerState|null; + child?: ViewState|ContainerState|null; + children?: LView[]; + parent: ViewState|null; +} + +/** LNode representing an element. */ +export interface LElement extends LNode { + /** The DOM element associated with this node. */ + readonly native: RElement; + + child: LContainer|LElement|LText|LProjection|null; + next: LContainer|LElement|LText|LProjection|null; + + /** If Component than data has ViewState (light DOM) */ + readonly data: ViewState|null; + + /** LElement nodes can be inside other LElement nodes or inside LViews. */ + readonly parent: LElement|LView; +} + +/** LNode representing a #text node. */ +export interface LText extends LNode { + /** The text node associated with this node. */ + native: RText; + child: null; + next: LContainer|LElement|LText|LProjection|null; + + /** LText nodes can be inside LElement nodes or inside LViews. */ + readonly parent: LElement|LView; + readonly data: null; +} + +/** + * Abstract node which contains root nodes of a view. + */ +export interface LView extends LNode { + readonly native: null; + child: LContainer|LElement|LText|LProjection|null; + next: LView|null; + + /** LView nodes can only be added to LContainers. */ + readonly parent: LContainer|null; + readonly data: ViewState; +} + +/** + * Abstract node container which contains other views. + */ +export interface LContainer extends LNode { + /** + * This comment node is appended to the container's parent element to mark where + * in the DOM the container's child views should be added. + * + * If the container is a root node of a view, this comment will not be appended + * until the parent view is processed. + */ + readonly native: RComment; + readonly data: ContainerState; + child: null; + next: LContainer|LElement|LText|LProjection|null; + + /** Containers can be added to elements or views. */ + readonly parent: LElement|LView|null; +} + +/** + * A projection state is just an array of projected nodes. + * + * It would be nice if we could not need an array, but since a projected note can be + * re-projected, the same node can be part of more than one LProjection which makes + * list approach not possible. + */ +export type ProjectionState = Array; + +export interface LProjection extends LNode { + readonly native: null; + child: null; + next: LContainer|LElement|LText|LProjection|null; + + readonly data: ProjectionState; + + /** Projections can be added to elements or views. */ + readonly parent: LElement|LView; +} + +/** + * Parsed selector in the following format: + * [tagName, attr1Name, attr1Val, ..., attrnName, attrnValue, 'class', className1, className2, ..., + * classNameN] + * + * * For example, given the following selector: + * `div.foo.bar[attr1=val1][attr2]` a parsed format would be: + * `['div', 'attr1', 'val1', 'attr2', '', 'class', 'foo', 'bar']`. + * + * Things to notice: + * - tag name is always at the position 0 + * - the `class` attribute is always the last attribute in a pre-parsed array + * - class names in a selector are at the end of an array (after the attribute with the name + * 'class'). + */ +export type SimpleCSSSelector = string[]; + +/** + * A complex selector expressed as an Array where: + * - element at index 0 is a selector (SimpleCSSSelector) to match + * - elements at index 1..n is a selector (SimpleCSSSelector) that should NOT match + */ +export type CSSSelectorWithNegations = [SimpleCSSSelector | null, SimpleCSSSelector[] | null]; + +/** + * A collection of complex selectors (CSSSelectorWithNegations) in a parsed form + */ +export type CSSSelector = CSSSelectorWithNegations[]; diff --git a/packages/core/src/render3/ng_dev_mode.ts b/packages/core/src/render3/ng_dev_mode.ts new file mode 100644 index 0000000000..b10ea4209c --- /dev/null +++ b/packages/core/src/render3/ng_dev_mode.ts @@ -0,0 +1,15 @@ +/** + * @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 + */ + +if (typeof ngDevMode == 'undefined') { + if (typeof window != 'undefined') (window as any).ngDevMode = true; + if (typeof self != 'undefined') (self as any).ngDevMode = true; + if (typeof global != 'undefined') (global as any).ngDevMode = true; +} + +export const _ngDevMode = true; diff --git a/packages/core/src/render3/node_assert.ts b/packages/core/src/render3/node_assert.ts new file mode 100644 index 0000000000..46aa989c29 --- /dev/null +++ b/packages/core/src/render3/node_assert.ts @@ -0,0 +1,24 @@ +/** + * @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 {assertEqual, assertNotEqual} from './assert'; +import {LNode, LNodeFlags} from './interfaces'; + +export function assertNodeType(node: LNode, type: LNodeFlags) { + assertNotEqual(node, null, 'node'); + assertEqual(node.flags & LNodeFlags.TYPE_MASK, type, 'Node.type', typeSerializer); +} + + +function typeSerializer(type: LNodeFlags): string { + if (type == LNodeFlags.Projection) return 'Projection'; + if (type == LNodeFlags.Container) return 'Container'; + if (type == LNodeFlags.View) return 'View'; + if (type == LNodeFlags.Element) return 'Element'; + return '??? ' + type + ' ???'; +} diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts new file mode 100644 index 0000000000..1ab6af45b0 --- /dev/null +++ b/packages/core/src/render3/node_manipulation.ts @@ -0,0 +1,293 @@ +/** + * @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 {assertNotNull} from './assert'; +import {ContainerState, LContainer, LElement, LNode, LNodeFlags, LProjection, LText, LView, ProjectionState, ViewOrContainerState, ViewState} from './interfaces'; +import {assertNodeType} from './node_assert'; +import {RComment, RElement, RNode, RText, Renderer3Fn} from './renderer'; + +export function findNativeParent(containerNode: LContainer): RNode|null { + let container: LContainer|null = containerNode; + while (container) { + ngDevMode && assertNodeType(container, LNodeFlags.Container); + const renderParent = container.data.renderParent; + if (renderParent !== null) { + return renderParent.native; + } + const viewOrElement: LView|LElement = container.parent !; + ngDevMode && assertNotNull(viewOrElement, 'container.parent'); + if ((viewOrElement.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element) { + // we are an LElement, which means we are past the last LContainer. + // This means than we have not been projected so just ignore. + return null; + } + ngDevMode && assertNodeType(viewOrElement, LNodeFlags.View); + container = (viewOrElement as LView).parent; + } + return null; +} + +export function findBeforeNode(index: number, state: ContainerState, native: RComment): RElement| + RText|RComment { + const children = state.children; + // Find the node to insert in front of + return index + 1 < children.length ? + (children[index + 1].data.nodesAndBindings[0] as LText | LElement | LContainer).native : + native; +} + +export function addRemoveViewFromContainer( + container: LContainer, rootNode: LView, insertMode: true, beforeNode: RNode | null): void; +export function addRemoveViewFromContainer( + container: LContainer, rootNode: LView, insertMode: false): void; +export function addRemoveViewFromContainer( + container: LContainer, rootNode: LView, insertMode: boolean, beforeNode?: RNode | null): void { + ngDevMode && assertNodeType(container, LNodeFlags.Container); + ngDevMode && assertNodeType(rootNode, LNodeFlags.View); + const parent = findNativeParent(container); + let node: LNode|null = rootNode.data.nodesAndBindings[0]; + if (parent) { + while (node) { + const type = node.flags & LNodeFlags.TYPE_MASK; + let nextNode: LNode|null = null; + const renderer = container.view.renderer; + const isFnRenderer = (renderer as Renderer3Fn).listen; + if (type === LNodeFlags.Element) { + insertMode ? + (isFnRenderer ? + (renderer as Renderer3Fn) + .insertBefore !(parent, node.native !, beforeNode as RNode | null) : + parent.insertBefore(node.native !, beforeNode as RNode | null, true)) : + (isFnRenderer ? + (renderer as Renderer3Fn).removeChild !(parent as RElement, node.native !) : + parent.removeChild(node.native !)); + nextNode = node.next; + } else if (type === LNodeFlags.Container) { + // if we get to a container, it must be a root node of a view because we are only + // propagating down into child views / containers and not child elements + const childContainerData: ContainerState = (node as LContainer).data; + insertMode ? + (isFnRenderer ? + (renderer as Renderer3Fn).appendChild !(parent as RElement, node.native !) : + parent.appendChild(node.native !)) : + (isFnRenderer ? + (renderer as Renderer3Fn).removeChild !(parent as RElement, node.native !) : + parent.removeChild(node.native !)); + nextNode = childContainerData.children.length ? + childContainerData.children[0].data.nodesAndBindings[0] : + null; + } else if (type === LNodeFlags.Projection) { + nextNode = (node as LProjection).data[0]; + } else { + nextNode = (node as LView).data.nodesAndBindings[0]; + } + if (nextNode === null) { + while (node && !node.next) { + node = node.parent; + if (node === rootNode) node = null; + } + node = node && node.next; + } else { + node = nextNode; + } + } + } +} + +/** + * Traverses the tree of component views and containers to remove listeners. + * + * Notes: + * - Will be used for onDestroy calls later, so needs to be bottom-up. + * - Must process containers instead of their views to avoid splicing + * when views are destroyed and re-added. + * - Using a while loop because it's faster than recursing + * - Destroy only called on movement to sibling or movement to parent (laterally or up) + */ +export function destroyViewTree(rootView: ViewState): void { + let viewOrContainerState: ViewOrContainerState|null = rootView; + + while (viewOrContainerState) { + let next: ViewOrContainerState|null = null; + + if (viewOrContainerState.children && viewOrContainerState.children.length) { + next = viewOrContainerState.children[0].data; + } else if (viewOrContainerState.child) { + next = viewOrContainerState.child; + } else if (viewOrContainerState.next) { + cleanUpView(viewOrContainerState as ViewState); + next = viewOrContainerState.next; + } + + if (next == null) { + while (viewOrContainerState && !viewOrContainerState !.next) { + cleanUpView(viewOrContainerState as ViewState); + viewOrContainerState = getParentState(viewOrContainerState, rootView); + } + cleanUpView(viewOrContainerState as ViewState || rootView); + + next = viewOrContainerState && viewOrContainerState.next; + } + viewOrContainerState = next; + } +} + +export function insertView(container: LContainer, newView: LView, index: number): LView { + const state = container.data; + const children = state.children; + + if (index > 0) { + // This is a new view, we need to add it to the children. + setViewNext(children[index - 1], newView); + } + + if (index < children.length && children[index].data.id !== newView.data.id) { + // View ID change replace the view. + setViewNext(newView, children[index]); + children.splice(index, 0, newView); + } else if (index >= children.length) { + children.push(newView); + } + + if (state.nextIndex <= index) { + state.nextIndex++; + } + + // If the container's renderParent is null, we know that it is a root node of its own parent view + // and we should wait until that parent processes its nodes (otherwise, we will insert this view's + // nodes twice - once now and once when its parent inserts its views). + if (container.data.renderParent !== null) { + addRemoveViewFromContainer( + container, newView, true, findBeforeNode(index, state, container.native)); + } + + // Notify query that view has been inserted + container.query && container.query.insert(container, newView, index); + return newView; +} + + +export function removeView(container: LContainer, removeIndex: number): LView { + const children = container.data.children; + const viewNode = children[removeIndex]; + if (removeIndex > 0) { + setViewNext(children[removeIndex - 1], viewNode.next); + } + children.splice(removeIndex, 1); + destroyViewTree(viewNode.data); + addRemoveViewFromContainer(container, viewNode, false); + // Notify query that view has been removed + container.query && container.query.remove(container, viewNode, removeIndex); + return viewNode; +} + +export function setViewNext(view: LView, next: LView | null): void { + view.next = next; + view.data.next = next ? next.data : null; +} + +export function getParentState( + state: ViewOrContainerState, rootView: ViewState): ViewOrContainerState|null { + let node; + if ((node = (state as ViewState) !.node) && + (node.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.View) { + // if it's an embedded view, the state needs to go up to the container, in case the + // container has a next + return node.parent !.data as any; + } else { + // otherwise, use parent view for containers or component views + return state.parent === rootView ? null : state.parent; + } +} + +/** Removes all listeners and call all onDestroys in a given view. */ +function cleanUpView(viewState: ViewState): void { + if (!viewState.cleanup) return; + const cleanup = viewState.cleanup !; + for (let i = 0; i < cleanup.length - 1; i += 2) { + if (typeof cleanup[i] === 'string') { + cleanup ![i + 1].removeEventListener(cleanup[i], cleanup[i + 2], cleanup[i + 3]); + i += 2; + } else { + cleanup[i].call(cleanup[i + 1]); + } + } + viewState.cleanup = null; +} + +export function appendChild(parent: LNode, child: RNode | null, currentView: ViewState): boolean { + // Only add native child element to parent element if the parent element is regular Element. + // If parent is: + // - Regular element => add child + // - Component host element => + // - Current View, and parent view same => content => don't add -> parent component will + // re-project if needed. + // - Current View, and parent view different => view => add Child + // - View element => View's get added separately. + if (child !== null && (parent.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element && + (parent.view !== + currentView /* Crossing View Boundaries, it is Component, but add Element of View */ + || parent.data === null /* Regular Element. */)) { + // We only add element if not in View or not projected. + + const renderer = currentView.renderer; + (renderer as Renderer3Fn).listen ? + (renderer as Renderer3Fn).appendChild !(parent.native !as RElement, child) : + parent.native !.appendChild(child); + return true; + } + return false; +} + +export function insertChild(node: LNode, currentView: ViewState) { + const parent = node.parent !; + // Only add child element to parent element if the parent element is regular Element. + // If parent is: + // - Normal element => add child + // - Component element => + // - Current View, and parent view same => content don't add -> parent component will + // re-project if needed. + // - Current View, and parent view different => view => add Child + // - View element => View's get added separately. + if ((parent.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element && + (parent.view !== + currentView /* Crossing View Boundaries, its Component, but add Element of View */ + || parent.data === null /* Regular Element. */)) { + // We only add element if not in View or not projected. + + let sibling = node.next; + let nativeSibling: RNode|null = null; + while (sibling && (nativeSibling = sibling.native) === null) { + sibling = sibling.next; + } + const renderer = currentView.renderer; + (renderer as Renderer3Fn).listen ? + (renderer as Renderer3Fn).insertBefore !(parent.native !, node.native !, nativeSibling) : + parent.native !.insertBefore(node.native !, nativeSibling, false); + } +} + +export function processProjectedNode( + projectedNodes: ProjectionState, node: LElement | LText | LContainer, + currentParent: LView | LElement, currentView: ViewState) { + if ((node.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Container && + (currentParent.flags & LNodeFlags.TYPE_MASK) === LNodeFlags.Element && + currentParent.data === null) { + // The node we are adding is a Container and we are adding it to Element + // which is not Component (no more re-projection). Assignee the final + // projection location. + const containerState = (node as LContainer).data; + containerState.renderParent = currentParent as LElement; + const views = containerState.children; + for (let i = 0; i < views.length; i++) { + addRemoveViewFromContainer(node as LContainer, views[i], true, null); + } + } + projectedNodes.push(node); + appendChild(currentParent, node.native, currentView); +} diff --git a/packages/core/src/render3/node_selector_matcher.ts b/packages/core/src/render3/node_selector_matcher.ts new file mode 100644 index 0000000000..09bee1dccd --- /dev/null +++ b/packages/core/src/render3/node_selector_matcher.ts @@ -0,0 +1,116 @@ +/** + * @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 './ng_dev_mode'; + +import {assertNotNull} from './assert'; +import {CSSSelector, CSSSelectorWithNegations, NodeBindings, SimpleCSSSelector} from './interfaces'; + +function isCssClassMatching(nodeClassAttrVal: string, cssClassToMatch: string): boolean { + const nodeClassesLen = nodeClassAttrVal.length; + const matchIndex = nodeClassAttrVal !.indexOf(cssClassToMatch); + const matchEndIdx = matchIndex + cssClassToMatch.length; + if (matchIndex === -1 // no match + || (matchIndex > 0 && nodeClassAttrVal ![matchIndex - 1] !== ' ') // no space before + || + (matchEndIdx < nodeClassesLen && nodeClassAttrVal ![matchEndIdx] !== ' ')) // no space after + { + return false; + } + return true; +} + +/** + * A utility function to match an Ivy node static data against a simple CSS selector + * + * @param {NodeBindings} node static data to match + * @param {SimpleCSSSelector} selector + * @returns {boolean} + */ +export function isNodeMatchingSimpleSelector( + lNodeStaticData: NodeBindings, selector: SimpleCSSSelector): boolean { + const noOfSelectorParts = selector.length; + ngDevMode && assertNotNull(selector[0], 'selector[0]'); + const tagNameInSelector = selector[0]; + + // check tag tame + if (tagNameInSelector !== '' && tagNameInSelector !== lNodeStaticData.tagName) { + return false; + } + + // short-circuit case where we are only matching on element's tag name + if (noOfSelectorParts === 1) { + return true; + } + + // short-circuit case where an element has no attrs but a selector tries to match some + if (noOfSelectorParts > 1 && !lNodeStaticData.attrs) { + return false; + } + + const attrsInNode = lNodeStaticData.attrs !; + + for (let i = 1; i < noOfSelectorParts; i += 2) { + const attrNameInSelector = selector[i]; + const attrIdxInNode = attrsInNode.indexOf(attrNameInSelector); + if (attrIdxInNode % 2 !== 0) { // attribute names are stored at even indexes + return false; + } else { + const attrValInSelector = selector[i + 1]; + if (attrValInSelector !== '') { + // selector should also match on an attribute value + const attrValInNode = attrsInNode[attrIdxInNode + 1]; + if (attrNameInSelector === 'class') { + // iterate over all the remaining items in the selector selector array = class names + for (i++; i < noOfSelectorParts; i++) { + if (!isCssClassMatching(attrValInNode, selector[i])) { + return false; + } + } + } else if (attrValInSelector !== attrValInNode) { + return false; + } + } + } + } + + return true; +} + +export function isNodeMatchingSelectorWithNegations( + lNodeStaticData: NodeBindings, selector: CSSSelectorWithNegations): boolean { + const positiveSelector = selector[0]; + if (positiveSelector != null && + !isNodeMatchingSimpleSelector(lNodeStaticData, positiveSelector)) { + return false; + } + + // do we have any negation parts in this selector? + const negativeSelectors = selector[1]; + if (negativeSelectors) { + for (let i = 0; i < negativeSelectors.length; i++) { + // if one of negative selectors matched than the whole selector doesn't match + if (isNodeMatchingSimpleSelector(lNodeStaticData, negativeSelectors[i])) { + return false; + } + } + } + + return true; +} + +export function isNodeMatchingSelector( + lNodeStaticData: NodeBindings, selector: CSSSelector): boolean { + for (let i = 0; i < selector.length; i++) { + if (isNodeMatchingSelectorWithNegations(lNodeStaticData, selector[i])) { + return true; + } + } + + return false; +} diff --git a/packages/core/src/render3/public_interfaces.ts b/packages/core/src/render3/public_interfaces.ts new file mode 100644 index 0000000000..1397c21a5c --- /dev/null +++ b/packages/core/src/render3/public_interfaces.ts @@ -0,0 +1,198 @@ +/** + * @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 {Type} from '../core'; +import {diPublic, refreshComponent} from './instructions'; + + +/** + * Definition of what a template rendering function should look like. + */ +export type ComponentTemplate = { + (ctx: T, creationMode: boolean): void; ngData?: never; +}; +export type EmbeddedTemplate = (ctx: T) => void; + +export interface ComponentType extends Type { ngComponentDef: ComponentDef; } + +export interface DirectiveType extends Type { ngDirectiveDef: DirectiveDef; } + +export const enum DirectiveDefFlags {ContentQuery = 0b10} + +/** + * `DirectiveDef` is a compiled version of the Directive used by the renderer instructions. + */ +export interface DirectiveDef { + /** + * Token representing the directive. Used by DI. + */ + type: Type; + + /** Function that makes a directive public to the DI system. */ + diPublic: ((def: DirectiveDef) => void)|null; + + /** + * List of inputs which are part of the components public API. + * + * The key is minified property name whereas the value is the original unminified name. + */ + inputs: {[P in keyof T]: P}; + + /** + * List of outputs which are part of the components public API. + * + * The key is minified property name whereas the value is the original unminified name.= + */ + outputs: {[P in keyof T]: P}; + + /** + * List of methods which are part of the components public API. + * + * The key is minified property name whereas the value is the original unminified name. + */ + methods: {[P in keyof T]: P}; + + /** + * factory function used to create a new directive instance. + * + * NOTE: this property is short (1 char) because it is used in + * component templates which is sensitive to size. + */ + n(): T; + + /** + * Refresh method. Used by the containing component to signal + * to the directive that it should be refreshed. (Directives + * usually call life cycle methods at this point.) + * + * NOTE: this property is short (1 char) because it is used in + * component templates which is sensitive to size. + * + * @param directiveIndex index of the directive in the containing template + * @param elementIndex index of an host element for a given directive. + */ + r(this: DirectiveDef, directiveIndex: number, elementIndex: number): void; +} + +export interface ComponentDef extends DirectiveDef { + /** + * Refresh method. Used by the containing component to signal + * to the directive that it should be refreshed. (Directives + * usually call life cycle methods at this point.) + * + * NOTE: this property is short (1 char) because it is used in + * component templates which is sensitive to size. + * + * @param directiveIndex index of the directive in the containing template + * @param elementIndex index of an host element for a given component. + */ + r(this: ComponentDef, directiveIndex: number, elementIndex: number): void; + + /** + * The tag name which should be used by the component. + * + * NOTE: only used with component directives. + */ + tag: string; + + /** + * The View template of the component. + * + * NOTE: only used with component directives. + */ + template: ComponentTemplate; +} + +export interface DirectiveDefArgs { + type: Type; + factory: () => T; + refresh?: (this: DirectiveDef, directiveIndex: number, elementIndex: number) => void; + inputs?: {[P in keyof T]?: string}; + outputs?: {[P in keyof T]?: string}; + methods?: {[P in keyof T]?: string}; + features?: DirectiveDefFeature[]; +} + +export interface ComponentDefArgs extends DirectiveDefArgs { + tag: string; + template: ComponentTemplate; + refresh?: (this: ComponentDef, directiveIndex: number, elementIndex: number) => void; + features?: ComponentDefFeature[]; +} + +export type DirectiveDefFeature = (directiveDef: DirectiveDef) => void; +export type ComponentDefFeature = (directiveDef: DirectiveDef) => void; + +/** + * Create a component definition object. + * + * + * # Example + * ``` + * class MyDirective { + * // Generated by Angular Template Compiler + * // [Symbol] syntax will not be supported by TypeScript until v2.7 + * static [COMPONENT_DEF_SYMBOL] = defineComponent({ + * ... + * }); + * } + * ``` + */ +export function defineComponent(componentDefinition: ComponentDefArgs): ComponentDef { + const def = >{ + type: componentDefinition.type, + diPublic: null, + n: componentDefinition.factory, + tag: (componentDefinition as ComponentDefArgs).tag || null !, + template: (componentDefinition as ComponentDefArgs).template || null !, + r: componentDefinition.refresh || refreshComponent, + inputs: invertObject(componentDefinition.inputs), + outputs: invertObject(componentDefinition.outputs), + methods: invertObject(componentDefinition.methods), + }; + const feature = componentDefinition.features; + feature && feature.forEach((fn) => fn(def)); + return def; +} + +export function NgOnChangesFeature(definition: DirectiveDef) { + // TODO: implement. See: https://app.asana.com/0/443577627818617/465170715764659 +} + +export function PublicFeature(definition: DirectiveDef) { + definition.diPublic = diPublic; +} + +const EMPTY = {}; + +/** Swaps the keys and values of an object. */ +function invertObject(obj: any): any { + if (obj == null) return EMPTY; + const newObj: any = {}; + for (let minifiedKey in obj) { + newObj[obj[minifiedKey]] = minifiedKey; + } + return newObj; +} + +/** + * Create a directive definition object. + * + * # Example + * ``` + * class MyDirective { + * // Generated by Angular Template Compiler + * // [Symbol] syntax will not be supported by TypeScript until v2.7 + * static [DIRECTIVE_DEF_SYMBOL] = defineDirective({ + * ... + * }); + * } + * ``` + */ +export const defineDirective = defineComponent as(directiveDefinition: DirectiveDefArgs) => + DirectiveDef; diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts new file mode 100644 index 0000000000..30376caf64 --- /dev/null +++ b/packages/core/src/render3/query.ts @@ -0,0 +1,210 @@ +/** + * @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 {Observable} from 'rxjs/Observable'; + +import {QueryList as IQueryList, Type} from '../core'; + +import {assertNotNull} from './assert'; +import {LContainer, LNode, LNodeFlags, LView, QueryState} from './interfaces'; + + + +/** + * A predicate which determines if a given element/directive should be included in the query + */ +export interface QueryPredicate { + /** + * Next predicate + */ + next: QueryPredicate|null; + + /** + * Destination to which the value should be added. + */ + list: QueryList; + + /** + * If looking for directives than it contains the directive type. + */ + type: Type|null; + + /** + * If selector then contains the selector parts where: + * - even index: + * - `null`: represents a tag name + * - `"#""`: represents a reference name + * - `string`: name of the attribute + * - odd index: + * - `null`: any value will match + * - `string`: the value which mast match. + */ + selector: any[]|null; + + /** + * Values which have been located. + * + * this is what builds up the `QueryList._valuesTree`. + */ + values: any[]; +} + +export class QueryState_ implements QueryState { + shallow: QueryPredicate|null = null; + deep: QueryPredicate|null = null; + + constructor(deep?: QueryPredicate) { this.deep = deep == null ? null : deep; } + + track(queryList: IQueryList, predicate: Type|any[], descend?: boolean): void { + // TODO(misko): This is not right. In case of inherited state, a calling track will incorrectly + // mutate parent. + if (descend) { + this.deep = createPredicate(this.deep, queryList, predicate); + } else { + this.shallow = createPredicate(this.shallow, queryList, predicate); + } + } + + child(): QueryState|null { + if (this.deep === null) { + // if we don't have any deep queries than no need to track anything more. + return null; + } + if (this.shallow === null) { + // DeepQuery: We can reuse the current state if the child state would be same as current + // state. + return this; + } else { + // We need to create new state + return new QueryState_(this.deep); + } + } + + add(node: LNode): void { + add(this.shallow, node); + add(this.deep, node); + } + + insert(container: LContainer, view: LView, index: number): void { + throw new Error('Method not implemented.'); + } + + remove(container: LContainer, view: LView, index: number): void { + throw new Error('Method not implemented.'); + } +} + +function add(predicate: QueryPredicate| null, node: LNode) { + while (predicate) { + const type = predicate.type; + if (type) { + const directives = node.view.directives; + const flags = node.flags; + for (let i = flags >> LNodeFlags.INDX_SHIFT, + ii = i + ((flags & LNodeFlags.SIZE_MASK) >> LNodeFlags.SIZE_SHIFT); + i < ii; i++) { + const def = directives[i << 1 | 1]; + if (def.diPublic && def.type === type) { + predicate.values.push(directives[i << 1]); + } + } + } + predicate = predicate.next; + } +} + +function createPredicate( + previous: QueryPredicate| null, queryList: QueryList, + predicate: Type| any[]): QueryPredicate { + const isArray = Array.isArray(predicate); + const values = []; + if ((queryList as any as QueryList_)._valuesTree === null) { + (queryList as any as QueryList_)._valuesTree = values; + } + return { + next: previous, + list: queryList, + type: isArray ? null : predicate as Type, + selector: isArray ? predicate as any[] : null, + values: values + }; +} + +class QueryList_/* implements IQueryList */ { + dirty: boolean = false; + changes: Observable; + + get length(): number { + ngDevMode && assertNotNull(this._values, 'refreshed'); + return this._values !.length; + } + + get first(): T|null { + ngDevMode && assertNotNull(this._values, 'refreshed'); + let values = this._values !; + return values.length ? values[0] : null; + } + + get last(): T|null { + ngDevMode && assertNotNull(this._values, 'refreshed'); + let values = this._values !; + return values.length ? values[values.length - 1] : null; + } + + /** @internal */ + _valuesTree: any[]|null = null; + /** @internal */ + _values: T[]|null = null; + + /** @internal */ + _refresh(): boolean { + // TODO(misko): needs more logic to flatten tree. + if (this._values === null) { + this._values = this._valuesTree; + return true; + } + return false; + } + + map(fn: (item: T, index: number, array: T[]) => U): U[] { + throw new Error('Method not implemented.'); + } + filter(fn: (item: T, index: number, array: T[]) => boolean): T[] { + throw new Error('Method not implemented.'); + } + find(fn: (item: T, index: number, array: T[]) => boolean): T|undefined { + throw new Error('Method not implemented.'); + } + reduce(fn: (prevValue: U, curValue: T, curIndex: number, array: T[]) => U, init: U): U { + throw new Error('Method not implemented.'); + } + forEach(fn: (item: T, index: number, array: T[]) => void): void { + throw new Error('Method not implemented.'); + } + some(fn: (value: T, index: number, array: T[]) => boolean): boolean { + throw new Error('Method not implemented.'); + } + toArray(): T[] { + ngDevMode && assertNotNull(this._values, 'refreshed'); + return this._values !; + } + toString(): string { throw new Error('Method not implemented.'); } + reset(res: (any[]|T)[]): void { throw new Error('Method not implemented.'); } + notifyOnChanges(): void { throw new Error('Method not implemented.'); } + setDirty(): void { throw new Error('Method not implemented.'); } + destroy(): void { throw new Error('Method not implemented.'); } +} + +// NOTE: this hack is here because IQueryList has private members and therefore +// it can't be implemented only extended. +export type QueryList = IQueryList; +export const QueryList: typeof IQueryList = QueryList_ as any; + +export function refreshQuery(query: QueryList): boolean { + return (query as any as QueryList_)._refresh(); +} diff --git a/packages/core/src/render3/renderer.ts b/packages/core/src/render3/renderer.ts new file mode 100644 index 0000000000..49de91c59b --- /dev/null +++ b/packages/core/src/render3/renderer.ts @@ -0,0 +1,129 @@ +/** + * @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 + */ + +/** + * The goal here is to make sure that the browser DOM API is the Renderer. + * We do this by defining a subset of DOM API to be the renderer and than + * use that time for rendering. + * + * At runtime we can than use the DOM api directly, in server or web-worker + * it will be easy to implement such API. + */ + +import {RendererStyleFlags2} from '../core'; +import {ComponentDef} from './public_interfaces'; + +// TODO: cleanup once the code is merged in angular/angular +export enum RendererStyleFlags3 { + Important = 1 << 0, + DashCase = 1 << 1 +} + +export type Renderer3 = Renderer3oo | Renderer3Fn; + +/** + * Object Oriented style of API needed to create elements and text nodes. + */ +export interface Renderer3oo { + createComment(data: string): RComment; + createElement(tagName: string): RElement; + createTextNode(data: string): RText; + + querySelector(selectors: string): RElement|null; +} + +/** + * Functional style of API needed to create elements and text nodes. + */ +export interface Renderer3Fn { + destroy(): void; + createElement(name: string, namespace?: string|null): RElement; + createComment(value: string): RComment; + createText(value: string): RText; + /** + * This property is allowed to be null / undefined, + * in which case the view engine won't call it. + * This is used as a performance optimization for production mode. + */ + destroyNode?: ((node: RNode) => void)|null; + appendChild(parent: RElement, newChild: RNode): void; + insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null): void; + removeChild(parent: RElement, oldChild: RNode): void; + selectRootElement(selectorOrNode: string|any): RElement; + + setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void; + removeAttribute(el: RElement, name: string, namespace?: string|null): void; + addClass(el: RElement, name: string): void; + removeClass(el: RElement, name: string): void; + setStyle( + el: RElement, style: string, value: any, + flags?: RendererStyleFlags2|RendererStyleFlags3): void; + removeStyle(el: RElement, style: string, flags?: RendererStyleFlags2|RendererStyleFlags3): void; + setProperty(el: RElement, name: string, value: any): void; + setValue(node: RText, value: string): void; + + // TODO(misko): Deprecate in favor of addEventListener/removeEventListener + listen(target: RNode, eventName: string, callback: (event: any) => boolean | void): () => void; +} + +export interface RendererFactory3 { + createRenderer(hostElement: RElement, componentDef: ComponentDef): Renderer3; + begin?(): void; + end?(): void; +} + +/** + * Subset of API needed for appending elements and text nodes. + */ +export interface RNode { + removeChild(oldChild: RNode): void; + + /** + * Insert a child node. + * + * Used exclusively for adding View root nodes into ViewAnchor location. + */ + insertBefore(newChild: RNode, refChild: RNode|null, isViewRoot: boolean): void; + + /** + * Append a child node. + * + * Used exclusively for building up DOM which are static (ie not View roots) + */ + appendChild(newChild: RNode): RNode; +} + +/** + * Subset of API needed for writing attributes, properties, and setting up + * listeners on Element. + */ +export interface RElement extends RNode { + style: RCSSStyleDeclaration; + classList: RDOMTokenList; + setAttribute(name: string, value: string): void; + removeAttribute(name: string): void; + setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void; + addEventListener(type: string, listener: EventListener, useCapture?: boolean): void; + removeEventListener(type: string, listener?: EventListener, options?: boolean): void; + + setProperty?(name: string, value: any): void; +} + +export interface RCSSStyleDeclaration { + removeProperty(propertyName: string): string; + setProperty(propertyName: string, value: string|null, priority?: string): void; +} + +export interface RDOMTokenList { + add(token: string): void; + remove(token: string): void; +} + +export interface RText extends RNode { textContent: string|null; } + +export interface RComment extends RNode {} diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts new file mode 100644 index 0000000000..cbec7c492a --- /dev/null +++ b/packages/core/src/render3/util.ts @@ -0,0 +1,23 @@ +/** + * @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 + */ + +/** +* Must use this method for CD (instead of === ) since NaN !== NaN +*/ +export function isDifferent(a: any, b: any): boolean { + // NaN is the only value that is not equal to itself so the first + // test checks if both a and b are not NaN + return !(a !== a && b !== b) && a !== b; +} + +export function stringify(value: any): string { + if (typeof value == 'function') return value.name || value; + if (typeof value == 'string') return value; + if (value == null) return ''; + return '' + value; +} \ No newline at end of file diff --git a/packages/core/test/BUILD.bazel b/packages/core/test/BUILD.bazel index 722de0a604..de17bc6aed 100644 --- a/packages/core/test/BUILD.bazel +++ b/packages/core/test/BUILD.bazel @@ -6,7 +6,10 @@ load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") ts_library( name = "test_lib", testonly = 1, - srcs = glob(["**/*.ts"]), + srcs = glob( + ["**/*.ts"], + exclude = ["render3/**/*.ts"], + ), tsconfig = "//packages:tsconfig", deps = [ "//packages/animations", diff --git a/packages/core/test/render3/BUILD.bazel b/packages/core/test/render3/BUILD.bazel new file mode 100644 index 0000000000..a27837958e --- /dev/null +++ b/packages/core/test/render3/BUILD.bazel @@ -0,0 +1,29 @@ +package(default_visibility = ["//visibility:public"]) + +load("@build_bazel_rules_typescript//:defs.bzl", "ts_library") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +ts_library( + name = "lib", + testonly = 1, + srcs = glob( + ["**/*.ts"], + exclude = ["**/*_perf.ts"], + ), + tsconfig = "//packages:tsconfig.json", + deps = [ + "//packages:types", + "//packages/core", + "//packages/platform-browser", + ], +) + +jasmine_node_test( + name = "render3", + bootstrap = [ + "angular_src/packages/core/test/render3/load_domino", + ], + deps = [ + ":lib", + ], +) diff --git a/packages/core/test/render3/basic_perf.ts b/packages/core/test/render3/basic_perf.ts new file mode 100644 index 0000000000..52023e5799 --- /dev/null +++ b/packages/core/test/render3/basic_perf.ts @@ -0,0 +1,74 @@ +/** + * @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 {C, E, T, V, c, defineComponent, e, rC, rc, v} from '../../src/render3/index'; + +import {document, renderComponent} from './render_util'; + +describe('iv perf test', () => { + + const count = 100000; + const noOfIterations = 10; + + describe('render', () => { + for (let iteration = 0; iteration < noOfIterations; iteration++) { + it(`${iteration}. create ${count} divs in DOM`, () => { + const start = new Date().getTime(); + const container = document.createElement('div'); + for (let i = 0; i < count; i++) { + const div = document.createElement('div'); + div.appendChild(document.createTextNode('-')); + container.appendChild(div); + } + const end = new Date().getTime(); + log(`${count} DIVs in DOM`, (end - start) / count); + }); + + it(`${iteration}. create ${count} divs in Render3`, () => { + class Component { + static ngComponentDef = defineComponent({ + type: Component, + tag: 'div', + template: function Template(ctx: any, cm: any) { + if (cm) { + C(0); + c(); + } + rC(0); + { + for (let i = 0; i < count; i++) { + let cm0 = V(0); + { + if (cm0) { + E(0, 'div'); + T(1, '-'); + e(); + } + } + v(); + } + } + rc(); + }, + factory: () => new Component + }); + } + + const start = new Date().getTime(); + renderComponent(Component); + const end = new Date().getTime(); + log(`${count} DIVs in Render3`, (end - start) / count); + }); + } + }); +}); + +function log(text: string, duration: number) { + // tslint:disable-next-line:no-console + console.log(text, duration * 1000, 'ns'); +} diff --git a/packages/core/test/render3/component_spec.ts b/packages/core/test/render3/component_spec.ts new file mode 100644 index 0000000000..66e04df8c9 --- /dev/null +++ b/packages/core/test/render3/component_spec.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 {T, b, defineComponent, markDirty, t} from '../../src/render3/index'; + +import {containerEl, renderComponent, requestAnimationFrame} from './render_util'; + +describe('component', () => { + class CounterComponent { + count = 0; + + increment() { this.count++; } + + static ngComponentDef = defineComponent({ + type: CounterComponent, + tag: 'counter', + template: function(ctx: CounterComponent, cm: boolean) { + if (cm) { + T(0); + } + t(0, b(ctx.count)); + }, + factory: () => new CounterComponent, + inputs: {count: 'count'}, + methods: {increment: 'increment'} + }); + } + + beforeEach( + () => { + + }); + + describe('renderComponent', () => { + it('should render on initial call', () => { + renderComponent(CounterComponent); + expect(containerEl.innerHTML).toEqual('0'); + }); + + it('should re-render on input change or method invocation', () => { + const component = renderComponent(CounterComponent); + expect(containerEl.innerHTML).toEqual('0'); + component.count = 123; + markDirty(component, requestAnimationFrame); + expect(containerEl.innerHTML).toEqual('0'); + requestAnimationFrame.flush(); + expect(containerEl.innerHTML).toEqual('123'); + component.increment(); + markDirty(component, requestAnimationFrame); + expect(containerEl.innerHTML).toEqual('123'); + requestAnimationFrame.flush(); + expect(containerEl.innerHTML).toEqual('124'); + }); + + }); + +}); diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts new file mode 100644 index 0000000000..0d925cfedc --- /dev/null +++ b/packages/core/test/render3/content_spec.ts @@ -0,0 +1,816 @@ +/** + * @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 {C, D, E, P, T, V, c, dP, detectChanges, e, m, rC, rc, v} from '../../src/render3/index'; + +import {createComponent, renderComponent, toHtml} from './render_util'; + +describe('content projection', () => { + it('should project content', () => { + + /** + *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, 'div'); + { P(1, 0); } + e(); + } + }); + + /** + * content + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, 'content'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
content
'); + }); + + it('should project content when root.', () => { + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + P(0, 0); + } + }); + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, 'content'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('content'); + }); + + it('should re-project content when root.', () => { + const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, 'div'); + { P(1, 0); } + e(); + } + }); + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, GrandChild.ngComponentDef); + { + D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef); + P(1, 0); + } + e(); + GrandChild.ngComponentDef.r(0, 0); + } + }); + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'b'); + T(2, 'Hello'); + e(); + T(3, 'World!'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual('
HelloWorld!
'); + }); + + it('should project content with container.', () => { + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, 'div'); + { P(1, 0); } + e(); + } + }); + const Parent = createComponent('parent', function(ctx: {value: any}, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, '('); + C(2); + c(); + T(3, ')'); + } + e(); + } + rC(2); + { + if (ctx.value) { + if (V(0)) { + T(0, 'content'); + } + v(); + } + } + rc(); + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
()
'); + parent.value = true; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
(content)
'); + parent.value = false; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
()
'); + }); + + it('should project content with container and if-else.', () => { + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, 'div'); + { P(1, 0); } + e(); + } + }); + const Parent = createComponent('parent', function(ctx: {value: any}, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, '('); + C(2); + c(); + T(3, ')'); + } + e(); + } + rC(2); + { + if (ctx.value) { + if (V(0)) { + T(0, 'content'); + } + v(); + } else { + if (V(1)) { + T(0, 'else'); + } + v(); + } + } + rc(); + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
(else)
'); + parent.value = true; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
(content)
'); + parent.value = false; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
(else)
'); + }); + + it('should support projection in embedded views', () => { + let childCmptInstance: any; + + /** + *
+ * % if (!skipContent) { + * + * + * + * % } + *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, 'div'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + if (!ctx.skipContent) { + if (V(0)) { + E(0, 'span'); + P(1, 0); + e(); + } + v(); + } + } + rc(); + }); + + /** + * content + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, childCmptInstance = Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, 'content'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
content
'); + + childCmptInstance.skipContent = true; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
'); + }); + + it('should support projection in embedded views when ng-content is a root node of an embedded view', + () => { + let childCmptInstance: any; + + /** + *
+ * % if (!skipContent) { + * + * % } + *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, 'div'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + if (!ctx.skipContent) { + if (V(0)) { + P(0, 0); + } + v(); + } + } + rc(); + }); + + /** + * content + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, childCmptInstance = Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, 'content'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
content
'); + + childCmptInstance.skipContent = true; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
'); + }); + + it('should project nodes into the last ng-content', () => { + /** + *
+ * + */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, 'div'); + { P(1, 0); } + e(); + E(2, 'span'); + { P(3, 0); } + e(); + } + }); + + /** + * content + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, 'content'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
content
'); + }); + + /** + * Warning: this test is _not_ in-line with what Angular does atm. + * Moreover the current implementation logic will result in DOM nodes + * being re-assigned from one parent to another. Proposal: have compiler + * to remove all but the latest occurrence of so we generate + * only one P(n, m, 0) instruction. It would make it consistent with the + * current Angular behaviour: + * http://plnkr.co/edit/OAYkNawTDPkYBFTqovTP?p=preview + */ + it('should project nodes into the last available ng-content', () => { + let childCmptInstance: any; + /** + * + *
+ * % if (show) { + * + * % } + *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + P(0, 0); + E(1, 'div'); + { + C(2); + c(); + } + e(); + } + rC(2); + { + if (ctx.show) { + if (V(0)) { + P(0, 0); + } + v(); + } + } + rc(); + }); + + /** + * content + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, childCmptInstance = Child.ngComponentDef.n(), Child.ngComponentDef); + T(1, 'content'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('content
'); + + childCmptInstance.show = true; + detectChanges(parent); + expect(toHtml(parent)).toEqual('
content
'); + }); + + describe('with selectors', () => { + + it('should project nodes using attribute selectors', () => { + /** + *
+ *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, + dP([[[['span', 'title', 'toFirst'], null]], [[['span', 'title', 'toSecond'], null]]])); + E(0, 'div', ['id', 'first']); + { P(1, 0, 1); } + e(); + E(2, 'div', ['id', 'second']); + { P(3, 0, 2); } + e(); + } + }); + + /** + * + * 1 + * 2 + * + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'span', ['title', 'toFirst']); + { T(2, '1'); } + e(); + E(3, 'span', ['title', 'toSecond']); + { T(4, '2'); } + e(); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual( + '
1
2
'); + }); + + it('should project nodes using class selectors', () => { + /** + *
+ *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, + dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]])); + E(0, 'div', ['id', 'first']); + { P(1, 0, 1); } + e(); + E(2, 'div', ['id', 'second']); + { P(3, 0, 2); } + e(); + } + }); + + /** + * + * 1 + * 2 + * + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'span', ['class', 'toFirst']); + { T(2, '1'); } + e(); + E(3, 'span', ['class', 'toSecond']); + { T(4, '2'); } + e(); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual( + '
1
2
'); + }); + + it('should project nodes using class selectors when element has multiple classes', () => { + /** + *
+ *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, + dP([[[['span', 'class', 'toFirst'], null]], [[['span', 'class', 'toSecond'], null]]])); + E(0, 'div', ['id', 'first']); + { P(1, 0, 1); } + e(); + E(2, 'div', ['id', 'second']); + { P(3, 0, 2); } + e(); + } + }); + + /** + * + * 1 + * 2 + * + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'span', ['class', 'other toFirst']); + { T(2, '1'); } + e(); + E(3, 'span', ['class', 'toSecond noise']); + { T(4, '2'); } + e(); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual( + '
1
2
'); + }); + + it('should project nodes into the first matching selector', () => { + /** + *
+ *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP([[[['span'], null]], [[['span', 'class', 'toSecond'], null]]])); + E(0, 'div', ['id', 'first']); + { P(1, 0, 1); } + e(); + E(2, 'div', ['id', 'second']); + { P(3, 0, 2); } + e(); + } + }); + + /** + * + * 1 + * 2 + * + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'span', ['class', 'toFirst']); + { T(2, '1'); } + e(); + E(3, 'span', ['class', 'toSecond']); + { T(4, '2'); } + e(); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual( + '
12
'); + }); + + it('should allow mixing ng-content with and without selectors', () => { + /** + *
+ *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP([[[['span', 'class', 'toFirst'], null]]])); + E(0, 'div', ['id', 'first']); + { P(1, 0, 1); } + e(); + E(2, 'div', ['id', 'second']); + { P(3, 0); } + e(); + } + }); + + /** + * + * 1 + * 2 + * + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'span', ['class', 'toFirst']); + { T(2, '1'); } + e(); + E(3, 'span'); + { T(4, 'remaining'); } + e(); + T(5, 'more remaining'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual( + '
1
remainingmore remaining
'); + }); + + it('should allow mixing ng-content with and without selectors - ng-content first', () => { + /** + *
+ *
+ */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP([[[['span', 'class', 'toSecond'], null]]])); + E(0, 'div', ['id', 'first']); + { P(1, 0); } + e(); + E(2, 'div', ['id', 'second']); + { P(3, 0, 1); } + e(); + } + }); + + /** + * + * 1 + * 2 + * remaining + * + */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'span'); + { T(2, '1'); } + e(); + E(3, 'span', ['class', 'toSecond']); + { T(4, '2'); } + e(); + T(5, 'remaining'); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual( + '
1remaining
2
'); + }); + + /** + * Descending into projected content for selector-matching purposes is not supported + * today: http://plnkr.co/edit/MYQcNfHSTKp9KvbzJWVQ?p=preview + */ + it('should not match selectors on re-projected content', () => { + + /** + * + *
+ * + */ + const GrandChild = createComponent('grand-child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP([[[['span'], null]]])); + P(0, 0, 1); + E(1, 'hr'); + e(); + P(2, 0, 0); + } + }); + + /** + * + * + * in child template + * + */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP()); + E(0, GrandChild.ngComponentDef); + { + D(0, GrandChild.ngComponentDef.n(), GrandChild.ngComponentDef); + P(1, 0); + E(2, 'span'); + { T(3, 'in child template'); } + e(); + } + e(); + GrandChild.ngComponentDef.r(0, 0); + } + }); + + /** + * + *
+ * parent content + *
+ *
+ */ + const Parent = createComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, 'span'); + { T(2, 'parent content'); } + e(); + } + e(); + } + Child.ngComponentDef.r(0, 0); + }); + + const parent = renderComponent(Parent); + expect(toHtml(parent)) + .toEqual( + 'in child template
parent content
'); + }); + + it('should match selectors against projected containers', () => { + + /** + * + * + * + */ + const Child = createComponent('child', function(ctx: any, cm: boolean) { + if (cm) { + m(0, dP([[[['div'], null]]])); + E(0, 'span'); + { P(1, 0, 1); } + e(); + } + }); + + /** + * + *
content
+ *
+ */ + const Parent = createComponent('parent', function(ctx: {value: any}, cm: boolean) { + if (cm) { + E(0, Child.ngComponentDef); + { + D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + C(1, undefined, 'div'); + c(); + } + e(); + } + rC(1); + { + if (true) { + if (V(0)) { + E(0, 'div'); + { T(1, 'content'); } + e(); + } + v(); + } + } + rc(); + Child.ngComponentDef.r(0, 0); + }); + const parent = renderComponent(Parent); + expect(toHtml(parent)).toEqual('
content
'); + }); + + }); + +}); diff --git a/packages/core/test/render3/control_flow_spec.ts b/packages/core/test/render3/control_flow_spec.ts new file mode 100644 index 0000000000..1eff32d70e --- /dev/null +++ b/packages/core/test/render3/control_flow_spec.ts @@ -0,0 +1,613 @@ +/** + * @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 {C, E, T, V, b, c, e, rC, rc, t, v} from '../../src/render3/index'; + +import {renderToHtml} from './render_util'; + +describe('JS control flow', () => { + it('should work with if block', () => { + const ctx: {message: string | null, condition: boolean} = {message: 'Hello', condition: true}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + if (ctx.condition) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'span'); + { T(1); } + e(); + } + t(1, b(ctx.message)); + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, ctx)).toEqual('
Hello
'); + + ctx.condition = false; + ctx.message = 'Hi!'; + expect(renderToHtml(Template, ctx)).toEqual('
'); + + ctx.condition = true; + expect(renderToHtml(Template, ctx)).toEqual('
Hi!
'); + }); + + it('should work with nested if blocks', () => { + const ctx: {condition: boolean, condition2: boolean} = {condition: true, condition2: true}; + + /** + *
+ * % if(ctx.condition) { + * + * % if(ctx.condition2) { + * Hello + * % } + * + * % } + *
+ */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + if (ctx.condition) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'span'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + if (ctx.condition2) { + let cm2 = V(2); + { + if (cm2) { + T(0, 'Hello'); + } + } + v(); + } + } + rc(); + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, ctx)).toEqual('
Hello
'); + + ctx.condition = false; + expect(renderToHtml(Template, ctx)).toEqual('
'); + + ctx.condition = true; + expect(renderToHtml(Template, ctx)).toEqual('
Hello
'); + + ctx.condition2 = false; + expect(renderToHtml(Template, ctx)).toEqual('
'); + + ctx.condition2 = true; + expect(renderToHtml(Template, ctx)).toEqual('
Hello
'); + + ctx.condition2 = false; + expect(renderToHtml(Template, ctx)).toEqual('
'); + + ctx.condition = false; + expect(renderToHtml(Template, ctx)).toEqual('
'); + + ctx.condition = true; + expect(renderToHtml(Template, ctx)).toEqual('
'); + + ctx.condition2 = true; + expect(renderToHtml(Template, ctx)).toEqual('
Hello
'); + }); + + it('should work with containers with views as parents', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { T(1, 'hello'); } + e(); + C(2); + c(); + } + rC(2); + { + if (ctx.condition1) { + let cm0 = V(0); + { + if (cm0) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition2) { + let cm0 = V(0); + { + if (cm0) { + T(0, 'world'); + } + } + v(); + } + } + rc(); + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, {condition1: true, condition2: true})) + .toEqual('
hello
world'); + expect(renderToHtml(Template, {condition1: false, condition2: false})) + .toEqual('
hello
'); + + }); + + it('should work with loop block', () => { + const ctx: {data: string[] | null} = {data: ['a', 'b', 'c']}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'ul'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + for (let i = 0; i < ctx.data.length; i++) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'li'); + { T(1); } + e(); + } + t(1, b(ctx.data[i])); + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, ctx)).toEqual('
  • a
  • b
  • c
'); + + ctx.data = ['e', 'f']; + expect(renderToHtml(Template, ctx)).toEqual('
  • e
  • f
'); + + ctx.data = []; + expect(renderToHtml(Template, ctx)).toEqual('
    '); + + ctx.data = ['a', 'b', 'c']; + expect(renderToHtml(Template, ctx)).toEqual('
    • a
    • b
    • c
    '); + + ctx.data.push('d'); + expect(renderToHtml(Template, ctx)) + .toEqual('
    • a
    • b
    • c
    • d
    '); + + ctx.data = ['e']; + expect(renderToHtml(Template, ctx)).toEqual('
    • e
    '); + }); + + it('should work with nested loop blocks', () => { + const ctx: {data: string[][] | null} = {data: [['a', 'b', 'c'], ['m', 'n']]}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'ul'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + for (let i = 0; i < ctx.data[0].length; i++) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'li'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + ctx.data[1].forEach((value: string, ind: number) => { + if (V(2)) { + T(0); + } + t(0, b(ctx.data[0][i] + value)); + v(); + }); + } + rc(); + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, ctx)).toEqual('
    • aman
    • bmbn
    • cmcn
    '); + + ctx.data = [[], []]; + expect(renderToHtml(Template, ctx)).toEqual('
      '); + }); + + it('should work with nested loop blocks where nested container is a root node', () => { + + /** + *
      + * Before + * % for (let i = 0; i < cafes.length; i++) { + *

      {{ cafes[i].name }}

      + * % for (let j = 0; j < cafes[i].entrees; j++) { + * {{ cafes[i].entrees[j] }} + * % } + * - + * % } + * After + *
      + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + T(1, 'Before'); + C(2); + c(); + T(3, 'After'); + } + e(); + } + rC(2); + { + for (let i = 0; i < ctx.cafes.length; i++) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'h2'); + { T(1); } + e(); + C(2); + c(); + T(3, '-'); + } + t(1, b(ctx.cafes[i].name)); + rC(2); + { + for (let j = 0; j < ctx.cafes[i].entrees.length; j++) { + if (V(1)) { + T(0); + } + t(0, b(ctx.cafes[i].entrees[j])); + v(); + } + } + rc(); + } + v(); + } + } + rc(); + } + + const ctx = { + cafes: [ + {name: '1', entrees: ['a', 'b', 'c']}, {name: '2', entrees: ['d', 'e', 'f']}, + {name: '3', entrees: ['g', 'h', 'i']} + ] + }; + + expect(renderToHtml(Template, ctx)) + .toEqual('
      Before

      1

      abc-

      2

      def-

      3

      ghi-After
      '); + + ctx.cafes = []; + expect(renderToHtml(Template, ctx)).toEqual('
      BeforeAfter
      '); + + ctx.cafes = [ + {name: '1', entrees: ['a', 'c']}, + {name: '2', entrees: ['d', 'e']}, + ]; + expect(renderToHtml(Template, ctx)).toEqual('
      Before

      1

      ac-

      2

      de-After
      '); + + }); + + it('should work with loop blocks nested three deep', () => { + + /** + *
      + * Before + * % for (let i = 0; i < cafes.length; i++) { + *

      {{ cafes[i].name }}

      + * % for (let j = 0; j < cafes[i].entrees.length; j++) { + *

      {{ cafes[i].entrees[j].name }}

      + * % for (let k = 0; k < cafes[i].entrees[j].foods.length; k++) { + * {{ cafes[i].entrees[j].foods[k] }} + * % } + * % } + * - + * % } + * After + *
      + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + T(1, 'Before'); + C(2); + c(); + T(3, 'After'); + } + e(); + } + rC(2); + { + for (let i = 0; i < ctx.cafes.length; i++) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'h2'); + { T(1); } + e(); + C(2); + c(); + T(3, '-'); + } + t(1, b(ctx.cafes[i].name)); + rC(2); + { + for (let j = 0; j < ctx.cafes[i].entrees.length; j++) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'h3'); + { T(1); } + e(); + C(2); + c(); + } + t(1, b(ctx.cafes[i].entrees[j].name)); + rC(2); + { + for (let k = 0; k < ctx.cafes[i].entrees[j].foods.length; k++) { + if (V(1)) { + T(0); + } + t(0, b(ctx.cafes[i].entrees[j].foods[k])); + v(); + } + } + rc(); + } + v(); + } + } + rc(); + } + v(); + } + } + rc(); + } + + const ctx = { + cafes: [ + { + name: '1', + entrees: + [{name: 'a', foods: [1, 2]}, {name: 'b', foods: [3, 4]}, {name: 'c', foods: [5, 6]}] + }, + { + name: '2', + entrees: [ + {name: 'd', foods: [1, 2]}, {name: 'e', foods: [3, 4]}, {name: 'f', foods: [5, 6]} + ] + } + ] + }; + + expect(renderToHtml(Template, ctx)) + .toEqual( + '
      ' + + 'Before' + + '

      1

      a

      12

      b

      34

      c

      56-' + + '

      2

      d

      12

      e

      34

      f

      56-' + + 'After' + + '
      '); + + ctx.cafes = []; + expect(renderToHtml(Template, ctx)).toEqual('
      BeforeAfter
      '); + }); + + it('should work with if/else blocks', () => { + const ctx: {message: string | null, condition: boolean} = {message: 'Hello', condition: true}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + if (ctx.condition) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'span'); + { T(1, 'Hello'); } + e(); + } + } + v(); + } else { + let cm2 = V(2); + { + if (cm2) { + E(0, 'div'); + { T(1, 'Goodbye'); } + e(); + } + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, ctx)).toEqual('
      Hello
      '); + + ctx.condition = false; + expect(renderToHtml(Template, ctx)).toEqual('
      Goodbye
      '); + + ctx.condition = true; + expect(renderToHtml(Template, ctx)).toEqual('
      Hello
      '); + }); +}); + +describe('JS for loop', () => { + it('should work with sibling for blocks', () => { + const ctx: {data1: string[] | null, + data2: number[] | null} = {data1: ['a', 'b', 'c'], data2: [1, 2]}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + for (let i = 0; i < ctx.data1.length; i++) { + if (V(1)) { + T(0); + } + t(0, b(ctx.data1[i])); + v(); + } + for (let j = 0; j < ctx.data2.length; j++) { + if (V(2)) { + T(0); + } + t(0, b(ctx.data2[j])); + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, ctx)).toEqual('
      abc12
      '); + + ctx.data1 = ['e', 'f']; + expect(renderToHtml(Template, ctx)).toEqual('
      ef12
      '); + + ctx.data2 = [8]; + expect(renderToHtml(Template, ctx)).toEqual('
      ef8
      '); + + ctx.data1 = ['x', 'y']; + expect(renderToHtml(Template, ctx)).toEqual('
      xy8
      '); + }); +}); + +describe('function calls', () => { + it('should work', () => { + const ctx: {data: string[]} = {data: ['foo', 'bar']}; + + function spanify(ctx: {message: string | null}, cm: boolean) { + const message = ctx.message; + if (cm) { + E(0, 'span'); + { T(1); } + e(); + } + t(1, b(message)); + } + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + T(1, 'Before'); + C(2); + c(); + C(3); + c(); + T(4, 'After'); + } + e(); + } + rC(2); + { + let cm0 = V(0); + { spanify({message: ctx.data[0]}, cm0); } + v(); + } + rc(); + rC(3); + { + let cm0 = V(0); + { spanify({message: ctx.data[1]}, cm0); } + v(); + } + rc(); + } + + expect(renderToHtml(Template, ctx)) + .toEqual('
      BeforefoobarAfter
      '); + + ctx.data = []; + expect(renderToHtml(Template, ctx)).toEqual('
      BeforeAfter
      '); + + }); +}); diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts new file mode 100644 index 0000000000..a79e82fe45 --- /dev/null +++ b/packages/core/test/render3/di_spec.ts @@ -0,0 +1,339 @@ +/** + * @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 {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core'; + +import {bloomFindPossibleInjector} from '../../src/render3/di'; +import {C, D, E, PublicFeature, T, V, b, b2, c, defineDirective, e, inject, injectElementRef, injectTemplateRef, injectViewContainerRef, rC, rc, t, v} from '../../src/render3/index'; +import {bloomAdd, createNode, createViewState, enterView, getOrCreateNodeInjector, leaveView} from '../../src/render3/instructions'; +import {LNodeFlags, LNodeInjector} from '../../src/render3/interfaces'; + +import {renderToHtml} from './render_util'; + +describe('di', () => { + describe('no dependencies', () => { + it('should create directive with no deps', () => { + class Directive { + value: string = 'Created'; + } + const DirectiveDef = defineDirective({type: Directive, factory: () => new Directive}); + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + D(0, DirectiveDef.n(), DirectiveDef); + T(1); + } + e(); + } + t(1, b(D(0).value)); + } + + expect(renderToHtml(Template, {})).toEqual('
      Created
      '); + }); + }); + + describe('view dependencies', () => { + it('should create directive with inter view dependencies', () => { + class DirectiveA { + value: string = 'A'; + } + const DirectiveADef = defineDirective( + {type: DirectiveA, factory: () => new DirectiveA, features: [PublicFeature]}); + + class DirectiveB { + value: string = 'B'; + } + const DirectiveBDef = defineDirective( + {type: DirectiveB, factory: () => new DirectiveB, features: [PublicFeature]}); + + class DirectiveC { + value: string; + constructor(a: DirectiveA, b: DirectiveB) { this.value = a.value + b.value; } + } + const DirectiveCDef = defineDirective({ + type: DirectiveC, + factory: () => new DirectiveC(inject(DirectiveA), inject(DirectiveB)) + }); + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + D(0, DirectiveADef.n(), DirectiveADef); + E(1, 'span'); + { + D(1, DirectiveBDef.n(), DirectiveBDef); + D(2, DirectiveCDef.n(), DirectiveCDef); + T(2); + } + e(); + } + e(); + } + t(2, b(D(2).value)); + } + + expect(renderToHtml(Template, {})).toEqual('
      AB
      '); + }); + }); + + describe('ElementRef', () => { + it('should create directive with ElementRef dependencies', () => { + class Directive { + value: string; + constructor(public elementRef: ElementRef) { + this.value = (elementRef.constructor as any).name; + } + } + const DirectiveDef = defineDirective({ + type: Directive, + factory: () => new Directive(injectElementRef()), + features: [PublicFeature] + }); + + class DirectiveSameInstance { + value: boolean; + constructor(elementRef: ElementRef, directive: Directive) { + this.value = elementRef === directive.elementRef; + } + } + const DirectiveSameInstanceDef = defineDirective({ + type: DirectiveSameInstance, + factory: () => new DirectiveSameInstance(injectElementRef(), inject(Directive)) + }); + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + D(0, DirectiveDef.n(), DirectiveDef); + D(1, DirectiveSameInstanceDef.n(), DirectiveSameInstanceDef); + T(1); + } + e(); + } + t(1, b2('', D(0).value, '-', D(1).value, '')); + } + + expect(renderToHtml(Template, {})).toEqual('
      ElementRef-true
      '); + }); + }); + + describe('TemplateRef', () => { + it('should create directive with TemplateRef dependencies', () => { + class Directive { + value: string; + constructor(public templateRef: TemplateRef) { + this.value = (templateRef.constructor as any).name; + } + } + const DirectiveDef = defineDirective({ + type: Directive, + factory: () => new Directive(injectTemplateRef()), + features: [PublicFeature] + }); + + class DirectiveSameInstance { + value: boolean; + constructor(templateRef: TemplateRef, directive: Directive) { + this.value = templateRef === directive.templateRef; + } + } + const DirectiveSameInstanceDef = defineDirective({ + type: DirectiveSameInstance, + factory: () => new DirectiveSameInstance(injectTemplateRef(), inject(Directive)) + }); + + + function Template(ctx: any, cm: any) { + if (cm) { + C(0, function() {}); + { + D(0, DirectiveDef.n(), DirectiveDef); + D(1, DirectiveSameInstanceDef.n(), DirectiveSameInstanceDef); + } + c(); + T(1); + } + t(1, b2('', D(0).value, '-', D(1).value, '')); + } + + expect(renderToHtml(Template, {})).toEqual('TemplateRef-true'); + }); + }); + + describe('ViewContainerRef', () => { + it('should create directive with ViewContainerRef dependencies', () => { + class Directive { + value: string; + constructor(public viewContainerRef: ViewContainerRef) { + this.value = (viewContainerRef.constructor as any).name; + } + } + const DirectiveDef = defineDirective({ + type: Directive, + factory: () => new Directive(injectViewContainerRef()), + features: [PublicFeature] + }); + + class DirectiveSameInstance { + value: boolean; + constructor(viewContainerRef: ViewContainerRef, directive: Directive) { + this.value = viewContainerRef === directive.viewContainerRef; + } + } + const DirectiveSameInstanceDef = defineDirective({ + type: DirectiveSameInstance, + factory: () => new DirectiveSameInstance(injectViewContainerRef(), inject(Directive)) + }); + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + D(0, DirectiveDef.n(), DirectiveDef); + D(1, DirectiveSameInstanceDef.n(), DirectiveSameInstanceDef); + T(1); + } + e(); + } + t(1, b2('', D(0).value, '-', D(1).value, '')); + } + + expect(renderToHtml(Template, {})).toEqual('
      ViewContainerRef-true
      '); + }); + }); + + describe('inject', () => { + describe('bloom filter', () => { + let di: LNodeInjector; + beforeEach(() => { + di = {} as any; + di.bf0 = 0; + di.bf1 = 0; + di.bf2 = 0; + di.bf3 = 0; + di.cbf0 = 0; + di.cbf1 = 0; + di.cbf2 = 0; + di.cbf3 = 0; + }); + + function bloomState() { return [di.bf3, di.bf2, di.bf1, di.bf0]; } + + it('should add values', () => { + bloomAdd(di, { __NG_ELEMENT_ID__: 0 } as any); + expect(bloomState()).toEqual([0, 0, 0, 1]); + bloomAdd(di, { __NG_ELEMENT_ID__: 32 + 1 } as any); + expect(bloomState()).toEqual([0, 0, 2, 1]); + bloomAdd(di, { __NG_ELEMENT_ID__: 64 + 2 } as any); + expect(bloomState()).toEqual([0, 4, 2, 1]); + bloomAdd(di, { __NG_ELEMENT_ID__: 96 + 3 } as any); + expect(bloomState()).toEqual([8, 4, 2, 1]); + }); + + it('should query values', () => { + bloomAdd(di, { __NG_ELEMENT_ID__: 0 } as any); + bloomAdd(di, { __NG_ELEMENT_ID__: 32 } as any); + bloomAdd(di, { __NG_ELEMENT_ID__: 64 } as any); + bloomAdd(di, { __NG_ELEMENT_ID__: 96 } as any); + + expect(bloomFindPossibleInjector(di, 0)).toEqual(di); + expect(bloomFindPossibleInjector(di, 1)).toEqual(null); + expect(bloomFindPossibleInjector(di, 32)).toEqual(di); + expect(bloomFindPossibleInjector(di, 64)).toEqual(di); + expect(bloomFindPossibleInjector(di, 96)).toEqual(di); + }); + }); + + it('should inject from parent view', () => { + class ParentDirective {} + const ParentDirectiveDef = defineDirective( + {type: ParentDirective, factory: () => new ParentDirective(), features: [PublicFeature]}); + + class ChildDirective { + value: string; + constructor(public parent: ParentDirective) { + this.value = (parent.constructor as any).name; + } + } + const ChildDirectiveDef = defineDirective({ + type: ChildDirective, + factory: () => new ChildDirective(inject(ParentDirective)), + features: [PublicFeature] + }); + + class Child2Directive { + value: boolean; + constructor(parent: ParentDirective, child: ChildDirective) { + this.value = parent === child.parent; + } + } + const Child2DirectiveDef = defineDirective({ + type: Child2Directive, + factory: () => new Child2Directive(inject(ParentDirective), inject(ChildDirective)) + }); + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + D(0, ParentDirectiveDef.n(), ParentDirectiveDef); + C(1); + c(); + } + e(); + } + rC(1); + { + if (V(0)) { + E(0, 'span'); + { + D(0, ChildDirectiveDef.n(), ChildDirectiveDef); + D(1, Child2DirectiveDef.n(), Child2DirectiveDef); + T(1); + } + e(); + } + t(1, b2('', D(0).value, '-', D(1).value, '')); + v(); + } + rc(); + } + + expect(renderToHtml(Template, {})).toEqual('
      ParentDirective-true
      '); + }); + + it('should inject from module Injector', () => { + + }); + }); + + describe('getOrCreateNodeInjector', () => { + it('should handle initial undefined state', () => { + const contentView = createViewState(-1, null !); + const oldView = enterView(contentView, null !); + try { + const parent = createNode(0, LNodeFlags.Element, null, null); + + // Simulate the situation where the previous parent is not initialized. + // This happens on first bootstrap because we don't init existing values + // so that we have smaller HelloWorld. + (parent as{parent: any}).parent = undefined; + + const injector = getOrCreateNodeInjector(); + expect(injector).not.toBe(null); + } finally { + leaveView(oldView); + } + }); + }); + +}); diff --git a/packages/core/test/render3/directive_spec.ts b/packages/core/test/render3/directive_spec.ts new file mode 100644 index 0000000000..92458cbc70 --- /dev/null +++ b/packages/core/test/render3/directive_spec.ts @@ -0,0 +1,46 @@ +/** + * @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 {D, E, b, defineDirective, e, p} from '../../src/render3/index'; + +import {renderToHtml} from './render_util'; + +describe('directive', () => { + + describe('host', () => { + + it('should support host bindings in directives', () => { + let directiveInstance: Directive|undefined; + + class Directive { + klass = 'foo'; + } + const DirectiveDef = defineDirective({ + type: Directive, + factory: () => directiveInstance = new Directive, + refresh: (directiveIndex: number, elementIndex: number) => { + p(elementIndex, 'className', b(D(directiveIndex).klass)); + } + }); + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + { D(0, DirectiveDef.n(), DirectiveDef); } + e(); + } + DirectiveDef.r(0, 0); + } + + expect(renderToHtml(Template, {})).toEqual(''); + directiveInstance !.klass = 'bar'; + expect(renderToHtml(Template, {})).toEqual(''); + }); + + }); +}); diff --git a/packages/core/test/render3/domino.d.ts b/packages/core/test/render3/domino.d.ts new file mode 100644 index 0000000000..ea774b9c78 --- /dev/null +++ b/packages/core/test/render3/domino.d.ts @@ -0,0 +1,12 @@ +/** + * @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 + */ + +declare module 'domino' { + function createWindow(html: string, url: string): Window; + const impl: {Element: any}; +} diff --git a/packages/core/test/render3/exports_spec.ts b/packages/core/test/render3/exports_spec.ts new file mode 100644 index 0000000000..06617a020f --- /dev/null +++ b/packages/core/test/render3/exports_spec.ts @@ -0,0 +1,295 @@ +/** + * @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 {C, D, E, T, V, a, b, c, defineComponent, defineDirective, e, k, p, rC, rc, t, v} from '../../src/render3/index'; + +import {renderToHtml} from './render_util'; + +describe('exports', () => { + it('should support export of DOM element', () => { + + /** {{ myInput.value }} */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'input', ['value', 'one']); + e(); + T(1); + } + let myInput = E(0); + t(1, (myInput as any).value); + } + + expect(renderToHtml(Template, {})).toEqual('one'); + }); + + it('should support basic export of component', () => { + + /** {{ myComp.name }} */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, MyComponent.ngComponentDef); + { D(0, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); } + e(); + T(1); + } + t(1, D(0).name); + } + + class MyComponent { + name = 'Nancy'; + + static ngComponentDef = defineComponent({ + type: MyComponent, + tag: 'comp', + template: function() {}, + factory: () => new MyComponent + }); + } + + expect(renderToHtml(Template, {})).toEqual('Nancy'); + }); + + it('should support component instance fed into directive', () => { + + let myComponent: MyComponent; + let myDir: MyDir; + class MyComponent { + constructor() { myComponent = this; } + static ngComponentDef = defineComponent({ + type: MyComponent, + tag: 'comp', + template: function() {}, + factory: () => new MyComponent + }); + } + + class MyDir { + myDir: MyComponent; + constructor() { myDir = this; } + static ngDirectiveDef = + defineDirective({type: MyDir, factory: () => new MyDir, inputs: {myDir: 'myDir'}}); + } + + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, MyComponent.ngComponentDef); + { D(0, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); } + e(); + E(1, 'div'); + { D(1, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + } + p(1, 'myDir', b(D(0))); + } + + renderToHtml(Template, {}); + expect(myDir !.myDir).toEqual(myComponent !); + }); + + it('should work with directives with exportAs set', () => { + + /**
      {{ myDir.name }} */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + D(0, SomeDirDef.n(), SomeDirDef); + e(); + T(1); + } + t(1, D(0).name); + } + + class SomeDir { + name = 'Drew'; + } + const SomeDirDef = defineDirective({type: SomeDir, factory: () => new SomeDir}); + + expect(renderToHtml(Template, {})).toEqual('
      Drew'); + }); + + describe('forward refs', () => { + it('should work with basic text bindings', () => { + /** {{ myInput.value}} */ + function Template(ctx: any, cm: boolean) { + if (cm) { + T(0); + E(1, 'input', ['value', 'one']); + e(); + } + let myInput = E(1); + t(0, b((myInput as any).value)); + } + + expect(renderToHtml(Template, {})).toEqual('one'); + }); + + + it('should work with element properties', () => { + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + e(); + E(1, 'input', ['value', 'one']); + e(); + } + let myInput = E(1); + p(0, 'title', b(myInput && (myInput as any).value)); + } + + expect(renderToHtml(Template, {})).toEqual('
      '); + }); + + it('should work with element attrs', () => { + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + e(); + E(1, 'input', ['value', 'one']); + e(); + } + let myInput = E(1); + a(0, 'aria-label', b(myInput && (myInput as any).value)); + } + + expect(renderToHtml(Template, {})).toEqual('
      '); + }); + + it('should work with element classes', () => { + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + e(); + E(1, 'input', ['type', 'checkbox', 'checked', 'true']); + e(); + } + let myInput = E(1); + k(0, 'red', b(myInput && (myInput as any).checked)); + } + + expect(renderToHtml(Template, {})) + .toEqual('
      '); + }); + + it('should work with component refs', () => { + + let myComponent: MyComponent; + let myDir: MyDir; + + class MyComponent { + constructor() { myComponent = this; } + + static ngComponentDef = defineComponent({ + type: MyComponent, + tag: 'comp', + template: function(ctx: MyComponent, cm: boolean) {}, + factory: () => new MyComponent + }); + } + + class MyDir { + myDir: MyComponent; + + constructor() { myDir = this; } + + static ngDirectiveDef = + defineDirective({type: MyDir, factory: () => new MyDir, inputs: {myDir: 'myDir'}}); + } + + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + E(1, MyComponent.ngComponentDef); + { D(1, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); } + e(); + } + p(0, 'myDir', b(D(1))); + } + + renderToHtml(Template, {}); + expect(myDir !.myDir).toEqual(myComponent !); + }); + + it('should work with multiple forward refs', () => { + /** {{ myInput.value }} {{ myComp.name }} + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + T(0); + T(1); + E(2, 'comp'); + { D(0, MyComponent.ngComponentDef.n(), MyComponent.ngComponentDef); } + e(); + E(3, 'input', ['value', 'one']); + e(); + } + let myInput = E(3); + let myComp = D(0) as MyComponent; + t(0, b(myInput && (myInput as any).value)); + t(1, b(myComp && myComp.name)); + } + + let myComponent: MyComponent; + + class MyComponent { + name = 'Nancy'; + + constructor() { myComponent = this; } + + static ngComponentDef = defineComponent({ + type: MyComponent, + tag: 'comp', + template: function() {}, + factory: () => new MyComponent + }); + } + expect(renderToHtml(Template, {})).toEqual('oneNancy'); + }); + + it('should work inside a view container', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div'); + { + C(1); + c(); + } + e(); + } + rC(1); + { + if (ctx.condition) { + let cm1 = V(1); + { + if (cm1) { + T(0); + E(1, 'input', ['value', 'one']); + e(); + } + let myInput = E(1); + t(0, b(myInput && (myInput as any).value)); + } + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, { + condition: true + })).toEqual('
      one
      '); + expect(renderToHtml(Template, {condition: false})).toEqual('
      '); + }); + }); +}); diff --git a/packages/core/test/render3/imported_renderer2.ts b/packages/core/test/render3/imported_renderer2.ts new file mode 100644 index 0000000000..cff5f6b84c --- /dev/null +++ b/packages/core/test/render3/imported_renderer2.ts @@ -0,0 +1,57 @@ +/** + * @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 {EventEmitter, NgZone, Renderer2} from '@angular/core'; +import {EventManager, ɵDomEventsPlugin, ɵDomRendererFactory2, ɵDomSharedStylesHost} from '@angular/platform-browser'; + + +// Adapted renderer: it creates a Renderer2 instance and adapts it to Renderer3 +// TODO: remove once this code is in angular/angular +export class NoopNgZone implements NgZone { + readonly hasPendingMicrotasks: boolean = false; + readonly hasPendingMacrotasks: boolean = false; + readonly isStable: boolean = true; + readonly onUnstable: EventEmitter = new EventEmitter(); + readonly onMicrotaskEmpty: EventEmitter = new EventEmitter(); + readonly onStable: EventEmitter = new EventEmitter(); + readonly onError: EventEmitter = new EventEmitter(); + + run(fn: () => any): any { return fn(); } + + runGuarded(fn: () => any): any { return fn(); } + + runOutsideAngular(fn: () => any): any { return fn(); } + + runTask(fn: () => any): T { return fn(); } +} + +// TODO: remove once this code is in angular/angular +export class SimpleDomEventsPlugin extends ɵDomEventsPlugin { + constructor(doc: any, ngZone: NgZone) { super(doc, ngZone); } + + supports(eventName: string): boolean { return true; } + + addEventListener(element: HTMLElement, eventName: string, handler: Function): Function { + let callback: EventListener = handler as EventListener; + element.addEventListener(eventName, callback, false); + return () => this.removeEventListener(element, eventName, callback); + } + + removeEventListener(target: any, eventName: string, callback: Function): void { + return target.removeEventListener.apply(target, [eventName, callback, false]); + } +} + +export function getRenderer2(document: any): Renderer2 { + const fakeNgZone: NgZone = new NoopNgZone(); + const eventManager = + new EventManager([new SimpleDomEventsPlugin(document, fakeNgZone)], fakeNgZone); + const rendererFactory2 = + new ɵDomRendererFactory2(eventManager, new ɵDomSharedStylesHost(document)); + return rendererFactory2.createRenderer(null, null); +} diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts new file mode 100644 index 0000000000..5bf5d7e740 --- /dev/null +++ b/packages/core/test/render3/integration_spec.ts @@ -0,0 +1,592 @@ +/** + * @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 {C, D, E, NC, T, V, a, b, b1, b2, b3, b4, b5, b6, b7, b8, bV, c, defineComponent, e, k, p, r, rC, rc, s, t, v} from '../../src/render3/index'; +import {NO_CHANGE} from '../../src/render3/instructions'; + +import {containerEl, renderToHtml} from './render_util'; + +describe('iv integration test', () => { + + describe('render', () => { + + it('should render basic template', () => { + expect(renderToHtml(Template, {})).toEqual('Greetings'); + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span', ['title', 'Hello']); + { T(1, 'Greetings'); } + e(); + } + } + }); + + it('should render and update basic "Hello, World" template', () => { + expect(renderToHtml(Template, 'World')).toEqual('

      Hello, World!

      '); + expect(renderToHtml(Template, 'New World')).toEqual('

      Hello, New World!

      '); + + function Template(name: string, cm: boolean) { + if (cm) { + E(0, 'h1'); + { T(1); } + e(); + } + t(1, b1('Hello, ', name, '!')); + } + }); + }); + + describe('text bindings', () => { + it('should render "undefined" as "" when used with `bind()`', () => { + function Template(name: string, cm: boolean) { + if (cm) { + T(0); + } + t(0, b(name)); + } + + expect(renderToHtml(Template, 'benoit')).toEqual('benoit'); + expect(renderToHtml(Template, undefined)).toEqual(''); + }); + + it('should render "null" as "" when used with `bind()`', () => { + function Template(name: string, cm: boolean) { + if (cm) { + T(0); + } + t(0, b(name)); + } + + expect(renderToHtml(Template, 'benoit')).toEqual('benoit'); + expect(renderToHtml(Template, null)).toEqual(''); + }); + + it('should support creation-time values in text nodes', () => { + function Template(value: string, cm: boolean) { + if (cm) { + T(0); + } + t(0, cm ? value : NO_CHANGE); + } + expect(renderToHtml(Template, 'once')).toEqual('once'); + expect(renderToHtml(Template, 'twice')).toEqual('once'); + }); + + it('should support creation-time bindings in interpolations', () => { + function Template(v: string, cm: boolean) { + if (cm) { + T(0); + T(1); + T(2); + T(3); + T(4); + T(5); + T(6); + T(7); + T(8); + } + t(0, b1('', cm ? v : NC, '|')); + t(1, b2('', v, '_', cm ? v : NC, '|')); + t(2, b3('', v, '_', v, '_', cm ? v : NC, '|')); + t(3, b4('', v, '_', v, '_', v, '_', cm ? v : NC, '|')); + t(4, b5('', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|')); + t(5, b6('', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|')); + t(6, b7('', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|')); + t(7, b8('', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '|')); + t(8, bV([ + '', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', v, '_', cm ? v : NC, '' + ])); + } + expect(renderToHtml(Template, 'a')) + .toEqual( + 'a|a_a|a_a_a|a_a_a_a|a_a_a_a_a|a_a_a_a_a_a|a_a_a_a_a_a_a|a_a_a_a_a_a_a_a|a_a_a_a_a_a_a_a_a'); + expect(renderToHtml(Template, 'A')) + .toEqual( + 'a|A_a|A_A_a|A_A_A_a|A_A_A_A_a|A_A_A_A_A_a|A_A_A_A_A_A_a|A_A_A_A_A_A_A_a|A_A_A_A_A_A_A_A_a'); + }); + + }); + + describe('Siblings update', () => { + it('should handle a flat list of static/bound text nodes', () => { + function Template(name: string, cm: boolean) { + if (cm) { + T(0, 'Hello '); + T(1); + T(2, '!'); + } + t(1, b(name)); + } + expect(renderToHtml(Template, 'world')).toEqual('Hello world!'); + expect(renderToHtml(Template, 'monde')).toEqual('Hello monde!'); + }); + + it('should handle a list of static/bound text nodes as element children', () => { + function Template(name: string, cm: boolean) { + if (cm) { + E(0, 'b'); + { + T(1, 'Hello '); + T(2); + T(3, '!'); + } + e(); + } + t(2, b(name)); + } + expect(renderToHtml(Template, 'world')).toEqual('Hello world!'); + expect(renderToHtml(Template, 'mundo')).toEqual('Hello mundo!'); + }); + + it('should render/update text node as a child of a deep list of elements', () => { + function Template(name: string, cm: boolean) { + if (cm) { + E(0, 'b'); + { + E(1, 'b'); + { + E(2, 'b'); + { + E(3, 'b'); + { T(4); } + e(); + } + e(); + } + e(); + } + e(); + } + t(4, b1('Hello ', name, '!')); + } + expect(renderToHtml(Template, 'world')).toEqual('Hello world!'); + expect(renderToHtml(Template, 'mundo')).toEqual('Hello mundo!'); + }); + + it('should update 2 sibling elements', () => { + function Template(id: any, cm: boolean) { + if (cm) { + E(0, 'b'); + { + E(1, 'span'); + e(); + E(2, 'span', ['class', 'foo']); + {} + e(); + } + e(); + } + a(2, 'id', b(id)); + } + expect(renderToHtml(Template, 'foo')) + .toEqual(''); + expect(renderToHtml(Template, 'bar')) + .toEqual(''); + }); + + it('should handle sibling text node after element with child text node', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'p'); + { T(1, 'hello'); } + e(); + T(2, 'world'); + } + } + + expect(renderToHtml(Template, null)).toEqual('

      hello

      world'); + }); + }); + + describe('basic components', () => { + + class TodoComponent { + value = ' one'; + + static ngComponentDef = defineComponent({ + type: TodoComponent, + tag: 'todo', + template: function TodoTemplate(ctx: any, cm: boolean) { + if (cm) { + E(0, 'p'); + { + T(1, 'Todo'); + T(2); + } + e(); + } + t(2, b(ctx.value)); + }, + factory: () => new TodoComponent + }); + } + + it('should support a basic component template', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, TodoComponent.ngComponentDef); + { D(0, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); } + e(); + } + TodoComponent.ngComponentDef.r(0, 0); + } + + expect(renderToHtml(Template, null)).toEqual('

      Todo one

      '); + }); + + it('should support a component template with sibling', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, TodoComponent.ngComponentDef); + { D(0, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); } + e(); + T(1, 'two'); + } + TodoComponent.ngComponentDef.r(0, 0); + } + expect(renderToHtml(Template, null)).toEqual('

      Todo one

      two'); + }); + + it('should support a component template with component sibling', () => { + /** + * + * + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, TodoComponent.ngComponentDef); + { D(0, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); } + e(); + E(1, TodoComponent.ngComponentDef); + { D(1, TodoComponent.ngComponentDef.n(), TodoComponent.ngComponentDef); } + e(); + } + TodoComponent.ngComponentDef.r(0, 0); + TodoComponent.ngComponentDef.r(1, 1); + } + expect(renderToHtml(Template, null)) + .toEqual('

      Todo one

      Todo one

      '); + }); + + it('should support a component with binding on host element', () => { + let cmptInstance: TodoComponentHostBinding|null; + + class TodoComponentHostBinding { + title = 'one'; + static ngComponentDef = defineComponent({ + type: TodoComponentHostBinding, + tag: 'todo', + template: function TodoComponentHostBindingTemplate( + ctx: TodoComponentHostBinding, cm: boolean) { + if (cm) { + T(0); + } + t(0, b(ctx.title)); + }, + factory: () => cmptInstance = new TodoComponentHostBinding, + refresh: function(directiveIndex: number, elementIndex: number): void { + // host bindings + p(elementIndex, 'title', b(D(directiveIndex).title)); + // refresh component's template + r(directiveIndex, elementIndex, this.template); + } + }); + } + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, TodoComponentHostBinding.ngComponentDef); + { + D(0, TodoComponentHostBinding.ngComponentDef.n(), + TodoComponentHostBinding.ngComponentDef); + } + e(); + } + TodoComponentHostBinding.ngComponentDef.r(0, 0); + } + + expect(renderToHtml(Template, {})).toEqual('one'); + cmptInstance !.title = 'two'; + expect(renderToHtml(Template, {})).toEqual('two'); + }); + + it('should support component with bindings in template', () => { + /**

      {{ name }}

      */ + class MyComp { + name = 'Bess'; + static ngComponentDef = defineComponent({ + type: MyComp, + tag: 'comp', + template: function MyCompTemplate(ctx: any, cm: boolean) { + if (cm) { + E(0, 'p'); + { T(1); } + e(); + } + t(1, b(ctx.name)); + }, + factory: () => new MyComp + }); + } + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, MyComp.ngComponentDef); + { D(0, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); } + e(); + } + MyComp.ngComponentDef.r(0, 0); + } + + expect(renderToHtml(Template, null)).toEqual('

      Bess

      '); + }); + + it('should support a component with sub-views', () => { + /** + * % if (condition) { + *
      text
      + * % } + */ + class MyComp { + condition: boolean; + static ngComponentDef = defineComponent({ + type: MyComp, + tag: 'comp', + template: function MyCompTemplate(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, 'div'); + { T(1, 'text'); } + e(); + } + v(); + } + } + rc(); + }, + factory: () => new MyComp, + inputs: {condition: 'condition'} + }); + } + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, MyComp.ngComponentDef); + { D(0, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); } + e(); + } + p(0, 'condition', b(ctx.condition)); + MyComp.ngComponentDef.r(0, 0); + } + + expect(renderToHtml(Template, {condition: true})).toEqual('
      text
      '); + expect(renderToHtml(Template, {condition: false})).toEqual(''); + + }); + + }); + + describe('element bindings', () => { + + describe('elementAttribute', () => { + it('should support attribute bindings', () => { + const ctx: {title: string | null} = {title: 'Hello'}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + a(0, 'title', b(ctx.title)); + } + + // initial binding + expect(renderToHtml(Template, ctx)).toEqual(''); + + // update binding + ctx.title = 'Hi!'; + expect(renderToHtml(Template, ctx)).toEqual(''); + + // remove attribute + ctx.title = null; + expect(renderToHtml(Template, ctx)).toEqual(''); + }); + + it('should stringify values used attribute bindings', () => { + const ctx: {title: any} = {title: NaN}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + a(0, 'title', b(ctx.title)); + } + + expect(renderToHtml(Template, ctx)).toEqual(''); + + ctx.title = {toString: () => 'Custom toString'}; + expect(renderToHtml(Template, ctx)).toEqual(''); + }); + + it('should update bindings', () => { + function Template(c: any, cm: boolean) { + if (cm) { + E(0, 'b'); + e(); + } + a(0, 'a', bV(c)); + a(0, 'a0', b(c[1])); + a(0, 'a1', b1(c[0], c[1], c[16])); + a(0, 'a2', b2(c[0], c[1], c[2], c[3], c[16])); + a(0, 'a3', b3(c[0], c[1], c[2], c[3], c[4], c[5], c[16])); + a(0, 'a4', b4(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[16])); + a(0, 'a5', b5(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[16])); + a(0, 'a6', + b6(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11], c[16])); + a(0, 'a7', b7(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11], + c[12], c[13], c[16])); + a(0, 'a8', b8(c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10], c[11], + c[12], c[13], c[14], c[15], c[16])); + } + let args = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')']; + expect(renderToHtml(Template, args)) + .toEqual( + ''); + args = args.reverse(); + expect(renderToHtml(Template, args)) + .toEqual( + ''); + args = args.reverse(); + expect(renderToHtml(Template, args)) + .toEqual( + ''); + }); + + it('should not update DOM if context has not changed', () => { + const ctx: {title: string | null} = {title: 'Hello'}; + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + C(1); + c(); + e(); + } + a(0, 'title', b(ctx.title)); + rC(1); + { + if (true) { + let cm1 = V(1); + { + if (cm1) { + E(0, 'b'); + {} + e(); + } + a(0, 'title', b(ctx.title)); + } + v(); + } + } + rc(); + } + + // initial binding + expect(renderToHtml(Template, ctx)) + .toEqual(''); + // update DOM manually + containerEl.querySelector('b') !.setAttribute('title', 'Goodbye'); + // refresh with same binding + expect(renderToHtml(Template, ctx)) + .toEqual(''); + // refresh again with same binding + expect(renderToHtml(Template, ctx)) + .toEqual(''); + }); + }); + + describe('elementStyle', () => { + + it('should support binding to styles', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + s(0, 'border-color', b(ctx)); + } + + expect(renderToHtml(Template, 'red')).toEqual(''); + expect(renderToHtml(Template, 'green')) + .toEqual(''); + expect(renderToHtml(Template, null)).toEqual(''); + }); + + it('should support binding to styles with suffix', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + s(0, 'font-size', b(ctx), 'px'); + } + + expect(renderToHtml(Template, '100')).toEqual(''); + expect(renderToHtml(Template, 200)).toEqual(''); + expect(renderToHtml(Template, null)).toEqual(''); + }); + }); + + describe('elementClass', () => { + + it('should support CSS class toggle', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + k(0, 'active', b(ctx)); + } + + expect(renderToHtml(Template, true)).toEqual(''); + expect(renderToHtml(Template, false)).toEqual(''); + + // truthy values + expect(renderToHtml(Template, 'a_string')).toEqual(''); + expect(renderToHtml(Template, 10)).toEqual(''); + + // falsy values + expect(renderToHtml(Template, '')).toEqual(''); + expect(renderToHtml(Template, 0)).toEqual(''); + }); + + it('should work correctly with existing static classes', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span', ['class', 'existing']); + e(); + } + k(0, 'active', b(ctx)); + } + + expect(renderToHtml(Template, true)).toEqual(''); + expect(renderToHtml(Template, false)).toEqual(''); + }); + }); + }); + +}); diff --git a/packages/core/test/render3/lifecycle_spec.ts b/packages/core/test/render3/lifecycle_spec.ts new file mode 100644 index 0000000000..94b7f58ee3 --- /dev/null +++ b/packages/core/test/render3/lifecycle_spec.ts @@ -0,0 +1,425 @@ +/** + * @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 {C, ComponentTemplate, D, E, L, LifeCycleGuard, T, V, b, c, defineComponent, e, l, p, rC, rc, v} from '../../src/render3/index'; +import {containerEl, renderToHtml} from './render_util'; + +describe('lifecycles', () => { + + describe('onDestroy', () => { + let events: string[]; + + beforeEach(() => { events = []; }); + + let Comp = createOnDestroyComponent('comp', function(ctx: any, cm: boolean) {}); + let Parent = createOnDestroyComponent('parent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + Comp.ngComponentDef.r(0, 0); + }); + + function createOnDestroyComponent(name: string, template: ComponentTemplate) { + return class Component { + val: string = ''; + ngOnDestroy() { events.push(`${name}${this.val}`); } + + static ngComponentDef = defineComponent({ + type: Component, + tag: name, + factory: () => { + const comp = new Component(); + l(LifeCycleGuard.ON_DESTROY, comp, comp.ngOnDestroy); + return comp; + }, + inputs: {val: 'val'}, + template: template + }); + }; + } + + it('should call destroy when view is removed', () => { + /** + * % if (condition) { + * + * % } + */ + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + Comp.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + } + + renderToHtml(Template, {condition: true}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp']); + }); + + it('should call destroy when multiple views are removed', () => { + /** + * % if (condition) { + * + * + * % } + */ + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + E(1, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', b('1')); + p(1, 'val', b('2')); + Comp.ngComponentDef.r(0, 0); + Comp.ngComponentDef.r(1, 1); + v(); + } + } + rc(); + } + + renderToHtml(Template, {condition: true}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp1', 'comp2']); + }); + + it('should be called in child components before parent components', () => { + /** + * % if (condition) { + * + * % } + * + * parent template: + */ + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, Parent.ngComponentDef); + { D(0, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + Parent.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + } + + renderToHtml(Template, {condition: true}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp', 'parent']); + }); + + it('should be called bottom up with children nested 2 levels deep', () => { + /** + * % if (condition) { + * + * % } + * + * grandparent template: + * parent template: + */ + + let Grandparent = createOnDestroyComponent('grandparent', function(ctx: any, cm: boolean) { + if (cm) { + E(0, Parent.ngComponentDef); + { D(0, Parent.ngComponentDef.n(), Parent.ngComponentDef); } + e(); + } + Parent.ngComponentDef.r(0, 0); + }); + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, Grandparent.ngComponentDef); + { D(0, Grandparent.ngComponentDef.n(), Grandparent.ngComponentDef); } + e(); + } + Grandparent.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + } + + renderToHtml(Template, {condition: true}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp', 'parent', 'grandparent']); + }); + + + it('should be called in consistent order if views are removed and re-added', () => { + /** + * % if (condition) { + * + * % if (condition2) { + * + * % } + * + * % } + */ + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + C(1); + c(); + E(2, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', b('1')); + Comp.ngComponentDef.r(0, 0); + rC(1); + { + if (ctx.condition2) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', b('2')); + Comp.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + p(2, 'val', b('3')); + Comp.ngComponentDef.r(1, 2); + v(); + } + } + rc(); + } + + renderToHtml(Template, {condition: true, condition2: true}); + renderToHtml(Template, {condition: false}); + + /** + * Current angular will process in this same order (root is the top-level removed view): + * + * root.child (comp1 view) onDestroy: null + * root.child.next (container) -> embeddedView + * embeddedView.child (comp2 view) onDestroy: null + * embeddedView onDestroy: [comp2] + * root.child.next.next (comp3 view) onDestroy: null + * root onDestroy: [comp1, comp3] + */ + expect(events).toEqual(['comp2', 'comp1', 'comp3']); + + events = []; + renderToHtml(Template, {condition: true, condition2: false}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp1', 'comp3']); + + events = []; + renderToHtml(Template, {condition: true, condition2: true}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp2', 'comp1', 'comp3']); + }); + + it('should be called in every iteration of a destroyed for loop', () => { + /** + * % if (condition) { + * + * % for (let i = 2; i < len; i++) { + * + * % } + * + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + C(1); + c(); + E(2, Comp.ngComponentDef); + { D(1, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', b('1')); + Comp.ngComponentDef.r(0, 0); + rC(1); + { + for (let j = 2; j < ctx.len; j++) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'val', b(j)); + Comp.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + p(2, 'val', b('5')); + Comp.ngComponentDef.r(1, 2); + v(); + } + } + rc(); + } + + /** + * Current angular will process in this same order (root is the top-level removed view): + * + * root.child (comp1 view) onDestroy: null + * root.child.next (container) -> embeddedView (children[0].data) + * embeddedView.child (comp2 view) onDestroy: null + * embeddedView onDestroy: [comp2] + * embeddedView.next.child (comp3 view) onDestroy: null + * embeddedView.next onDestroy: [comp3] + * embeddedView.next.next.child (comp4 view) onDestroy: null + * embeddedView.next.next onDestroy: [comp4] + * embeddedView.next.next -> container -> root + * root onDestroy: [comp1, comp5] + */ + renderToHtml(Template, {condition: true, len: 5}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']); + + events = []; + renderToHtml(Template, {condition: true, len: 4}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp2', 'comp3', 'comp1', 'comp5']); + + events = []; + renderToHtml(Template, {condition: true, len: 5}); + renderToHtml(Template, {condition: false}); + expect(events).toEqual(['comp2', 'comp3', 'comp4', 'comp1', 'comp5']); + }); + + it('should call destroy properly if view also has listeners', () => { + /** + * % if (condition) { + * + * + * + * % } + */ + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, 'button'); + { + L('click', ctx.onClick.bind(ctx)); + T(1, 'Click me'); + } + e(); + E(2, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + E(3, 'button'); + { + L('click', ctx.onClick.bind(ctx)); + T(4, 'Click me'); + } + e(); + } + Comp.ngComponentDef.r(0, 2); + v(); + } + } + rc(); + } + + class App { + counter = 0; + condition = true; + onClick() { this.counter++; } + } + + const ctx: {counter: number} = new App(); + renderToHtml(Template, ctx); + + const buttons = containerEl.querySelectorAll('button') !; + buttons[0].click(); + expect(ctx.counter).toEqual(1); + buttons[1].click(); + expect(ctx.counter).toEqual(2); + + renderToHtml(Template, {condition: false}); + + buttons[0].click(); + buttons[1].click(); + expect(events).toEqual(['comp']); + expect(ctx.counter).toEqual(2); + }); + + + }); + +}); diff --git a/packages/core/test/render3/listeners_spec.ts b/packages/core/test/render3/listeners_spec.ts new file mode 100644 index 0000000000..89bb7acc49 --- /dev/null +++ b/packages/core/test/render3/listeners_spec.ts @@ -0,0 +1,324 @@ +/** + * @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 {C, D, E, L, T, V, c, defineComponent, e, rC, rc, v} from '../../src/render3/index'; +import {containerEl, renderComponent, renderToHtml} from './render_util'; + + +describe('event listeners', () => { + let comps: MyComp[] = []; + + class MyComp { + showing = true; + counter = 0; + + onClick() { this.counter++; } + + static ngComponentDef = defineComponent({ + type: MyComp, + tag: 'comp', + /** */ + template: function CompTemplate(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + L('click', ctx.onClick.bind(ctx)); + T(1, 'Click me'); + } + e(); + } + }, + factory: () => { + let comp = new MyComp(); + comps.push(comp); + return comp; + } + }); + } + + beforeEach(() => { comps = []; }); + + it('should call function on event emit', () => { + const comp = renderComponent(MyComp); + const button = containerEl.querySelector('button') !; + button.click(); + expect(comp.counter).toEqual(1); + + button.click(); + expect(comp.counter).toEqual(2); + }); + + it('should evaluate expression on event emit', () => { + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + L('click', () => ctx.showing = !ctx.showing); + T(1, 'Click me'); + } + e(); + } + } + + const ctx = {showing: false}; + renderToHtml(Template, ctx); + const button = containerEl.querySelector('button') !; + + button.click(); + expect(ctx.showing).toBe(true); + + button.click(); + expect(ctx.showing).toBe(false); + }); + + it('should support listeners in views', () => { + + /** + * % if (ctx.showing) { + * + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + } + rC(0); + { + if (ctx.showing) { + if (V(1)) { + E(0, 'button'); + { + L('click', ctx.onClick.bind(ctx)); + T(1, 'Click me'); + } + e(); + } + v(); + } + } + rc(); + } + + let comp = new MyComp(); + renderToHtml(Template, comp); + const button = containerEl.querySelector('button') !; + + button.click(); + expect(comp.counter).toEqual(1); + + button.click(); + expect(comp.counter).toEqual(2); + + // the listener should be removed when the view is removed + comp.showing = false; + renderToHtml(Template, comp); + button.click(); + expect(comp.counter).toEqual(2); + }); + + it('should destroy listeners in nested views', () => { + + /** + * % if (showing) { + * Hello + * % if (button) { + * + * % } + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.showing) { + if (V(0)) { + T(0, 'Hello'); + C(1); + c(); + } + rC(1); + { + if (ctx.button) { + if (V(0)) { + E(0, 'button'); + { + L('click', ctx.onClick.bind(ctx)); + T(1, 'Click'); + } + e(); + } + v(); + } + } + rc(); + v(); + } + } + rc(); + } + + const comp = {showing: true, counter: 0, button: true, onClick: function() { this.counter++; }}; + renderToHtml(Template, comp); + const button = containerEl.querySelector('button') !; + + button.click(); + expect(comp.counter).toEqual(1); + + // the child view listener should be removed when the parent view is removed + comp.showing = false; + renderToHtml(Template, comp); + button.click(); + expect(comp.counter).toEqual(1); + }); + + it('should destroy listeners in component views', () => { + + /** + * % if (showing) { + * Hello + * + * + * % } + * + * comp: + * + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.showing) { + if (V(0)) { + T(0, 'Hello'); + E(1, MyComp.ngComponentDef); + { D(0, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); } + e(); + E(2, MyComp.ngComponentDef); + { D(1, MyComp.ngComponentDef.n(), MyComp.ngComponentDef); } + e(); + } + MyComp.ngComponentDef.r(0, 1); + MyComp.ngComponentDef.r(1, 2); + v(); + } + } + rc(); + } + + const ctx = {showing: true}; + renderToHtml(Template, ctx); + const buttons = containerEl.querySelectorAll('button') !; + + buttons[0].click(); + expect(comps[0] !.counter).toEqual(1); + + buttons[1].click(); + expect(comps[1] !.counter).toEqual(1); + + // the child view listener should be removed when the parent view is removed + ctx.showing = false; + renderToHtml(Template, ctx); + buttons[0].click(); + buttons[1].click(); + expect(comps[0] !.counter).toEqual(1); + expect(comps[1] !.counter).toEqual(1); + }); + + it('should support listeners with sibling nested containers', () => { + /** + * % if (condition) { + * Hello + * % if (sub1) { + * + * % } + * + * % if (sub2) { + * + * % } + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + T(0, 'Hello'); + C(1); + c(); + C(2); + c(); + } + rC(1); + { + if (ctx.sub1) { + if (V(0)) { + E(0, 'button'); + { + L('click', () => ctx.counter1++); + T(1, 'Click'); + } + e(); + } + v(); + } + } + rc(); + rC(2); + { + if (ctx.sub2) { + if (V(0)) { + E(0, 'button'); + { + L('click', () => ctx.counter2++); + T(1, 'Click'); + } + e(); + } + v(); + } + } + rc(); + v(); + } + } + rc(); + } + + const ctx = {condition: true, counter1: 0, counter2: 0, sub1: true, sub2: true}; + renderToHtml(Template, ctx); + const buttons = containerEl.querySelectorAll('button') !; + + buttons[0].click(); + expect(ctx.counter1).toEqual(1); + + buttons[1].click(); + expect(ctx.counter2).toEqual(1); + + // the child view listeners should be removed when the parent view is removed + ctx.condition = false; + renderToHtml(Template, ctx); + buttons[0].click(); + buttons[1].click(); + expect(ctx.counter1).toEqual(1); + expect(ctx.counter2).toEqual(1); + + }); + +}); diff --git a/packages/core/test/render3/load_domino.ts b/packages/core/test/render3/load_domino.ts new file mode 100644 index 0000000000..5857630e34 --- /dev/null +++ b/packages/core/test/render3/load_domino.ts @@ -0,0 +1,19 @@ +/** + * @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 + */ + +if (typeof window == 'undefined') { + const createWindow = require('domino').createWindow; + const window = createWindow('', 'http://localhost'); + (global as any).document = window.document; + + // Trick to avoid Event patching from + // https://github.com/angular/angular/blob/7cf5e95ac9f0f2648beebf0d5bd9056b79946970/packages/platform-browser/src/dom/events/dom_events.ts#L112-L132 + // It fails with Domino with TypeError: Cannot assign to read only property + // 'stopImmediatePropagation' of object '#' + (global as any).Event = null; +} diff --git a/packages/core/test/render3/node_selector_matcher_spec.ts b/packages/core/test/render3/node_selector_matcher_spec.ts new file mode 100644 index 0000000000..82a9c27372 --- /dev/null +++ b/packages/core/test/render3/node_selector_matcher_spec.ts @@ -0,0 +1,177 @@ +/** + * @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 {CSSSelector, CSSSelectorWithNegations, NodeBindings, SimpleCSSSelector} from '../../src/render3/interfaces'; +import {isNodeMatchingSelector, isNodeMatchingSelectorWithNegations, isNodeMatchingSimpleSelector} from '../../src/render3/node_selector_matcher'; + +function testLStaticData(tagName: string, attrs: string[] | null): NodeBindings { + return {tagName, attrs, initialInputs: undefined, inputs: undefined, outputs: undefined}; +} + +describe('css selector matching', () => { + + describe('isNodeMatchingSimpleSelector', () => { + + function isMatching( + tagName: string, attrs: string[] | null, selector: SimpleCSSSelector): boolean { + return isNodeMatchingSimpleSelector(testLStaticData(tagName, attrs), selector); + } + + describe('element matching', () => { + + it('should match element name only if names are the same', () => { + expect(isMatching('span', null, ['span'])).toBeTruthy(); + expect(isMatching('span', null, ['div'])).toBeFalsy(); + }); + + /** + * We assume that compiler will lower-case tag names both in LNode + * and in a selector. + */ + it('should match element name case-sensitively', () => { + expect(isMatching('span', null, ['SPAN'])).toBeFalsy(); + expect(isMatching('SPAN', null, ['span'])).toBeFalsy(); + }); + + }); + + describe('attributes matching', () => { + + // TODO: do we need to differentiate no value and empty value? that is: title vs. title="" ? + + it('should match single attribute without value', () => { + expect(isMatching('span', ['title', ''], ['', 'title', ''])).toBeTruthy(); + expect(isMatching('span', ['title', 'my title'], ['', 'title', ''])).toBeTruthy(); + expect(isMatching('span', null, ['', 'title', ''])).toBeFalsy(); + expect(isMatching('span', ['title', ''], ['', 'other', ''])).toBeFalsy(); + }); + + it('should match selector with one attribute without value when element has several attributes', + () => { + expect(isMatching('span', ['id', 'my_id', 'title', 'test_title'], [ + '', 'title', '' + ])).toBeTruthy(); + }); + + + it('should match single attribute with value', () => { + expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'My Title'])).toBeTruthy(); + expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'Other Title'])).toBeFalsy(); + }); + + it('should match single attribute with value', () => { + expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'My Title'])).toBeTruthy(); + expect(isMatching('span', ['title', 'My Title'], ['', 'title', 'Other Title'])).toBeFalsy(); + }); + + it('should not match attribute when element name does not match', () => { + expect(isMatching('span', ['title', 'My Title'], ['div', 'title', ''])).toBeFalsy(); + expect(isMatching('span', ['title', 'My Title'], ['div', 'title', 'My title'])).toBeFalsy(); + }); + + /** + * We assume that compiler will lower-case all attribute names when generating code + */ + it('should match attribute name case-sensitively', () => { + expect(isMatching('span', ['foo', ''], ['', 'foo', ''])).toBeTruthy(); + expect(isMatching('span', ['foo', ''], ['', 'Foo', ''])).toBeFalsy(); + }); + + it('should match attribute values case-sensitively', () => { + expect(isMatching('span', ['foo', 'Bar'], ['', 'foo', 'Bar'])).toBeTruthy(); + expect(isMatching('span', ['foo', 'Bar'], ['', 'Foo', 'bar'])).toBeFalsy(); + }); + + it('should match class as an attribute', () => { + expect(isMatching('span', ['class', 'foo'], ['', 'class', ''])).toBeTruthy(); + expect(isMatching('span', ['class', 'foo'], ['', 'class', 'foo'])).toBeTruthy(); + }); + }); + + describe('class matching', () => { + + it('should match with a class selector when an element has multiple classes', () => { + expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'foo'])).toBeTruthy(); + expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'bar'])).toBeTruthy(); + expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'baz'])).toBeFalsy(); + }); + + it('should not match on partial class name', () => { + expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'foo'])).toBeFalsy(); + expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'bar'])).toBeFalsy(); + expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'ob'])).toBeFalsy(); + expect(isMatching('span', ['class', 'foobar'], ['', 'class', 'foobar'])).toBeTruthy(); + }); + + it('should support selectors with multiple classes', () => { + expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'foo', 'bar'])).toBeTruthy(); + expect(isMatching('span', ['class', 'foo'], ['', 'class', 'foo', 'bar'])).toBeFalsy(); + expect(isMatching('span', ['class', 'bar'], ['', 'class', 'foo', 'bar'])).toBeFalsy(); + }); + + it('should support selectors with multiple classes regardless of class name order', () => { + expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'foo', 'bar'])).toBeTruthy(); + expect(isMatching('span', ['class', 'foo bar'], ['', 'class', 'bar', 'foo'])).toBeTruthy(); + expect(isMatching('span', ['class', 'bar foo'], ['', 'class', 'foo', 'bar'])).toBeTruthy(); + expect(isMatching('span', ['class', 'bar foo'], ['', 'class', 'bar', 'foo'])).toBeTruthy(); + }); + + it('should match class name case-sensitively', () => { + expect(isMatching('span', ['class', 'Foo'], ['', 'class', 'Foo'])).toBeTruthy(); + expect(isMatching('span', ['class', 'Foo'], ['', 'class', 'foo'])).toBeFalsy(); + }); + + }); + + }); + + describe('isNodeMatchingSelectorWithNegations', () => { + function isMatching( + tagName: string, attrs: string[] | null, selector: CSSSelectorWithNegations): boolean { + return isNodeMatchingSelectorWithNegations(testLStaticData(tagName, attrs), selector); + } + + it('should match when negation part is null', () => { + expect(isMatching('span', null, [['span'], null])).toBeTruthy(); + }); + + it('should not match when negation part does not match', () => { + // not matching ":not(span)" + expect(isMatching('span', ['foo', ''], [null, [['span']]])).toBeFalsy(); + // not matching ":not([foo])" + expect(isMatching('span', ['foo', ''], [['span'], [['', 'foo', '']]])).toBeFalsy(); + }); + }); + + describe('isNodeMatchingSelector', () => { + + function isMatching(tagName: string, attrs: string[] | null, selector: CSSSelector): boolean { + return isNodeMatchingSelector(testLStaticData(tagName, attrs), selector); + } + + it('should match when there is only one simple selector without negations', () => { + expect(isMatching('span', null, [[['span'], null]])).toBeTruthy(); + expect(isMatching('span', null, [[['div'], null]])).toBeFalsy(); + }); + + it('should atch when there are multiple parts and only one is matching', () => { + // matching "div, [foo=bar]" + expect(isMatching('span', ['foo', 'bar'], [ + [['div'], null], [['', 'foo', 'bar'], null] + ])).toBeTruthy(); + }); + + it('should not match when there are multiple parts and none is matching', () => { + // not matching "div, [foo=baz]" + expect(isMatching('span', ['foo', 'bar'], [ + [['div'], null], [['', 'foo', 'baz'], null] + ])).toBeFalsy(); + }); + }); + +}); diff --git a/packages/core/test/render3/outputs_spec.ts b/packages/core/test/render3/outputs_spec.ts new file mode 100644 index 0000000000..1a8ca3e231 --- /dev/null +++ b/packages/core/test/render3/outputs_spec.ts @@ -0,0 +1,400 @@ +/** + * @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 {EventEmitter} from '@angular/core'; + +import {C, D, E, L, LifeCycleGuard, T, V, b, c, defineComponent, defineDirective, e, l, p, rC, rc, v} from '../../src/render3/index'; + +import {containerEl, renderToHtml} from './render_util'; + +describe('outputs', () => { + let buttonToggle: ButtonToggle; + + class ButtonToggle { + change = new EventEmitter(); + resetStream = new EventEmitter(); + + static ngComponentDef = defineComponent({ + tag: 'button-toggle', + type: ButtonToggle, + template: function(ctx: any, cm: boolean) {}, + factory: () => buttonToggle = new ButtonToggle(), + outputs: {change: 'change', resetStream: 'reset'} + }); + } + + + it('should call component output function when event is emitted', () => { + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + ButtonToggle.ngComponentDef.r(0, 0); + } + + let counter = 0; + const ctx = {onChange: () => counter++}; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + + buttonToggle !.change.next(); + expect(counter).toEqual(2); + }); + + it('should support more than 1 output function on the same node', () => { + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + L('change', ctx.onChange.bind(ctx)); + L('reset', ctx.onReset.bind(ctx)); + } + e(); + } + ButtonToggle.ngComponentDef.r(0, 0); + } + + let counter = 0; + let resetCounter = 0; + const ctx = {onChange: () => counter++, onReset: () => resetCounter++}; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + + buttonToggle !.resetStream.next(); + expect(resetCounter).toEqual(1); + }); + + it('should eval component output expression when event is emitted', () => { + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + L('change', () => ctx.counter++); + } + e(); + } + ButtonToggle.ngComponentDef.r(0, 0); + } + + const ctx = {counter: 0}; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(ctx.counter).toEqual(1); + + buttonToggle !.change.next(); + expect(ctx.counter).toEqual(2); + }); + + it('should unsubscribe from output when view is destroyed', () => { + + /** + * % if (condition) { + * + * % } + */ + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + ButtonToggle.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + } + + let counter = 0; + const ctx = {onChange: () => counter++, condition: true}; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + + ctx.condition = false; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + }); + + it('should unsubscribe from output in nested view', () => { + + /** + * % if (condition) { + * % if (condition2) { + * + * % } + * % } + */ + + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition2) { + if (V(0)) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + ButtonToggle.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + v(); + } + } + rc(); + } + + let counter = 0; + const ctx = {onChange: () => counter++, condition: true, condition2: true}; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + + ctx.condition = false; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + }); + + it('should work properly when view also has listeners and destroys', () => { + let destroyComp: DestroyComp; + + class DestroyComp { + events: string[] = []; + ngOnDestroy() { this.events.push('destroy'); } + + static ngComponentDef = defineComponent({ + tag: 'destroy-comp', + type: DestroyComp, + template: function(ctx: any, cm: boolean) {}, + factory: () => { + destroyComp = new DestroyComp(); + l(LifeCycleGuard.ON_DESTROY, destroyComp, destroyComp.ngOnDestroy); + return destroyComp; + } + }); + } + + /** + * % if (condition) { + * + * + * + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + if (ctx.condition) { + if (V(0)) { + E(0, 'button'); + { + L('click', ctx.onClick.bind(ctx)); + T(1, 'Click me'); + } + e(); + E(2, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + E(3, DestroyComp.ngComponentDef); + { D(1, DestroyComp.ngComponentDef.n(), DestroyComp.ngComponentDef); } + e(); + } + ButtonToggle.ngComponentDef.r(0, 2); + DestroyComp.ngComponentDef.r(1, 3); + v(); + } + } + rc(); + } + + let clickCounter = 0; + let changeCounter = 0; + const ctx = {condition: true, onChange: () => changeCounter++, onClick: () => clickCounter++}; + renderToHtml(Template, ctx); + + buttonToggle !.change.next(); + expect(changeCounter).toEqual(1); + expect(clickCounter).toEqual(0); + + const button = containerEl.querySelector('button'); + button !.click(); + expect(changeCounter).toEqual(1); + expect(clickCounter).toEqual(1); + + ctx.condition = false; + renderToHtml(Template, ctx); + + expect(destroyComp !.events).toEqual(['destroy']); + + buttonToggle !.change.next(); + button !.click(); + expect(changeCounter).toEqual(1); + expect(clickCounter).toEqual(1); + }); + + it('should fire event listeners along with outputs if they match', () => { + let buttonDir: MyButton; + + /** */ + class MyButton { + click = new EventEmitter(); + + static ngDirectiveDef = defineDirective( + {type: MyButton, factory: () => buttonDir = new MyButton, outputs: {click: 'click'}}); + } + + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef); + L('click', ctx.onClick.bind(ctx)); + } + e(); + } + } + + let counter = 0; + renderToHtml(Template, {counter, onClick: () => counter++}); + + // To match current Angular behavior, the click listener is still + // set up in addition to any matching outputs. + const button = containerEl.querySelector('button') !; + button.click(); + expect(counter).toEqual(1); + + buttonDir !.click.next(); + expect(counter).toEqual(2); + }); + + it('should work with two outputs of the same name', () => { + let otherDir: OtherDir; + + class OtherDir { + change = new EventEmitter(); + + static ngDirectiveDef = defineDirective( + {type: OtherDir, factory: () => otherDir = new OtherDir, outputs: {change: 'change'}}); + } + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + D(1, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + ButtonToggle.ngComponentDef.r(0, 0); + } + + let counter = 0; + renderToHtml(Template, {counter, onChange: () => counter++}); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + + otherDir !.change.next(); + expect(counter).toEqual(2); + }); + + it('should work with an input and output of the same name', () => { + let otherDir: OtherDir; + + class OtherDir { + change: boolean; + + static ngDirectiveDef = defineDirective( + {type: OtherDir, factory: () => otherDir = new OtherDir, inputs: {change: 'change'}}); + } + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, ButtonToggle.ngComponentDef); + { + D(0, ButtonToggle.ngComponentDef.n(), ButtonToggle.ngComponentDef); + D(1, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + p(0, 'change', b(ctx.change)); + ButtonToggle.ngComponentDef.r(0, 0); + } + + let counter = 0; + renderToHtml(Template, {counter, onChange: () => counter++, change: true}); + expect(otherDir !.change).toEqual(true); + + renderToHtml(Template, {counter, onChange: () => counter++, change: false}); + expect(otherDir !.change).toEqual(false); + + buttonToggle !.change.next(); + expect(counter).toEqual(1); + }); + +}); diff --git a/packages/core/test/render3/properties_spec.ts b/packages/core/test/render3/properties_spec.ts new file mode 100644 index 0000000000..6b68088306 --- /dev/null +++ b/packages/core/test/render3/properties_spec.ts @@ -0,0 +1,436 @@ +/** + * @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 {EventEmitter} from '@angular/core'; + +import {C, D, E, L, T, V, b, b1, c, defineComponent, defineDirective, e, p, rC, rc, t, v} from '../../src/render3/index'; +import {NO_CHANGE} from '../../src/render3/instructions'; + +import {renderToHtml} from './render_util'; + +describe('elementProperty', () => { + + it('should support bindings to properties', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + p(0, 'id', b(ctx)); + } + + expect(renderToHtml(Template, 'testId')).toEqual(''); + expect(renderToHtml(Template, 'otherId')).toEqual(''); + }); + + it('should support creation time bindings to properties', () => { + function expensive(ctx: string): any { + if (ctx === 'cheapId') { + return ctx; + } else { + throw 'Too expensive!'; + } + } + + function Template(ctx: string, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + p(0, 'id', cm ? expensive(ctx) : NO_CHANGE); + } + + expect(renderToHtml(Template, 'cheapId')).toEqual(''); + expect(renderToHtml(Template, 'expensiveId')).toEqual(''); + }); + + it('should support interpolation for properties', () => { + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'span'); + e(); + } + p(0, 'id', b1('_', ctx, '_')); + } + + expect(renderToHtml(Template, 'testId')).toEqual(''); + expect(renderToHtml(Template, 'otherId')).toEqual(''); + }); + + describe('input properties', () => { + let button: MyButton; + let otherDir: OtherDir; + + class MyButton { + disabled: boolean; + + static ngDirectiveDef = defineDirective( + {type: MyButton, factory: () => button = new MyButton(), inputs: {disabled: 'disabled'}}); + } + + class OtherDir { + id: boolean; + clickStream = new EventEmitter(); + + static ngDirectiveDef = defineDirective({ + type: OtherDir, + factory: () => otherDir = new OtherDir(), + inputs: {id: 'id'}, + outputs: {clickStream: 'click'} + }); + } + + it('should check input properties before setting (directives)', () => { + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef); + D(1, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef); + T(1, 'Click me'); + } + e(); + } + + p(0, 'disabled', b(ctx.isDisabled)); + p(0, 'id', b(ctx.id)); + } + + const ctx: any = {isDisabled: true, id: 0}; + expect(renderToHtml(Template, ctx)).toEqual(``); + expect(button !.disabled).toEqual(true); + expect(otherDir !.id).toEqual(0); + + ctx.isDisabled = false; + ctx.id = 1; + expect(renderToHtml(Template, ctx)).toEqual(``); + expect(button !.disabled).toEqual(false); + expect(otherDir !.id).toEqual(1); + }); + + it('should support mixed element properties and input properties', () => { + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef); + T(1, 'Click me'); + } + e(); + } + + p(0, 'disabled', b(ctx.isDisabled)); + p(0, 'id', b(ctx.id)); + } + + const ctx: any = {isDisabled: true, id: 0}; + expect(renderToHtml(Template, ctx)).toEqual(``); + expect(button !.disabled).toEqual(true); + + ctx.isDisabled = false; + ctx.id = 1; + expect(renderToHtml(Template, ctx)).toEqual(``); + expect(button !.disabled).toEqual(false); + }); + + it('should check that property is not an input property before setting (component)', () => { + let comp: Comp; + class Comp { + id: number; + + static ngComponentDef = defineComponent({ + tag: 'comp', + type: Comp, + template: function(ctx: any, cm: boolean) {}, + factory: () => comp = new Comp(), + inputs: {id: 'id'} + }); + } + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + p(0, 'id', b(ctx.id)); + Comp.ngComponentDef.r(0, 0); + } + + expect(renderToHtml(Template, {id: 1})).toEqual(``); + expect(comp !.id).toEqual(1); + + expect(renderToHtml(Template, {id: 2})).toEqual(``); + expect(comp !.id).toEqual(2); + }); + + it('should support two input properties with the same name', () => { + let otherDisabledDir: OtherDisabledDir; + + class OtherDisabledDir { + disabled: boolean; + + static ngDirectiveDef = defineDirective({ + type: OtherDisabledDir, + factory: () => otherDisabledDir = new OtherDisabledDir(), + inputs: {disabled: 'disabled'} + }); + } + + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + D(0, MyButton.ngDirectiveDef.n(), MyButton.ngDirectiveDef); + D(1, OtherDisabledDir.ngDirectiveDef.n(), OtherDisabledDir.ngDirectiveDef); + T(1, 'Click me'); + } + e(); + } + p(0, 'disabled', b(ctx.isDisabled)); + } + + const ctx: any = {isDisabled: true}; + expect(renderToHtml(Template, ctx)).toEqual(``); + expect(button !.disabled).toEqual(true); + expect(otherDisabledDir !.disabled).toEqual(true); + + ctx.isDisabled = false; + expect(renderToHtml(Template, ctx)).toEqual(``); + expect(button !.disabled).toEqual(false); + expect(otherDisabledDir !.disabled).toEqual(false); + }); + + it('should set input property if there is an output first', () => { + /** */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'button'); + { + D(0, OtherDir.ngDirectiveDef.n(), OtherDir.ngDirectiveDef); + L('click', ctx.onClick.bind(ctx)); + T(1, 'Click me'); + } + e(); + } + p(0, 'id', b(ctx.id)); + } + + let counter = 0; + const ctx: any = {id: 1, onClick: () => counter++}; + expect(renderToHtml(Template, ctx)).toEqual(``); + expect(otherDir !.id).toEqual(1); + + otherDir !.clickStream.next(); + expect(counter).toEqual(1); + + ctx.id = 2; + renderToHtml(Template, ctx); + expect(otherDir !.id).toEqual(2); + }); + }); + + describe('attributes and input properties', () => { + let myDir: MyDir; + class MyDir { + role: string; + direction: string; + changeStream = new EventEmitter(); + + static ngDirectiveDef = defineDirective({ + type: MyDir, + factory: () => myDir = new MyDir(), + inputs: {role: 'role', direction: 'dir'}, + outputs: {changeStream: 'change'} + }); + } + + let dirB: MyDirB; + class MyDirB { + roleB: string; + + static ngDirectiveDef = defineDirective( + {type: MyDirB, factory: () => dirB = new MyDirB(), inputs: {roleB: 'role'}}); + } + + it('should set input property based on attribute if existing', () => { + + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'button']); + { D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + } + } + + expect(renderToHtml(Template, {})).toEqual(`
      `); + expect(myDir !.role).toEqual('button'); + }); + + it('should set input property and attribute if both defined', () => { + + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'button']); + { D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + } + p(0, 'role', b(ctx.role)); + } + + expect(renderToHtml(Template, {role: 'listbox'})).toEqual(`
      `); + expect(myDir !.role).toEqual('listbox'); + + renderToHtml(Template, {role: 'button'}); + expect(myDir !.role).toEqual('button'); + }); + + it('should set two directive input properties based on same attribute', () => { + + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'button']); + { + D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); + D(1, MyDirB.ngDirectiveDef.n(), MyDirB.ngDirectiveDef); + } + e(); + } + } + + expect(renderToHtml(Template, {})).toEqual(`
      `); + expect(myDir !.role).toEqual('button'); + expect(dirB !.roleB).toEqual('button'); + }); + + it('should process two attributes on same directive', () => { + + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'button', 'dir', 'rtl']); + { D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + } + } + + expect(renderToHtml(Template, {})).toEqual(`
      `); + expect(myDir !.role).toEqual('button'); + expect(myDir !.direction).toEqual('rtl'); + }); + + it('should process attributes and outputs properly together', () => { + + /**
      */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'button']); + { + D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); + L('change', ctx.onChange.bind(ctx)); + } + e(); + } + } + + let counter = 0; + expect(renderToHtml(Template, { + onChange: () => counter++ + })).toEqual(`
      `); + expect(myDir !.role).toEqual('button'); + + myDir !.changeStream.next(); + expect(counter).toEqual(1); + }); + + it('should process attributes properly for directives with later indices', () => { + + + /** + *
      + *
      + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'button', 'dir', 'rtl']); + { D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + E(1, 'div', ['role', 'listbox']); + { D(1, MyDirB.ngDirectiveDef.n(), MyDirB.ngDirectiveDef); } + e(); + } + } + + expect(renderToHtml(Template, {})) + .toEqual(`
      `); + expect(myDir !.role).toEqual('button'); + expect(myDir !.direction).toEqual('rtl'); + expect(dirB !.roleB).toEqual('listbox'); + }); + + it('should process attributes properly inside a for loop', () => { + + class Comp { + static ngComponentDef = defineComponent({ + tag: 'comp', + type: Comp, + template: function(ctx: any, cm: boolean) { + if (cm) { + E(0, 'div', ['role', 'button']); + { D(0, MyDir.ngDirectiveDef.n(), MyDir.ngDirectiveDef); } + e(); + T(1); + } + t(1, b(D(0).role)); + }, + factory: () => new Comp() + }); + } + + /** + * % for (let i = 0; i < 3; i++) { + * + * % } + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + C(0); + c(); + } + rC(0); + { + for (let i = 0; i < 2; i++) { + if (V(0)) { + E(0, Comp.ngComponentDef); + { D(0, Comp.ngComponentDef.n(), Comp.ngComponentDef); } + e(); + } + Comp.ngComponentDef.r(0, 0); + v(); + } + } + rc(); + } + + expect(renderToHtml(Template, {})) + .toEqual( + `
      button
      button
      `); + }); + + }); + +}); diff --git a/packages/core/test/render3/query_spec.ts b/packages/core/test/render3/query_spec.ts new file mode 100644 index 0000000000..6c2805d231 --- /dev/null +++ b/packages/core/test/render3/query_spec.ts @@ -0,0 +1,51 @@ +/** + * @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 {D, E, Q, QueryList, e, m, rQ} from '../../src/render3/index'; + +import {createComponent, renderComponent} from './render_util'; + +describe('query', () => { + it('should project query children', () => { + const Child = createComponent('child', function(ctx: any, cm: boolean) {}); + + let child1 = null; + let child2 = null; + const Cmp = createComponent('cmp', function(ctx: any, cm: boolean) { + /** + * + * + * + * + * class Cmp { + * @ViewChildren(Child) query0; + * @ViewChildren(Child, {descend: true}) query1; + * } + */ + let tmp: any; + if (cm) { + m(0, Q(Child, false)); + m(1, Q(Child, true)); + E(0, Child.ngComponentDef); + { + child1 = D(0, Child.ngComponentDef.n(), Child.ngComponentDef); + E(1, Child.ngComponentDef); + { child2 = D(1, Child.ngComponentDef.n(), Child.ngComponentDef); } + e(); + } + e(); + } + rQ(tmp = m>(0)) && (ctx.query0 = tmp as QueryList); + rQ(tmp = m>(1)) && (ctx.query1 = tmp as QueryList); + }); + + const parent = renderComponent(Cmp); + expect((parent.query0 as QueryList).toArray()).toEqual([child1]); + expect((parent.query1 as QueryList).toArray()).toEqual([child1, child2]); + }); +}); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts new file mode 100644 index 0000000000..c96edda9fb --- /dev/null +++ b/packages/core/test/render3/render_util.ts @@ -0,0 +1,83 @@ +/** + * @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 {ComponentTemplate, ComponentType, PublicFeature, defineComponent, renderComponent as _renderComponent} from '../../src/render3/index'; +import {NG_HOST_SYMBOL, createNode, createViewState, renderTemplate} from '../../src/render3/instructions'; +import {LElement, LNodeFlags} from '../../src/render3/interfaces'; +import {RElement, RText, Renderer3} from '../../src/render3/renderer'; +import {getRenderer2} from './imported_renderer2'; + +export const document = ((global || window) as any).document; +export let containerEl: HTMLElement = null !; +let host: LElement; +let activeRenderer: Renderer3 = + (typeof process !== 'undefined' && process.argv[3] && process.argv[3] === '--r=renderer2') ? + getRenderer2(document) : + document; +// tslint:disable-next-line:no-console +console.log( + `Running tests with ${activeRenderer === document ? 'document' : 'Renderer2'} renderer...`); + +export const requestAnimationFrame: + {(fn: () => void): void; flush(): void; queue: (() => void)[];} = function(fn: () => void) { + requestAnimationFrame.queue.push(fn); + } as any; +requestAnimationFrame.flush = function() { + while (requestAnimationFrame.queue.length) { + requestAnimationFrame.queue.shift() !(); + } +}; + +export function resetDOM() { + requestAnimationFrame.queue = []; + containerEl = document.createElement('div'); + containerEl.setAttribute('host', ''); + host = createNode(null, LNodeFlags.Element, containerEl, createViewState(-1, activeRenderer)); + // TODO: assert that the global state is clean (e.g. ngData, previousOrParentNode, etc) +} + +export function renderToHtml(template: ComponentTemplate, ctx: any) { + renderTemplate(host, template, ctx); + return toHtml(host.native); +} + +beforeEach(resetDOM); + +export function renderComponent(type: ComponentType): T { + return _renderComponent(type, {renderer: activeRenderer, host: containerEl}); +} + +export function toHtml(componentOrElement: T | RElement): string { + const node = (componentOrElement as any)[NG_HOST_SYMBOL] as LElement; + if (node) { + return toHtml(node.native); + } else { + return containerEl.innerHTML.replace(' style=""', '').replace(//g, ''); + } +} + +export function createComponent( + name: string, template: ComponentTemplate): ComponentType { + return class Component { + value: any; + static ngComponentDef = defineComponent({ + type: Component, + tag: name, + factory: () => new Component, + template: template, + features: [PublicFeature] + }); + }; +} + + + +// Verify that DOM is a type of render. This is here for error checking only and has no use. +export const renderer: Renderer3 = null as any as Document; +export const element: RElement = null as any as HTMLElement; +export const text: RText = null as any as Text; diff --git a/packages/core/test/render3/util_spec.ts b/packages/core/test/render3/util_spec.ts new file mode 100644 index 0000000000..097132de07 --- /dev/null +++ b/packages/core/test/render3/util_spec.ts @@ -0,0 +1,35 @@ +/** + * @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 {isDifferent} from '../../src/render3/util'; + +describe('util', () => { + + describe('isDifferent', () => { + + it('should mark non-equal arguments as different', () => { + expect(isDifferent({}, {})).toBeTruthy(); + expect(isDifferent('foo', 'bar')).toBeTruthy(); + expect(isDifferent(0, 1)).toBeTruthy(); + }); + + it('should not mark equal arguments as different', () => { + const obj = {}; + expect(isDifferent(obj, obj)).toBeFalsy(); + expect(isDifferent('foo', 'foo')).toBeFalsy(); + expect(isDifferent(1, 1)).toBeFalsy(); + }); + + it('should not mark NaN as different', () => { expect(isDifferent(NaN, NaN)).toBeFalsy(); }); + + it('should mark NaN with other values as different', () => { + expect(isDifferent(NaN, 'foo')).toBeTruthy(); + expect(isDifferent(5, NaN)).toBeTruthy(); + }); + }); +}); \ No newline at end of file diff --git a/test-main.js b/test-main.js index 6ce3887dab..46eb33c3b7 100644 --- a/test-main.js +++ b/test-main.js @@ -90,9 +90,6 @@ System.import('@angular/core/testing') return System.import(path).then(function(module) { if (module.hasOwnProperty('main')) { module.main(); - } else { - throw new Error( - 'Module ' + path + ' does not implement main() method.'); } }); })); diff --git a/tools/cjs-jasmine/index.ts b/tools/cjs-jasmine/index.ts index 3bc4ea2c5e..0b72880492 100644 --- a/tools/cjs-jasmine/index.ts +++ b/tools/cjs-jasmine/index.ts @@ -56,6 +56,7 @@ var specFiles: any = '@angular/platform-browser/**', '@angular/platform-browser-dynamic/**', '@angular/core/test/zone/**', + '@angular/core/test/render3/**', '@angular/core/test/fake_async_spec.*', '@angular/forms/test/**', '@angular/router/test/route_config/route_config_spec.*',