feat(core): Moving Renderer3 into @angular/core (#20855)

PR Close #20855
This commit is contained in:
Miško Hevery 2017-12-01 14:23:03 -08:00 committed by Igor Minar
parent bc66d27938
commit 0fa818b318
39 changed files with 8544 additions and 4 deletions

View File

@ -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/**',

View File

@ -14,6 +14,7 @@ ng_module(
module_name = "@angular/core",
tsconfig = "//packages:tsconfig",
deps = [
"//packages:types",
"@rxjs",
],
)

View File

@ -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<T>(
actual: T, expected: T, condition: string, serializer?: ((v: T) => string)) {
(actual != expected) && assertThrow(actual, expected, condition, '==', serializer);
}
export function assertLessThan<T>(actual: T, expected: T, condition: string) {
(actual < expected) && assertThrow(actual, expected, condition, '>');
}
export function assertNotNull<T>(actual: T, condition: string) {
assertNotEqual(actual, null, condition);
}
export function assertNotEqual<T>(actual: T, expected: T, condition: string) {
(actual == expected) && assertThrow(actual, expected, condition, '!=');
}
export function assertThrow<T>(
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)}!`);
}

View File

@ -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?: (<T>(component: T, componentDef: ComponentDef<T>) => 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<T>(
componentType: ComponentType<T>, opts: CreateComponentOptionArgs): ComponentRef<T> {
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<T>(detectChanges: () => void, context: T): EmbeddedViewRef<T> {
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<T> {
context: T;
destroyed: boolean;
destroy(): void;
onDestroy(cb: Function): void;
}
function implement() {
throw new Error('NotImplemented');
}
function addDestroyable<T, C>(obj: any, context: C): T&DestroyRef<C> {
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<T>(
componentType: ComponentType<T>, 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<T>(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<T>).ngComponentDef.r(0, 0);
isDirty = false;
} finally {
leaveView(oldView);
}
}
let isDirty = false;
export function markDirty<T>(
component: T, scheduler: (fn: () => void) => void = requestAnimationFrame) {
ngDevMode && assertNotNull(component, 'component');
if (!isDirty) {
isDirty = true;
scheduler(detectChanges.bind(null, component));
}
}
export function getHostElement<T>(component: T): RElement {
return ((component as any)[NG_HOST_SYMBOL] as LElement).native;
}

View File

@ -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<T>(token: Type<T>, 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<any>): 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<any> {
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<any>(injectElementRef(), data.template));
}
class TemplateRef<T> implements ITemplateRef<T> {
readonly elementRef: IElementRef;
constructor(elementRef: IElementRef, template: ComponentTemplate<T>) {
this.elementRef = elementRef;
}
createEmbeddedView(context: T): IEmbeddedViewRef<T> { 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<C>(
templateRef: ITemplateRef<C>, context?: C|undefined,
index?: number|undefined): IEmbeddedViewRef<C> {
throw notImplemented();
}
createComponent<C>(
componentFactory: ComponentFactory<C>, index?: number|undefined,
injector?: Injector|undefined, projectableNodes?: any[][]|undefined,
ngModule?: INgModuleRef<any>|undefined): IComponentRef<C> {
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.');
}

View File

@ -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};

File diff suppressed because it is too large Load Diff

View File

@ -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<any>|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<T>(queryList: QueryList<T>, predicate: Type<T>|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<any>|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<LElement|LText|LContainer>;
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[];

View File

@ -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;

View File

@ -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 + ' ???';
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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<T> = {
(ctx: T, creationMode: boolean): void; ngData?: never;
};
export type EmbeddedTemplate<T> = (ctx: T) => void;
export interface ComponentType<T> extends Type<T> { ngComponentDef: ComponentDef<T>; }
export interface DirectiveType<T> extends Type<T> { ngDirectiveDef: DirectiveDef<T>; }
export const enum DirectiveDefFlags {ContentQuery = 0b10}
/**
* `DirectiveDef` is a compiled version of the Directive used by the renderer instructions.
*/
export interface DirectiveDef<T> {
/**
* Token representing the directive. Used by DI.
*/
type: Type<T>;
/** Function that makes a directive public to the DI system. */
diPublic: ((def: DirectiveDef<any>) => 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<T>, directiveIndex: number, elementIndex: number): void;
}
export interface ComponentDef<T> extends DirectiveDef<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 component.
*/
r(this: ComponentDef<T>, 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<T>;
}
export interface DirectiveDefArgs<T> {
type: Type<T>;
factory: () => T;
refresh?: (this: DirectiveDef<T>, 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<T> extends DirectiveDefArgs<T> {
tag: string;
template: ComponentTemplate<T>;
refresh?: (this: ComponentDef<T>, directiveIndex: number, elementIndex: number) => void;
features?: ComponentDefFeature[];
}
export type DirectiveDefFeature = <T>(directiveDef: DirectiveDef<T>) => void;
export type ComponentDefFeature = <T>(directiveDef: DirectiveDef<T>) => 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<T>(componentDefinition: ComponentDefArgs<T>): ComponentDef<T> {
const def = <ComponentDef<any>>{
type: componentDefinition.type,
diPublic: null,
n: componentDefinition.factory,
tag: (componentDefinition as ComponentDefArgs<T>).tag || null !,
template: (componentDefinition as ComponentDefArgs<T>).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<T>(definition: DirectiveDef<T>) {
// TODO: implement. See: https://app.asana.com/0/443577627818617/465170715764659
}
export function PublicFeature<T>(definition: DirectiveDef<T>) {
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<T>(directiveDefinition: DirectiveDefArgs<T>) =>
DirectiveDef<T>;

View File

@ -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<T> {
/**
* Next predicate
*/
next: QueryPredicate<any>|null;
/**
* Destination to which the value should be added.
*/
list: QueryList<T>;
/**
* If looking for directives than it contains the directive type.
*/
type: Type<T>|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<any>|null = null;
deep: QueryPredicate<any>|null = null;
constructor(deep?: QueryPredicate<any>) { this.deep = deep == null ? null : deep; }
track<T>(queryList: IQueryList<T>, predicate: Type<T>|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<any>| 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<T>(
previous: QueryPredicate<any>| null, queryList: QueryList<T>,
predicate: Type<T>| any[]): QueryPredicate<T> {
const isArray = Array.isArray(predicate);
const values = <any>[];
if ((queryList as any as QueryList_<T>)._valuesTree === null) {
(queryList as any as QueryList_<T>)._valuesTree = values;
}
return {
next: previous,
list: queryList,
type: isArray ? null : predicate as Type<T>,
selector: isArray ? predicate as any[] : null,
values: values
};
}
class QueryList_<T>/* implements IQueryList<T> */ {
dirty: boolean = false;
changes: Observable<T>;
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<U>(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<U>(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<T> = IQueryList<T>;
export const QueryList: typeof IQueryList = QueryList_ as any;
export function refreshQuery(query: QueryList<any>): boolean {
return (query as any as QueryList_<any>)._refresh();
}

View File

@ -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<any>): 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 {}

View File

@ -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;
}

View File

@ -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",

View File

@ -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",
],
)

View File

@ -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');
}

View File

@ -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');
});
});
});

