feat(core): Moving Renderer3 into @angular/core (#20855)
PR Close #20855
This commit is contained in:
parent
bc66d27938
commit
0fa818b318
|
@ -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/**',
|
||||
|
|
|
@ -14,6 +14,7 @@ ng_module(
|
|||
module_name = "@angular/core",
|
||||
tsconfig = "//packages:tsconfig",
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"@rxjs",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -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)}!`);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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.');
|
||||
}
|
|
@ -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
|
@ -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[];
|
|
@ -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;
|
|
@ -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 + ' ???';
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>;
|
|
@ -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();
|
||||
}
|
|
@ -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 {}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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');
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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>');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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>');
|
||||
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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>');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -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};
|
||||
}
|
|
@ -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>');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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>`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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.');
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
|
|
@ -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.*',
|
||||
|
|
Loading…
Reference in New Issue