feat(ivy): support context discovery for containers & ICU expressions (#27644)
Context discovery was only available on elements. This PR adds support for containers and ICU expressions. FW-378 #resolve FW-665 #comment linker integration tests PR Close #27644
This commit is contained in:
parent
062c7af4f3
commit
4c1cd1bb78
|
@ -6,15 +6,13 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import './ng_dev_mode';
|
import './ng_dev_mode';
|
||||||
|
|
||||||
import {assertDomNode} from './assert';
|
import {assertDomNode} from './assert';
|
||||||
import {EMPTY_ARRAY} from './definition';
|
import {EMPTY_ARRAY} from './definition';
|
||||||
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
|
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
|
||||||
import {TNode, TNodeFlags} from './interfaces/node';
|
import {TNode, TNodeFlags} from './interfaces/node';
|
||||||
import {RElement} from './interfaces/renderer';
|
import {RElement} from './interfaces/renderer';
|
||||||
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
|
import {CONTEXT, HEADER_OFFSET, HOST, LView, TVIEW} from './interfaces/view';
|
||||||
import {getComponentViewByIndex, getNativeByTNode, readElementValue, readPatchedData} from './util';
|
import {getComponentViewByIndex, getNativeByTNode, getTNode, readElementValue, readPatchedData} from './util';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Returns the matching `LContext` data for a given DOM node, directive or component instance.
|
/** Returns the matching `LContext` data for a given DOM node, directive or component instance.
|
||||||
|
@ -67,8 +65,8 @@ export function getLContext(target: any): LContext|null {
|
||||||
}
|
}
|
||||||
|
|
||||||
// the goal is not to fill the entire context full of data because the lookups
|
// the goal is not to fill the entire context full of data because the lookups
|
||||||
// are expensive. Instead, only the target data (the element, compontent or
|
// are expensive. Instead, only the target data (the element, component, container, ICU
|
||||||
// directive details) are filled into the context. If called multiple times
|
// expression or directive details) are filled into the context. If called multiple times
|
||||||
// with different target values then the missing target data will be filled in.
|
// with different target values then the missing target data will be filled in.
|
||||||
const native = readElementValue(lView[nodeIndex]);
|
const native = readElementValue(lView[nodeIndex]);
|
||||||
const existingCtx = readPatchedData(native);
|
const existingCtx = readPatchedData(native);
|
||||||
|
@ -208,10 +206,15 @@ function traverseNextElement(tNode: TNode): TNode|null {
|
||||||
return tNode.child;
|
return tNode.child;
|
||||||
} else if (tNode.next) {
|
} else if (tNode.next) {
|
||||||
return tNode.next;
|
return tNode.next;
|
||||||
} else if (tNode.parent) {
|
} else {
|
||||||
return tNode.parent.next || null;
|
// Let's take the following template: <div><span>text</span></div><component/>
|
||||||
|
// After checking the text node, we need to find the next parent that has a "next" TNode,
|
||||||
|
// in this case the parent `div`, so that we can find the component.
|
||||||
|
while (tNode.parent && !tNode.parent.next) {
|
||||||
|
tNode = tNode.parent;
|
||||||
|
}
|
||||||
|
return tNode.parent && tNode.parent.next;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -9,8 +9,8 @@
|
||||||
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
|
import {SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS, getTemplateContent} from '../sanitization/html_sanitizer';
|
||||||
import {InertBodyHelper} from '../sanitization/inert_body';
|
import {InertBodyHelper} from '../sanitization/inert_body';
|
||||||
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
|
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
|
||||||
|
|
||||||
import {assertDefined, assertEqual, assertGreaterThan} from './assert';
|
import {assertDefined, assertEqual, assertGreaterThan} from './assert';
|
||||||
|
import {attachPatchData} from './context_discovery';
|
||||||
import {allocExpando, createNodeAtIndex, elementAttribute, load, textBinding} from './instructions';
|
import {allocExpando, createNodeAtIndex, elementAttribute, load, textBinding} from './instructions';
|
||||||
import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container';
|
import {LContainer, NATIVE, RENDER_PARENT} from './interfaces/container';
|
||||||
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n';
|
import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, IcuType, TI18n, TIcu} from './interfaces/i18n';
|
||||||
|
@ -337,9 +337,7 @@ const parentIndexStack: number[] = [];
|
||||||
export function i18nStart(index: number, message: string, subTemplateIndex?: number): void {
|
export function i18nStart(index: number, message: string, subTemplateIndex?: number): void {
|
||||||
const tView = getLView()[TVIEW];
|
const tView = getLView()[TVIEW];
|
||||||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||||||
ngDevMode &&
|
i18nIndexStack[++i18nIndexStackPointer] = index;
|
||||||
assertEqual(
|
|
||||||
tView.firstTemplatePass, true, `You should only call i18nEnd on first template pass`);
|
|
||||||
if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) {
|
if (tView.firstTemplatePass && tView.data[index + HEADER_OFFSET] === null) {
|
||||||
i18nStartFirstPass(tView, index, message, subTemplateIndex);
|
i18nStartFirstPass(tView, index, message, subTemplateIndex);
|
||||||
}
|
}
|
||||||
|
@ -350,7 +348,6 @@ export function i18nStart(index: number, message: string, subTemplateIndex?: num
|
||||||
*/
|
*/
|
||||||
function i18nStartFirstPass(
|
function i18nStartFirstPass(
|
||||||
tView: TView, index: number, message: string, subTemplateIndex?: number) {
|
tView: TView, index: number, message: string, subTemplateIndex?: number) {
|
||||||
i18nIndexStack[++i18nIndexStackPointer] = index;
|
|
||||||
const viewData = getLView();
|
const viewData = getLView();
|
||||||
const expandoStartIndex = tView.blueprint.length - HEADER_OFFSET;
|
const expandoStartIndex = tView.blueprint.length - HEADER_OFFSET;
|
||||||
const previousOrParentTNode = getPreviousOrParentTNode();
|
const previousOrParentTNode = getPreviousOrParentTNode();
|
||||||
|
@ -484,7 +481,6 @@ function appendI18nNode(tNode: TNode, parentTNode: TNode, previousTNode: TNode |
|
||||||
// Nodes that inject ViewContainerRef also have a comment node that should be moved
|
// Nodes that inject ViewContainerRef also have a comment node that should be moved
|
||||||
appendChild(slotValue[NATIVE], tNode, viewData);
|
appendChild(slotValue[NATIVE], tNode, viewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tNode;
|
return tNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -566,12 +562,7 @@ export function i18nPostprocess(
|
||||||
export function i18nEnd(): void {
|
export function i18nEnd(): void {
|
||||||
const tView = getLView()[TVIEW];
|
const tView = getLView()[TVIEW];
|
||||||
ngDevMode && assertDefined(tView, `tView should be defined`);
|
ngDevMode && assertDefined(tView, `tView should be defined`);
|
||||||
ngDevMode &&
|
i18nEndFirstPass(tView);
|
||||||
assertEqual(
|
|
||||||
tView.firstTemplatePass, true, `You should only call i18nEnd on first template pass`);
|
|
||||||
if (tView.firstTemplatePass) {
|
|
||||||
i18nEndFirstPass(tView);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -675,6 +666,7 @@ function readCreateOpCodes(
|
||||||
previousTNode = currentTNode;
|
previousTNode = currentTNode;
|
||||||
currentTNode = createNodeAtIndex(
|
currentTNode = createNodeAtIndex(
|
||||||
expandoStartIndex++, TNodeType.IcuContainer, commentRNode, null, null);
|
expandoStartIndex++, TNodeType.IcuContainer, commentRNode, null, null);
|
||||||
|
attachPatchData(commentRNode, viewData);
|
||||||
(currentTNode as TIcuContainerNode).activeCaseIndex = null;
|
(currentTNode as TIcuContainerNode).activeCaseIndex = null;
|
||||||
// We will add the case nodes later, during the update phase
|
// We will add the case nodes later, during the update phase
|
||||||
setIsParent(false);
|
setIsParent(false);
|
||||||
|
@ -1510,4 +1502,4 @@ function parseNodes(
|
||||||
nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
|
nestedIcuNodeIndex << I18nMutateOpCode.SHIFT_REF | I18nMutateOpCode.Remove);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import {resolveForwardRef} from '../di/forward_ref';
|
import {resolveForwardRef} from '../di/forward_ref';
|
||||||
import {InjectionToken} from '../di/injection_token';
|
import {InjectionToken} from '../di/injection_token';
|
||||||
import {Injector} from '../di/injector';
|
import {Injector} from '../di/injector';
|
||||||
|
@ -44,7 +43,6 @@ import {NO_CHANGE} from './tokens';
|
||||||
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util';
|
import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A permanent marker promise which signifies that the current CD tree is
|
* A permanent marker promise which signifies that the current CD tree is
|
||||||
* clean.
|
* clean.
|
||||||
|
@ -212,24 +210,26 @@ export function createNodeAtIndex(
|
||||||
|
|
||||||
let tNode = tView.data[adjustedIndex] as TNode;
|
let tNode = tView.data[adjustedIndex] as TNode;
|
||||||
if (tNode == null) {
|
if (tNode == null) {
|
||||||
const previousOrParentTNode = getPreviousOrParentTNode();
|
|
||||||
const isParent = getIsParent();
|
|
||||||
// TODO(misko): Refactor createTNode so that it does not depend on LView.
|
// TODO(misko): Refactor createTNode so that it does not depend on LView.
|
||||||
tNode = tView.data[adjustedIndex] = createTNode(lView, type, adjustedIndex, name, attrs, null);
|
tNode = tView.data[adjustedIndex] = createTNode(lView, type, adjustedIndex, name, attrs, null);
|
||||||
|
}
|
||||||
|
|
||||||
// Now link ourselves into the tree.
|
// Now link ourselves into the tree.
|
||||||
if (previousOrParentTNode) {
|
// We need this even if tNode exists, otherwise we might end up pointing to unexisting tNodes when
|
||||||
if (isParent && previousOrParentTNode.child == null &&
|
// we use i18n (especially with ICU expressions that update the DOM during the update phase).
|
||||||
(tNode.parent !== null || previousOrParentTNode.type === TNodeType.View)) {
|
const previousOrParentTNode = getPreviousOrParentTNode();
|
||||||
// We are in the same view, which means we are adding content node to the parent view.
|
const isParent = getIsParent();
|
||||||
previousOrParentTNode.child = tNode;
|
if (previousOrParentTNode) {
|
||||||
} else if (!isParent) {
|
if (isParent && previousOrParentTNode.child == null &&
|
||||||
previousOrParentTNode.next = tNode;
|
(tNode.parent !== null || previousOrParentTNode.type === TNodeType.View)) {
|
||||||
}
|
// We are in the same view, which means we are adding content node to the parent view.
|
||||||
|
previousOrParentTNode.child = tNode;
|
||||||
|
} else if (!isParent) {
|
||||||
|
previousOrParentTNode.next = tNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tView.firstChild == null && type === TNodeType.Element) {
|
if (tView.firstChild == null) {
|
||||||
tView.firstChild = tNode;
|
tView.firstChild = tNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,6 +504,7 @@ export function elementContainerStart(
|
||||||
|
|
||||||
appendChild(native, tNode, lView);
|
appendChild(native, tNode, lView);
|
||||||
createDirectivesAndLocals(tView, lView, localRefs);
|
createDirectivesAndLocals(tView, lView, localRefs);
|
||||||
|
attachPatchData(native, lView);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mark the end of the <ng-container>. */
|
/** Mark the end of the <ng-container>. */
|
||||||
|
@ -1921,6 +1922,8 @@ export function template(
|
||||||
createDirectivesAndLocals(tView, lView, localRefs, localRefExtractor);
|
createDirectivesAndLocals(tView, lView, localRefs, localRefExtractor);
|
||||||
const currentQueries = lView[QUERIES];
|
const currentQueries = lView[QUERIES];
|
||||||
const previousOrParentTNode = getPreviousOrParentTNode();
|
const previousOrParentTNode = getPreviousOrParentTNode();
|
||||||
|
const native = getNativeByTNode(previousOrParentTNode, lView);
|
||||||
|
attachPatchData(native, lView);
|
||||||
if (currentQueries) {
|
if (currentQueries) {
|
||||||
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode);
|
lView[QUERIES] = currentQueries.addNode(previousOrParentTNode as TContainerNode);
|
||||||
}
|
}
|
||||||
|
|
|
@ -284,7 +284,7 @@ export interface TView {
|
||||||
/**
|
/**
|
||||||
* Pointer to the `TNode` that represents the root of the view.
|
* Pointer to the `TNode` that represents the root of the view.
|
||||||
*
|
*
|
||||||
* If this is a `TNode` for an `LViewNode`, this is an embedded view of a container.
|
* If this is a `TViewNode` for an `LViewNode`, this is an embedded view of a container.
|
||||||
* We need this pointer to be able to efficiently find this node when inserting the view
|
* We need this pointer to be able to efficiently find this node when inserting the view
|
||||||
* into an anchor.
|
* into an anchor.
|
||||||
*
|
*
|
||||||
|
|
|
@ -399,29 +399,30 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
expect(fixture.nativeElement).toHaveText('baz');
|
expect(fixture.nativeElement).toHaveText('baz');
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy(
|
it('should not detach views in ViewContainers when the parent view is destroyed.', () => {
|
||||||
'FW-665: Discovery util fails with "Unable to find the given context data for the given target"')
|
TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]});
|
||||||
.it('should not detach views in ViewContainers when the parent view is destroyed.', () => {
|
const template =
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, SomeViewport]});
|
'<div *ngIf="ctxBoolProp"><ng-template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></ng-template></div>';
|
||||||
const template =
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
'<div *ngIf="ctxBoolProp"><ng-template some-viewport let-greeting="someTmpl"><span>{{greeting}}</span></ng-template></div>';
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
|
|
||||||
fixture.componentInstance.ctxBoolProp = true;
|
fixture.componentInstance.ctxBoolProp = true;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const ngIfEl = fixture.debugElement.children[0];
|
const ngIfEl = fixture.debugElement.children[0];
|
||||||
const someViewport: SomeViewport = ngIfEl.childNodes[0].injector.get(SomeViewport);
|
const someViewport: SomeViewport =
|
||||||
expect(someViewport.container.length).toBe(2);
|
ngIfEl.childNodes
|
||||||
expect(ngIfEl.children.length).toBe(2);
|
.find(debugElement => debugElement.nativeNode.nodeType === Node.COMMENT_NODE)
|
||||||
|
.injector.get(SomeViewport);
|
||||||
|
expect(someViewport.container.length).toBe(2);
|
||||||
|
expect(ngIfEl.children.length).toBe(2);
|
||||||
|
|
||||||
fixture.componentInstance.ctxBoolProp = false;
|
fixture.componentInstance.ctxBoolProp = false;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(someViewport.container.length).toBe(2);
|
expect(someViewport.container.length).toBe(2);
|
||||||
expect(fixture.debugElement.children.length).toBe(0);
|
expect(fixture.debugElement.children.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use a comment while stamping out `<ng-template>` elements.', () => {
|
it('should use a comment while stamping out `<ng-template>` elements.', () => {
|
||||||
const fixture =
|
const fixture =
|
||||||
|
@ -798,40 +799,37 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
emitter.fireEvent('fired !');
|
emitter.fireEvent('fired !');
|
||||||
}));
|
}));
|
||||||
|
|
||||||
fixmeIvy(
|
it('should support events via EventEmitter on template elements', async(() => {
|
||||||
'FW-665: Discovery util fails with Unable to find the given context data for the given target')
|
const fixture =
|
||||||
.it('should support events via EventEmitter on template elements', async(() => {
|
TestBed
|
||||||
const fixture =
|
.configureTestingModule(
|
||||||
TestBed
|
{declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]})
|
||||||
.configureTestingModule({
|
.overrideComponent(MyComp, {
|
||||||
declarations: [MyComp, DirectiveEmittingEvent, DirectiveListeningEvent]
|
set: {
|
||||||
})
|
template:
|
||||||
.overrideComponent(MyComp, {
|
'<ng-template emitter listener (event)="ctxProp=$event"></ng-template>'
|
||||||
set: {
|
}
|
||||||
template:
|
})
|
||||||
'<ng-template emitter listener (event)="ctxProp=$event"></ng-template>'
|
.createComponent(MyComp);
|
||||||
}
|
const tc = fixture.debugElement.childNodes.find(
|
||||||
})
|
debugElement => debugElement.nativeNode.nodeType === Node.COMMENT_NODE);
|
||||||
.createComponent(MyComp);
|
|
||||||
|
|
||||||
const tc = fixture.debugElement.childNodes[0];
|
const emitter = tc.injector.get(DirectiveEmittingEvent);
|
||||||
|
const myComp = fixture.debugElement.injector.get(MyComp);
|
||||||
|
const listener = tc.injector.get(DirectiveListeningEvent);
|
||||||
|
|
||||||
const emitter = tc.injector.get(DirectiveEmittingEvent);
|
myComp.ctxProp = '';
|
||||||
const myComp = fixture.debugElement.injector.get(MyComp);
|
expect(listener.msg).toEqual('');
|
||||||
const listener = tc.injector.get(DirectiveListeningEvent);
|
|
||||||
|
|
||||||
myComp.ctxProp = '';
|
emitter.event.subscribe({
|
||||||
expect(listener.msg).toEqual('');
|
next: () => {
|
||||||
|
expect(listener.msg).toEqual('fired !');
|
||||||
|
expect(myComp.ctxProp).toEqual('fired !');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
emitter.event.subscribe({
|
emitter.fireEvent('fired !');
|
||||||
next: () => {
|
}));
|
||||||
expect(listener.msg).toEqual('fired !');
|
|
||||||
expect(myComp.ctxProp).toEqual('fired !');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
emitter.fireEvent('fired !');
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should support [()] syntax', async(() => {
|
it('should support [()] syntax', async(() => {
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTwoWayBinding]});
|
TestBed.configureTestingModule({declarations: [MyComp, DirectiveWithTwoWayBinding]});
|
||||||
|
|
|
@ -8,17 +8,15 @@
|
||||||
import {createInjector} from '@angular/core';
|
import {createInjector} from '@angular/core';
|
||||||
|
|
||||||
import {StaticInjector} from '../../src/di/injector';
|
import {StaticInjector} from '../../src/di/injector';
|
||||||
import {getComponent, getContext, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getRootComponents, getViewComponent} from '../../src/render3/discovery_utils';
|
import {getComponent, getContext, getDirectives, getInjectionTokens, getInjector, getListeners, getLocalRefs, getRootComponents, getViewComponent, loadLContext} from '../../src/render3/discovery_utils';
|
||||||
import {ProvidersFeature, RenderFlags, defineComponent, defineDirective, getHostElement} from '../../src/render3/index';
|
import {ProvidersFeature, RenderFlags, defineComponent, defineDirective, elementContainerEnd, elementContainerStart, getHostElement, i18n, i18nApply, i18nExp} from '../../src/render3/index';
|
||||||
|
|
||||||
import {element, elementEnd, elementStart, elementStyling, elementStylingApply, template, bind, elementProperty, text, textBinding, markDirty, listener} from '../../src/render3/instructions';
|
import {element, elementEnd, elementStart, elementStyling, elementStylingApply, template, bind, elementProperty, text, textBinding, markDirty, listener} from '../../src/render3/instructions';
|
||||||
|
|
||||||
import {ComponentFixture} from './render_util';
|
import {ComponentFixture} from './render_util';
|
||||||
import {NgIf} from './common_with_def';
|
import {NgIf} from './common_with_def';
|
||||||
|
|
||||||
describe('discovery utils', () => {
|
describe('discovery utils', () => {
|
||||||
let fixture: ComponentFixture<MyApp>;
|
let fixture: ComponentFixture<MyApp>;
|
||||||
let myApp: MyApp[];
|
let myApp: MyApp;
|
||||||
let dirA: DirectiveA[];
|
let dirA: DirectiveA[];
|
||||||
let childComponent: DirectiveA[];
|
let childComponent: DirectiveA[];
|
||||||
let child: NodeListOf<Element>;
|
let child: NodeListOf<Element>;
|
||||||
|
@ -29,7 +27,6 @@ describe('discovery utils', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
log = [];
|
log = [];
|
||||||
myApp = [];
|
|
||||||
dirA = [];
|
dirA = [];
|
||||||
childComponent = [];
|
childComponent = [];
|
||||||
fixture = new ComponentFixture(
|
fixture = new ComponentFixture(
|
||||||
|
@ -63,6 +60,7 @@ describe('discovery utils', () => {
|
||||||
* <p></p>
|
* <p></p>
|
||||||
* <VIEW>
|
* <VIEW>
|
||||||
* </child>
|
* </child>
|
||||||
|
* <i18n>ICU expression</i18n>
|
||||||
* </#VIEW>
|
* </#VIEW>
|
||||||
* </my-app>
|
* </my-app>
|
||||||
* ```
|
* ```
|
||||||
|
@ -96,15 +94,19 @@ describe('discovery utils', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MSG_DIV = `{<7B>0<EFBFBD>, select,
|
||||||
|
other {ICU expression}
|
||||||
|
}`;
|
||||||
|
|
||||||
class MyApp {
|
class MyApp {
|
||||||
text: string = 'INIT';
|
text: string = 'INIT';
|
||||||
constructor() { myApp.push(this); }
|
constructor() { myApp = this; }
|
||||||
|
|
||||||
static ngComponentDef = defineComponent({
|
static ngComponentDef = defineComponent({
|
||||||
type: MyApp,
|
type: MyApp,
|
||||||
selectors: [['my-app']],
|
selectors: [['my-app']],
|
||||||
factory: () => new MyApp(),
|
factory: () => new MyApp(),
|
||||||
consts: 9,
|
consts: 13,
|
||||||
vars: 1,
|
vars: 1,
|
||||||
directives: [Child, DirectiveA, NgIf],
|
directives: [Child, DirectiveA, NgIf],
|
||||||
template: (rf: RenderFlags, ctx: MyApp) => {
|
template: (rf: RenderFlags, ctx: MyApp) => {
|
||||||
|
@ -121,10 +123,18 @@ describe('discovery utils', () => {
|
||||||
element(0, 'child');
|
element(0, 'child');
|
||||||
}
|
}
|
||||||
}, 1, 0, 'child', ['ngIf', '']);
|
}, 1, 0, 'child', ['ngIf', '']);
|
||||||
|
elementStart(9, 'i18n');
|
||||||
|
i18n(10, MSG_DIV);
|
||||||
|
elementEnd();
|
||||||
|
elementContainerStart(11);
|
||||||
|
{ text(12, 'content'); }
|
||||||
|
elementContainerEnd();
|
||||||
}
|
}
|
||||||
if (rf & RenderFlags.Update) {
|
if (rf & RenderFlags.Update) {
|
||||||
textBinding(1, bind(ctx.text));
|
textBinding(1, bind(ctx.text));
|
||||||
elementProperty(8, 'ngIf', bind(true));
|
elementProperty(8, 'ngIf', bind(true));
|
||||||
|
i18nExp(bind(ctx.text));
|
||||||
|
i18nApply(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -141,7 +151,7 @@ describe('discovery utils', () => {
|
||||||
expect(() => getComponent(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/);
|
expect(() => getComponent(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/);
|
||||||
});
|
});
|
||||||
it('should return component from element', () => {
|
it('should return component from element', () => {
|
||||||
expect(getComponent<MyApp>(fixture.hostElement)).toEqual(myApp[0]);
|
expect(getComponent<MyApp>(fixture.hostElement)).toEqual(myApp);
|
||||||
expect(getComponent<Child>(child[0])).toEqual(childComponent[0]);
|
expect(getComponent<Child>(child[0])).toEqual(childComponent[0]);
|
||||||
expect(getComponent<Child>(child[1])).toEqual(childComponent[1]);
|
expect(getComponent<Child>(child[1])).toEqual(childComponent[1]);
|
||||||
});
|
});
|
||||||
|
@ -153,7 +163,7 @@ describe('discovery utils', () => {
|
||||||
expect(() => getContext(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/);
|
expect(() => getContext(dirA[1] as any)).toThrowError(/Expecting instance of DOM Node/);
|
||||||
});
|
});
|
||||||
it('should return context from element', () => {
|
it('should return context from element', () => {
|
||||||
expect(getContext<MyApp>(child[0])).toEqual(myApp[0]);
|
expect(getContext<MyApp>(child[0])).toEqual(myApp);
|
||||||
expect(getContext<{$implicit: boolean}>(child[2]) !.$implicit).toEqual(true);
|
expect(getContext<{$implicit: boolean}>(child[2]) !.$implicit).toEqual(true);
|
||||||
expect(getContext<Child>(p[0])).toEqual(childComponent[0]);
|
expect(getContext<Child>(p[0])).toEqual(childComponent[0]);
|
||||||
});
|
});
|
||||||
|
@ -161,7 +171,7 @@ describe('discovery utils', () => {
|
||||||
|
|
||||||
describe('getHostElement', () => {
|
describe('getHostElement', () => {
|
||||||
it('should return element on component', () => {
|
it('should return element on component', () => {
|
||||||
expect(getHostElement(myApp[0])).toEqual(fixture.hostElement);
|
expect(getHostElement(myApp)).toEqual(fixture.hostElement);
|
||||||
expect(getHostElement(childComponent[0])).toEqual(child[0]);
|
expect(getHostElement(childComponent[0])).toEqual(child[0]);
|
||||||
expect(getHostElement(childComponent[1])).toEqual(child[1]);
|
expect(getHostElement(childComponent[1])).toEqual(child[1]);
|
||||||
});
|
});
|
||||||
|
@ -181,7 +191,7 @@ describe('discovery utils', () => {
|
||||||
expect(getInjector(p[0]).get(String)).toEqual('Child');
|
expect(getInjector(p[0]).get(String)).toEqual('Child');
|
||||||
});
|
});
|
||||||
it('should return node-injector from component with providers', () => {
|
it('should return node-injector from component with providers', () => {
|
||||||
expect(getInjector(myApp[0]).get(String)).toEqual('Module');
|
expect(getInjector(myApp).get(String)).toEqual('Module');
|
||||||
expect(getInjector(childComponent[0]).get(String)).toEqual('Child');
|
expect(getInjector(childComponent[0]).get(String)).toEqual('Child');
|
||||||
expect(getInjector(childComponent[1]).get(String)).toEqual('Child');
|
expect(getInjector(childComponent[1]).get(String)).toEqual('Child');
|
||||||
});
|
});
|
||||||
|
@ -206,34 +216,34 @@ describe('discovery utils', () => {
|
||||||
describe('getViewComponent', () => {
|
describe('getViewComponent', () => {
|
||||||
it('should return null when called on root component', () => {
|
it('should return null when called on root component', () => {
|
||||||
expect(getViewComponent(fixture.hostElement)).toEqual(null);
|
expect(getViewComponent(fixture.hostElement)).toEqual(null);
|
||||||
expect(getViewComponent(myApp[0])).toEqual(null);
|
expect(getViewComponent(myApp)).toEqual(null);
|
||||||
});
|
});
|
||||||
it('should return containing component of child component', () => {
|
it('should return containing component of child component', () => {
|
||||||
expect(getViewComponent<MyApp>(child[0])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(child[0])).toEqual(myApp);
|
||||||
expect(getViewComponent<MyApp>(child[1])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(child[1])).toEqual(myApp);
|
||||||
expect(getViewComponent<MyApp>(child[2])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(child[2])).toEqual(myApp);
|
||||||
|
|
||||||
expect(getViewComponent<MyApp>(childComponent[0])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(childComponent[0])).toEqual(myApp);
|
||||||
expect(getViewComponent<MyApp>(childComponent[1])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(childComponent[1])).toEqual(myApp);
|
||||||
expect(getViewComponent<MyApp>(childComponent[2])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(childComponent[2])).toEqual(myApp);
|
||||||
});
|
});
|
||||||
it('should return containing component of any view element', () => {
|
it('should return containing component of any view element', () => {
|
||||||
expect(getViewComponent<MyApp>(span[0])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(span[0])).toEqual(myApp);
|
||||||
expect(getViewComponent<MyApp>(div[0])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(div[0])).toEqual(myApp);
|
||||||
expect(getViewComponent<Child>(p[0])).toEqual(childComponent[0]);
|
expect(getViewComponent<Child>(p[0])).toEqual(childComponent[0]);
|
||||||
expect(getViewComponent<Child>(p[1])).toEqual(childComponent[1]);
|
expect(getViewComponent<Child>(p[1])).toEqual(childComponent[1]);
|
||||||
expect(getViewComponent<Child>(p[2])).toEqual(childComponent[2]);
|
expect(getViewComponent<Child>(p[2])).toEqual(childComponent[2]);
|
||||||
});
|
});
|
||||||
it('should return containing component of child directive', () => {
|
it('should return containing component of child directive', () => {
|
||||||
expect(getViewComponent<MyApp>(dirA[0])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(dirA[0])).toEqual(myApp);
|
||||||
expect(getViewComponent<MyApp>(dirA[1])).toEqual(myApp[0]);
|
expect(getViewComponent<MyApp>(dirA[1])).toEqual(myApp);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getLocalRefs', () => {
|
describe('getLocalRefs', () => {
|
||||||
it('should retrieve empty map', () => {
|
it('should retrieve empty map', () => {
|
||||||
expect(getLocalRefs(fixture.hostElement)).toEqual({});
|
expect(getLocalRefs(fixture.hostElement)).toEqual({});
|
||||||
expect(getLocalRefs(myApp[0])).toEqual({});
|
expect(getLocalRefs(myApp)).toEqual({});
|
||||||
expect(getLocalRefs(span[0])).toEqual({});
|
expect(getLocalRefs(span[0])).toEqual({});
|
||||||
expect(getLocalRefs(child[0])).toEqual({});
|
expect(getLocalRefs(child[0])).toEqual({});
|
||||||
});
|
});
|
||||||
|
@ -249,8 +259,8 @@ describe('discovery utils', () => {
|
||||||
|
|
||||||
describe('getRootComponents', () => {
|
describe('getRootComponents', () => {
|
||||||
it('should return root components from component', () => {
|
it('should return root components from component', () => {
|
||||||
const rootComponents = [myApp[0]];
|
const rootComponents = [myApp];
|
||||||
expect(getRootComponents(myApp[0])).toEqual(rootComponents);
|
expect(getRootComponents(myApp)).toEqual(rootComponents);
|
||||||
expect(getRootComponents(childComponent[0])).toEqual(rootComponents);
|
expect(getRootComponents(childComponent[0])).toEqual(rootComponents);
|
||||||
expect(getRootComponents(childComponent[1])).toEqual(rootComponents);
|
expect(getRootComponents(childComponent[1])).toEqual(rootComponents);
|
||||||
expect(getRootComponents(dirA[0])).toEqual(rootComponents);
|
expect(getRootComponents(dirA[0])).toEqual(rootComponents);
|
||||||
|
@ -289,12 +299,46 @@ describe('discovery utils', () => {
|
||||||
describe('markDirty', () => {
|
describe('markDirty', () => {
|
||||||
it('should re-render component', () => {
|
it('should re-render component', () => {
|
||||||
expect(span[0].textContent).toEqual('INIT');
|
expect(span[0].textContent).toEqual('INIT');
|
||||||
myApp[0].text = 'WORKS';
|
myApp.text = 'WORKS';
|
||||||
markDirty(myApp[0]);
|
markDirty(myApp);
|
||||||
fixture.requestAnimationFrame.flush();
|
fixture.requestAnimationFrame.flush();
|
||||||
expect(span[0].textContent).toEqual('WORKS');
|
expect(span[0].textContent).toEqual('WORKS');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('loadLContext', () => {
|
||||||
|
it('should work on components', () => {
|
||||||
|
const lContext = loadLContext(child[0]);
|
||||||
|
expect(lContext).toBeDefined();
|
||||||
|
expect(lContext.native as any).toBe(child[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work on templates', () => {
|
||||||
|
const templateComment = Array.from(fixture.hostElement.childNodes)
|
||||||
|
.find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE);
|
||||||
|
const lContext = loadLContext(templateComment);
|
||||||
|
expect(lContext).toBeDefined();
|
||||||
|
expect(lContext.native as any).toBe(templateComment);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work on ICU expressions', () => {
|
||||||
|
const icuComment = Array.from(fixture.hostElement.querySelector('i18n') !.childNodes)
|
||||||
|
.find((node: ChildNode) => node.nodeType === Node.COMMENT_NODE);
|
||||||
|
const lContext = loadLContext(icuComment);
|
||||||
|
expect(lContext).toBeDefined();
|
||||||
|
expect(lContext.native as any).toBe(icuComment);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work on ng-container', () => {
|
||||||
|
const ngContainerComment = Array.from(fixture.hostElement.childNodes)
|
||||||
|
.find(
|
||||||
|
(node: ChildNode) => node.nodeType === Node.COMMENT_NODE &&
|
||||||
|
node.textContent === `ng-container`);
|
||||||
|
const lContext = loadLContext(ngContainerComment);
|
||||||
|
expect(lContext).toBeDefined();
|
||||||
|
expect(lContext.native as any).toBe(ngContainerComment);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('discovery utils deprecated', () => {
|
describe('discovery utils deprecated', () => {
|
||||||
|
|
Loading…
Reference in New Issue