View File

@ -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', () => {
/**
* <div><ng-content></ng-content></div>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP());
E(0, 'div');
{ P(1, 0); }
e();
}
});
/**
* <child>content</child>
*/
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('<child><div>content</div></child>');
});
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('<child>content</child>');
});
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('<child><grand-child><div><b>Hello</b>World!</div></grand-child></child>');
});
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('<child><div>()</div></child>');
parent.value = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>(content)</div></child>');
parent.value = false;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>()</div></child>');
});
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('<child><div>(else)</div></child>');
parent.value = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>(content)</div></child>');
parent.value = false;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>(else)</div></child>');
});
it('should support projection in embedded views', () => {
let childCmptInstance: any;
/**
* <div>
* % if (!skipContent) {
* <span>
* <ng-content></ng-content>
* </span>
* % }
* </div>
*/
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();
});
/**
* <child>content</child>
*/
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('<child><div><span>content</span></div></child>');
childCmptInstance.skipContent = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div></div></child>');
});
it('should support projection in embedded views when ng-content is a root node of an embedded view',
() => {
let childCmptInstance: any;
/**
* <div>
* % if (!skipContent) {
* <ng-content></ng-content>
* % }
* </div>
*/
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();
});
/**
* <child>content</child>
*/
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('<child><div>content</div></child>');
childCmptInstance.skipContent = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div></div></child>');
});
it('should project nodes into the last ng-content', () => {
/**
* <div><ng-content></ng-content></div>
* <span><ng-content></ng-content></span>
*/
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();
}
});
/**
* <child>content</child>
*/
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('<child><div></div><span>content</span></child>');
});
/**
* 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 <ng-content> 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;
/**
* <ng-content></ng-content>
* <div>
* % if (show) {
* <ng-content></ng-content>
* % }
* </div>
*/
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();
});
/**
* <child>content</child>
*/
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('<child>content<div></div></child>');
childCmptInstance.show = true;
detectChanges(parent);
expect(toHtml(parent)).toEqual('<child><div>content</div></child>');
});
describe('with selectors', () => {
it('should project nodes using attribute selectors', () => {
/**
* <div id="first"><ng-content select="span[title=toFirst]"></ng-content></div>
* <div id="second"><ng-content select="span[title=toSecond]"></ng-content></div>
*/
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();
}
});
/**
* <child>
* <span title="toFirst">1</span>
* <span title="toSecond">2</span>
* </child>
*/
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(
'<child><div id="first"><span title="toFirst">1</span></div><div id="second"><span title="toSecond">2</span></div></child>');
});
it('should project nodes using class selectors', () => {
/**
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
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();
}
});
/**
* <child>
* <span class="toFirst">1</span>
* <span class="toSecond">2</span>
* </child>
*/
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(
'<child><div id="first"><span class="toFirst">1</span></div><div id="second"><span class="toSecond">2</span></div></child>');
});
it('should project nodes using class selectors when element has multiple classes', () => {
/**
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
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();
}
});
/**
* <child>
* <span class="other toFirst">1</span>
* <span class="toSecond noise">2</span>
* </child>
*/
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(
'<child><div id="first"><span class="other toFirst">1</span></div><div id="second"><span class="toSecond noise">2</span></div></child>');
});
it('should project nodes into the first matching selector', () => {
/**
* <div id="first"><ng-content select="span"></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
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();
}
});
/**
* <child>
* <span class="toFirst">1</span>
* <span class="toSecond">2</span>
* </child>
*/
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(
'<child><div id="first"><span class="toFirst">1</span><span class="toSecond">2</span></div><div id="second"></div></child>');
});
it('should allow mixing ng-content with and without selectors', () => {
/**
* <div id="first"><ng-content select="span.toFirst"></ng-content></div>
* <div id="second"><ng-content></ng-content></div>
*/
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();
}
});
/**
* <child>
* <span class="other toFirst">1</span>
* <span class="toSecond noise">2</span>
* </child>
*/
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(
'<child><div id="first"><span class="toFirst">1</span></div><div id="second"><span>remaining</span>more remaining</div></child>');
});
it('should allow mixing ng-content with and without selectors - ng-content first', () => {
/**
* <div id="first"><ng-content></ng-content></div>
* <div id="second"><ng-content select="span.toSecond"></ng-content></div>
*/
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();
}
});
/**
* <child>
* <span>1</span>
* <span class="toSecond">2</span>
* remaining
* </child>
*/
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(
'<child><div id="first"><span>1</span>remaining</div><div id="second"><span class="toSecond">2</span></div></child>');
});
/**
* 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', () => {
/**
* <ng-content select="span"></ng-content>
* <hr>
* <ng-content></ng-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);
}
});
/**
* <grand-child>
* <ng-content></ng-content>
* <span>in child template</span>
* </grand-child>
*/
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);
}
});
/**
* <child>
* <div>
* parent content
* </div>
* </child>
*/
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(
'<child><grand-child><span>in child template</span><hr><span>parent content</span></grand-child></child>');
});
it('should match selectors against projected containers', () => {
/**
* <span>
* <ng-content select="div"></ng-content>
* </span>
*/
const Child = createComponent('child', function(ctx: any, cm: boolean) {
if (cm) {
m(0, dP([[[['div'], null]]]));
E(0, 'span');
{ P(1, 0, 1); }
e();
}
});
/**
* <child>
* <div *ngIf="true">content</div>
* </child>
*/
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('<child><span><div>content</div></span></child>');
});
});
});

View File

@ -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('<div><span>Hello</span></div>');
ctx.condition = false;
ctx.message = 'Hi!';
expect(renderToHtml(Template, ctx)).toEqual('<div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hi!</span></div>');
});
it('should work with nested if blocks', () => {
const ctx: {condition: boolean, condition2: boolean} = {condition: true, condition2: true};
/**
* <div>
* % if(ctx.condition) {
* <span>
* % if(ctx.condition2) {
* Hello
* % }
* </span>
* % }
* </div>
*/
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('<div><span>Hello</span></div>');
ctx.condition = false;
expect(renderToHtml(Template, ctx)).toEqual('<div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
ctx.condition2 = false;
expect(renderToHtml(Template, ctx)).toEqual('<div><span></span></div>');
ctx.condition2 = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
ctx.condition2 = false;
expect(renderToHtml(Template, ctx)).toEqual('<div><span></span></div>');
ctx.condition = false;
expect(renderToHtml(Template, ctx)).toEqual('<div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span></span></div>');
ctx.condition2 = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
});
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('<div>hello</div>world');
expect(renderToHtml(Template, {condition1: false, condition2: false}))
.toEqual('<div>hello</div>');
});
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('<ul><li>a</li><li>b</li><li>c</li></ul>');
ctx.data = ['e', 'f'];
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>e</li><li>f</li></ul>');
ctx.data = [];
expect(renderToHtml(Template, ctx)).toEqual('<ul></ul>');
ctx.data = ['a', 'b', 'c'];
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>a</li><li>b</li><li>c</li></ul>');
ctx.data.push('d');
expect(renderToHtml(Template, ctx))
.toEqual('<ul><li>a</li><li>b</li><li>c</li><li>d</li></ul>');
ctx.data = ['e'];
expect(renderToHtml(Template, ctx)).toEqual('<ul><li>e</li></ul>');
});
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('<ul><li>aman</li><li>bmbn</li><li>cmcn</li></ul>');
ctx.data = [[], []];
expect(renderToHtml(Template, ctx)).toEqual('<ul></ul>');
});
it('should work with nested loop blocks where nested container is a root node', () => {
/**
* <div>
* Before
* % for (let i = 0; i < cafes.length; i++) {
* <h2> {{ cafes[i].name }} </h2>
* % for (let j = 0; j < cafes[i].entrees; j++) {
* {{ cafes[i].entrees[j] }}
* % }
* -
* % }
* After
* <div>
*/
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('<div>Before<h2>1</h2>abc-<h2>2</h2>def-<h2>3</h2>ghi-After</div>');
ctx.cafes = [];
expect(renderToHtml(Template, ctx)).toEqual('<div>BeforeAfter</div>');
ctx.cafes = [
{name: '1', entrees: ['a', 'c']},
{name: '2', entrees: ['d', 'e']},
];
expect(renderToHtml(Template, ctx)).toEqual('<div>Before<h2>1</h2>ac-<h2>2</h2>de-After</div>');
});
it('should work with loop blocks nested three deep', () => {
/**
* <div>
* Before
* % for (let i = 0; i < cafes.length; i++) {
* <h2> {{ cafes[i].name }} </h2>
* % for (let j = 0; j < cafes[i].entrees.length; j++) {
* <h3> {{ cafes[i].entrees[j].name }} </h3>
* % for (let k = 0; k < cafes[i].entrees[j].foods.length; k++) {
* {{ cafes[i].entrees[j].foods[k] }}
* % }
* % }
* -
* % }
* After
* <div>
*/
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(
'<div>' +
'Before' +
'<h2>1</h2><h3>a</h3>12<h3>b</h3>34<h3>c</h3>56-' +
'<h2>2</h2><h3>d</h3>12<h3>e</h3>34<h3>f</h3>56-' +
'After' +
'</div>');
ctx.cafes = [];
expect(renderToHtml(Template, ctx)).toEqual('<div>BeforeAfter</div>');
});
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('<div><span>Hello</span></div>');
ctx.condition = false;
expect(renderToHtml(Template, ctx)).toEqual('<div><div>Goodbye</div></div>');
ctx.condition = true;
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
});
});
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('<div>abc12</div>');
ctx.data1 = ['e', 'f'];
expect(renderToHtml(Template, ctx)).toEqual('<div>ef12</div>');
ctx.data2 = [8];
expect(renderToHtml(Template, ctx)).toEqual('<div>ef8</div>');
ctx.data1 = ['x', 'y'];
expect(renderToHtml(Template, ctx)).toEqual('<div>xy8</div>');
});
});
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('<div>Before<span>foo</span><span>bar</span>After</div>');
ctx.data = [];
expect(renderToHtml(Template, ctx)).toEqual('<div>Before<span></span><span></span>After</div>');
});
});

View File

@ -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<Directive>(0).value));
}
expect(renderToHtml(Template, {})).toEqual('<div>Created</div>');
});
});
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<DirectiveC>(2).value));
}
expect(renderToHtml(Template, {})).toEqual('<div><span>AB</span></div>');
});
});
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<Directive>(0).value, '-', D<DirectiveSameInstance>(1).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('<div>ElementRef-true</div>');
});
});
describe('TemplateRef', () => {
it('should create directive with TemplateRef dependencies', () => {
class Directive {
value: string;
constructor(public templateRef: TemplateRef<any>) {
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<any>, 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<Directive>(0).value, '-', D<DirectiveSameInstance>(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<Directive>(0).value, '-', D<DirectiveSameInstance>(1).value, ''));
}
expect(renderToHtml(Template, {})).toEqual('<div>ViewContainerRef-true</div>');
});
});
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<ChildDirective>(0).value, '-', D<Child2Directive>(1).value, ''));
v();
}
rc();
}
expect(renderToHtml(Template, {})).toEqual('<div><span>ParentDirective-true</span></div>');
});
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);
}
});
});
});

View File

@ -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<Directive>(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('<span class="foo"></span>');
directiveInstance !.klass = 'bar';
expect(renderToHtml(Template, {})).toEqual('<span class="bar"></span>');
});
});
});

12
packages/core/test/render3/domino.d.ts vendored Normal file
View File

@ -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};
}

View File

@ -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', () => {
/** <input value="one" #myInput> {{ 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('<input value="one">one');
});
it('should support basic export of component', () => {
/** <comp #myComp></comp> {{ 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<MyComponent>(0).name);
}
class MyComponent {
name = 'Nancy';
static ngComponentDef = defineComponent({
type: MyComponent,
tag: 'comp',
template: function() {},
factory: () => new MyComponent
});
}
expect(renderToHtml(Template, {})).toEqual('<comp></comp>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'}});
}
/** <comp #myComp></comp> <div [myDir]="myComp"></div> */
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<MyComponent>(0)));
}
renderToHtml(Template, {});
expect(myDir !.myDir).toEqual(myComponent !);
});
it('should work with directives with exportAs set', () => {
/** <div someDir #myDir="someDir"></div> {{ myDir.name }} */
function Template(ctx: any, cm: boolean) {
if (cm) {
E(0, 'div');
D(0, SomeDirDef.n(), SomeDirDef);
e();
T(1);
}
t(1, D<SomeDir>(0).name);
}
class SomeDir {
name = 'Drew';
}
const SomeDirDef = defineDirective({type: SomeDir, factory: () => new SomeDir});
expect(renderToHtml(Template, {})).toEqual('<div></div>Drew');
});
describe('forward refs', () => {
it('should work with basic text bindings', () => {
/** {{ myInput.value}} <input value="one" #myInput> */
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<input value="one">');
});
it('should work with element properties', () => {
/** <div [title]="myInput.value"</div> <input value="one" #myInput> */
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('<div title="one"></div><input value="one">');
});
it('should work with element attrs', () => {
/** <div [attr.aria-label]="myInput.value"</div> <input value="one" #myInput> */
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('<div aria-label="one"></div><input value="one">');
});
it('should work with element classes', () => {
/** <div [class.red]="myInput.checked"</div> <input type="checkbox" checked #myInput> */
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('<div class="red"></div><input type="checkbox" checked="true">');
});
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'}});
}
/** <div [myDir]="myComp"></div><comp #myComp></comp> */
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<MyComponent>(1)));
}
renderToHtml(Template, {});
expect(myDir !.myDir).toEqual(myComponent !);
});
it('should work with multiple forward refs', () => {
/** {{ myInput.value }} {{ myComp.name }} <comp #myComp></comp> <input value="one" #myInput>
*/
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<comp></comp><input value="one">');
});
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('<div>one<input value="one"></div>');
expect(renderToHtml(Template, {condition: false})).toEqual('<div></div>');
});
});
});

View File

@ -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<any> = new EventEmitter();
readonly onMicrotaskEmpty: EventEmitter<any> = new EventEmitter();
readonly onStable: EventEmitter<any> = new EventEmitter();
readonly onError: EventEmitter<any> = new EventEmitter();
run(fn: () => any): any { return fn(); }
runGuarded(fn: () => any): any { return fn(); }
runOutsideAngular(fn: () => any): any { return fn(); }
runTask<T>(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);
}

View File

@ -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('<span title="Hello">Greetings</span>');
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('<h1>Hello, World!</h1>');
expect(renderToHtml(Template, 'New World')).toEqual('<h1>Hello, New World!</h1>');
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('<b>Hello world!</b>');
expect(renderToHtml(Template, 'mundo')).toEqual('<b>Hello mundo!</b>');
});
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('<b><b><b><b>Hello world!</b></b></b></b>');
expect(renderToHtml(Template, 'mundo')).toEqual('<b><b><b><b>Hello mundo!</b></b></b></b>');
});
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('<b><span></span><span class="foo" id="foo"></span></b>');
expect(renderToHtml(Template, 'bar'))
.toEqual('<b><span></span><span class="foo" id="bar"></span></b>');
});
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('<p>hello</p>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><p>Todo one</p></todo>');
});
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><p>Todo one</p></todo>two');
});
it('should support a component template with component sibling', () => {
/**
* <todo></todo>
* <todo></todo>
*/
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><p>Todo one</p></todo><todo><p>Todo one</p></todo>');
});
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<TodoComponentHostBinding>(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('<todo title="one">one</todo>');
cmptInstance !.title = 'two';
expect(renderToHtml(Template, {})).toEqual('<todo title="two">two</todo>');
});
it('should support component with bindings in template', () => {
/** <p> {{ name }} </p>*/
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('<comp><p>Bess</p></comp>');
});
it('should support a component with sub-views', () => {
/**
* % if (condition) {
* <div>text</div>
* % }
*/
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'}
});
}
/** <comp [condition]="condition"></comp> */
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('<comp><div>text</div></comp>');
expect(renderToHtml(Template, {condition: false})).toEqual('<comp></comp>');
});
});
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('<span title="Hello"></span>');
// update binding
ctx.title = 'Hi!';
expect(renderToHtml(Template, ctx)).toEqual('<span title="Hi!"></span>');
// remove attribute
ctx.title = null;
expect(renderToHtml(Template, ctx)).toEqual('<span></span>');
});
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('<span title="NaN"></span>');
ctx.title = {toString: () => 'Custom toString'};
expect(renderToHtml(Template, ctx)).toEqual('<span title="Custom toString"></span>');
});
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(
'<b a="(0a1b2c3d4e5f6g7)" a0="0" a1="(0)" a2="(0a1)" a3="(0a1b2)" a4="(0a1b2c3)" a5="(0a1b2c3d4)" a6="(0a1b2c3d4e5)" a7="(0a1b2c3d4e5f6)" a8="(0a1b2c3d4e5f6g7)"></b>');
args = args.reverse();
expect(renderToHtml(Template, args))
.toEqual(
'<b a=")7g6f5e4d3c2b1a0(" a0="7" a1=")7(" a2=")7g6(" a3=")7g6f5(" a4=")7g6f5e4(" a5=")7g6f5e4d3(" a6=")7g6f5e4d3c2(" a7=")7g6f5e4d3c2b1(" a8=")7g6f5e4d3c2b1a0("></b>');
args = args.reverse();
expect(renderToHtml(Template, args))
.toEqual(
'<b a="(0a1b2c3d4e5f6g7)" a0="0" a1="(0)" a2="(0a1)" a3="(0a1b2)" a4="(0a1b2c3)" a5="(0a1b2c3d4)" a6="(0a1b2c3d4e5)" a7="(0a1b2c3d4e5f6)" a8="(0a1b2c3d4e5f6g7)"></b>');
});
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('<span title="Hello"><b title="Hello"></b></span>');
// update DOM manually
containerEl.querySelector('b') !.setAttribute('title', 'Goodbye');
// refresh with same binding
expect(renderToHtml(Template, ctx))
.toEqual('<span title="Hello"><b title="Goodbye"></b></span>');
// refresh again with same binding
expect(renderToHtml(Template, ctx))
.toEqual('<span title="Hello"><b title="Goodbye"></b></span>');
});
});
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('<span style="border-color: red;"></span>');
expect(renderToHtml(Template, 'green'))
.toEqual('<span style="border-color: green;"></span>');
expect(renderToHtml(Template, null)).toEqual('<span></span>');
});
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('<span style="font-size: 100px;"></span>');
expect(renderToHtml(Template, 200)).toEqual('<span style="font-size: 200px;"></span>');
expect(renderToHtml(Template, null)).toEqual('<span></span>');
});
});
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('<span class="active"></span>');
expect(renderToHtml(Template, false)).toEqual('<span class=""></span>');
// truthy values
expect(renderToHtml(Template, 'a_string')).toEqual('<span class="active"></span>');
expect(renderToHtml(Template, 10)).toEqual('<span class="active"></span>');
// falsy values
expect(renderToHtml(Template, '')).toEqual('<span class=""></span>');
expect(renderToHtml(Template, 0)).toEqual('<span class=""></span>');
});
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('<span class="existing active"></span>');
expect(renderToHtml(Template, false)).toEqual('<span class="existing"></span>');
});
});
});
});

View File

@ -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<any>) {
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) {
* <comp></comp>
* % }
*/
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) {
* <comp [val]="1"></comp>
* <comp [val]="2"></comp>
* % }
*/
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></parent>
* % }
*
* parent template: <comp></comp>
*/
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></grandparent>
* % }
*
* grandparent template: <parent></parent>
* parent template: <comp></comp>
*/
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) {
* <comp [val]="1"></comp>
* % if (condition2) {
* <comp [val]="2"></comp>
* % }
* <comp [val]="3"></comp>
* % }
*/
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) {
* <comp [val]="1"></comp>
* % for (let i = 2; i < len; i++) {
* <comp [val]="i"></comp>
* % }
* <comp [val]="5"></comp>
* % }
*/
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) {
* <button (click)="onClick()">Click me</button>
* <comp></comp>
* <button (click)="onClick()">Click me</button>
* % }
*/
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);
});
});
});

View File

@ -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',
/** <button (click)="onClick()"> Click me </button> */
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', () => {
/** <button (click)="showing=!showing"> Click me </button> */
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) {
* <button (click)="onClick()"> Click me </button>
* % }
*/
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) {
* <button (click)="onClick()"> Click </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></comp>
* <comp></comp>
* % }
*
* comp:
* <button (click)="onClick()"> Click </button>
*/
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) {
* <button (click)="counter1++">there</button>
* % }
*
* % if (sub2) {
* <button (click)="counter2++">world</button>
* % }
* % }
*/
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);
});
});

View File

@ -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 '#<Event>'
(global as any).Event = null;
}

View File

@ -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', () => {
// <span foo=""> not matching ":not(span)"
expect(isMatching('span', ['foo', ''], [null, [['span']]])).toBeFalsy();
// <span foo=""> 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', () => {
// <span foo="bar"> 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', () => {
// <span foo="bar"> not matching "div, [foo=baz]"
expect(isMatching('span', ['foo', 'bar'], [
[['div'], null], [['', 'foo', 'baz'], null]
])).toBeFalsy();
});
});
});

View File

@ -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', () => {
/** <button-toggle (change)="onChange()"></button-toggle> */
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', () => {
/** <button-toggle (change)="onChange()" (reset)="onReset()"></button-toggle> */
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', () => {
/** <button-toggle (change)="counter++"></button-toggle> */
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) {
* <button-toggle (change)="onChange()"></button-toggle>
* % }
*/
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) {
* <button-toggle (change)="onChange()"></button-toggle>
* % }
* % }
*/
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) {
* <button (click)="onClick()">Click me</button>
* <button-toggle (change)="onChange()"></button-toggle>
* <destroy-comp></destroy-comp>
* % }
*/
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;
/** <button myButton (click)="onClick()">Click me</button> */
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'}});
}
/** <button-toggle (change)="onChange()" otherDir></button-toggle> */
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'}});
}
/** <button-toggle (change)="onChange()" otherDir [change]="change"></button-toggle> */
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);
});
});

View File

@ -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('<span id="testId"></span>');
expect(renderToHtml(Template, 'otherId')).toEqual('<span id="otherId"></span>');
});
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('<span id="cheapId"></span>');
expect(renderToHtml(Template, 'expensiveId')).toEqual('<span id="cheapId"></span>');
});
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('<span id="_testId_"></span>');
expect(renderToHtml(Template, 'otherId')).toEqual('<span id="_otherId_"></span>');
});
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)', () => {
/** <button myButton [id]="id" [disabled]="isDisabled">Click me</button> */
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(`<button>Click me</button>`);
expect(button !.disabled).toEqual(true);
expect(otherDir !.id).toEqual(0);
ctx.isDisabled = false;
ctx.id = 1;
expect(renderToHtml(Template, ctx)).toEqual(`<button>Click me</button>`);
expect(button !.disabled).toEqual(false);
expect(otherDir !.id).toEqual(1);
});
it('should support mixed element properties and input properties', () => {
/** <button myButton [id]="id" [disabled]="isDisabled">Click me</button> */
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(`<button id="0">Click me</button>`);
expect(button !.disabled).toEqual(true);
ctx.isDisabled = false;
ctx.id = 1;
expect(renderToHtml(Template, ctx)).toEqual(`<button id="1">Click me</button>`);
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'}
});
}
/** <comp [id]="id"></comp> */
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(`<comp></comp>`);
expect(comp !.id).toEqual(1);
expect(renderToHtml(Template, {id: 2})).toEqual(`<comp></comp>`);
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'}
});
}
/** <button myButton otherDisabledDir [disabled]="isDisabled">Click me</button> */
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(`<button>Click me</button>`);
expect(button !.disabled).toEqual(true);
expect(otherDisabledDir !.disabled).toEqual(true);
ctx.isDisabled = false;
expect(renderToHtml(Template, ctx)).toEqual(`<button>Click me</button>`);
expect(button !.disabled).toEqual(false);
expect(otherDisabledDir !.disabled).toEqual(false);
});
it('should set input property if there is an output first', () => {
/** <button otherDir [id]="id" (click)="onClick()">Click me</button> */
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(`<button>Click me</button>`);
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', () => {
/** <div role="button" myDir></div> */
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(`<div role="button"></div>`);
expect(myDir !.role).toEqual('button');
});
it('should set input property and attribute if both defined', () => {
/** <div role="button" [role]="role" myDir></div> */
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(`<div role="button"></div>`);
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', () => {
/** <div role="button" myDir myDirB></div> */
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(`<div role="button"></div>`);
expect(myDir !.role).toEqual('button');
expect(dirB !.roleB).toEqual('button');
});
it('should process two attributes on same directive', () => {
/** <div role="button" dir="rtl" myDir></div> */
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(`<div role="button" dir="rtl"></div>`);
expect(myDir !.role).toEqual('button');
expect(myDir !.direction).toEqual('rtl');
});
it('should process attributes and outputs properly together', () => {
/** <div role="button" (change)="onChange()" myDir></div> */
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(`<div role="button"></div>`);
expect(myDir !.role).toEqual('button');
myDir !.changeStream.next();
expect(counter).toEqual(1);
});
it('should process attributes properly for directives with later indices', () => {
/**
* <div role="button" dir="rtl" myDir></div>
* <div role="listbox" myDirB></div>
*/
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(`<div role="button" dir="rtl"></div><div role="listbox"></div>`);
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<MyDir>(0).role));
},
factory: () => new Comp()
});
}
/**
* % for (let i = 0; i < 3; i++) {
* <comp></comp>
* % }
*/
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(
`<comp><div role="button"></div>button</comp><comp><div role="button"></div>button</comp>`);
});
});
});

View File

@ -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) {
/**
* <child>
* <child>
* </child>
* </child>
* 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<QueryList<any>>(0)) && (ctx.query0 = tmp as QueryList<any>);
rQ(tmp = m<QueryList<any>>(1)) && (ctx.query1 = tmp as QueryList<any>);
});
const parent = renderComponent(Cmp);
expect((parent.query0 as QueryList<any>).toArray()).toEqual([child1]);
expect((parent.query1 as QueryList<any>).toArray()).toEqual([child1, child2]);
});
});

View File

@ -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<any>, ctx: any) {
renderTemplate(host, template, ctx);
return toHtml(host.native);
}
beforeEach(resetDOM);
export function renderComponent<T>(type: ComponentType<T>): T {
return _renderComponent(type, {renderer: activeRenderer, host: containerEl});
}
export function toHtml<T>(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(/<!--[\w]*-->/g, '');
}
}
export function createComponent(
name: string, template: ComponentTemplate<any>): ComponentType<any> {
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;

View File

@ -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();
});
});
});

View File

@ -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.');
}
});
}));

View File

@ -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.*',