feat(ivy): add support for resolving view data from a DOM node (#25627)
PR Close #25627
This commit is contained in:
@ -21,6 +21,8 @@ import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
|
||||
|
||||
import {NgIf} from './common_with_def';
|
||||
import {ComponentFixture, TemplateFixture, containerEl, createComponent, renderToHtml} from './render_util';
|
||||
import {MONKEY_PATCH_KEY_NAME, getElementContext} from '../../src/render3/element_discovery';
|
||||
import {StylingIndex} from '../../src/render3/styling';
|
||||
|
||||
describe('render3 integration test', () => {
|
||||
|
||||
@ -1404,7 +1406,6 @@ describe('render3 integration test', () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const rendererFactory = new MockRendererFactory();
|
||||
new ComponentFixture(StyledComp, {rendererFactory});
|
||||
expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']);
|
||||
@ -1412,6 +1413,419 @@ describe('render3 integration test', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('element discovery', () => {
|
||||
it('should only monkey-patch immediate child nodes in a component', () => {
|
||||
class StructuredComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: StructuredComp,
|
||||
selectors: [['structured-comp']],
|
||||
factory: () => new StructuredComp(),
|
||||
consts: 2,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'div');
|
||||
elementStart(1, 'p');
|
||||
elementEnd();
|
||||
elementEnd();
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(StructuredComp);
|
||||
fixture.update();
|
||||
|
||||
const host = fixture.hostElement;
|
||||
const parent = host.querySelector('div') as any;
|
||||
const child = host.querySelector('p') as any;
|
||||
|
||||
expect(parent[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
expect(child[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only monkey-patch immediate child nodes in a sub component', () => {
|
||||
class ChildComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ChildComp,
|
||||
selectors: [['child-comp']],
|
||||
factory: () => new ChildComp(),
|
||||
consts: 3,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: ChildComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'div');
|
||||
element(1, 'div');
|
||||
element(2, 'div');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class ParentComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ParentComp,
|
||||
selectors: [['parent-comp']],
|
||||
directives: [ChildComp],
|
||||
factory: () => new ParentComp(),
|
||||
consts: 2,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: ParentComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'section');
|
||||
elementStart(1, 'child-comp');
|
||||
elementEnd();
|
||||
elementEnd();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(ParentComp);
|
||||
fixture.update();
|
||||
|
||||
const host = fixture.hostElement;
|
||||
const child = host.querySelector('child-comp') as any;
|
||||
expect(child[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
|
||||
|
||||
const [kid1, kid2, kid3] = Array.from(host.querySelectorAll('child-comp > *'));
|
||||
expect(kid1[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
expect(kid2[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
expect(kid3[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only monkey-patch immediate child nodes in an embedded template container', () => {
|
||||
class StructuredComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: StructuredComp,
|
||||
selectors: [['structured-comp']],
|
||||
directives: [NgIf],
|
||||
factory: () => new StructuredComp(),
|
||||
consts: 2,
|
||||
vars: 1,
|
||||
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'section');
|
||||
template(1, (rf, ctx) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'div');
|
||||
element(1, 'p');
|
||||
elementEnd();
|
||||
element(2, 'div');
|
||||
}
|
||||
}, 3, 0, null, ['ngIf', '']);
|
||||
elementEnd();
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
elementProperty(1, 'ngIf', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(StructuredComp);
|
||||
fixture.update();
|
||||
|
||||
const host = fixture.hostElement;
|
||||
const [section, div1, p, div2] = Array.from(host.querySelectorAll('section, div, p'));
|
||||
|
||||
expect(section.nodeName.toLowerCase()).toBe('section');
|
||||
expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
|
||||
expect(div1.nodeName.toLowerCase()).toBe('div');
|
||||
expect(div1[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
|
||||
expect(p.nodeName.toLowerCase()).toBe('p');
|
||||
expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
|
||||
|
||||
expect(div2.nodeName.toLowerCase()).toBe('div');
|
||||
expect(div2[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return a context object from a given dom node', () => {
|
||||
class StructuredComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: StructuredComp,
|
||||
selectors: [['structured-comp']],
|
||||
directives: [NgIf],
|
||||
factory: () => new StructuredComp(),
|
||||
consts: 2,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'section');
|
||||
element(1, 'div');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(StructuredComp);
|
||||
fixture.update();
|
||||
|
||||
const section = fixture.hostElement.querySelector('section') !;
|
||||
const sectionContext = getElementContext(section) !;
|
||||
const sectionLView = sectionContext.lViewData;
|
||||
expect(sectionContext.index).toEqual(HEADER_OFFSET);
|
||||
expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET);
|
||||
expect(sectionContext.native).toBe(section);
|
||||
|
||||
const div = fixture.hostElement.querySelector('div') !;
|
||||
const divContext = getElementContext(div) !;
|
||||
const divLView = divContext.lViewData;
|
||||
expect(divContext.index).toEqual(HEADER_OFFSET + 1);
|
||||
expect(divLView.length).toBeGreaterThan(HEADER_OFFSET);
|
||||
expect(divContext.native).toBe(div);
|
||||
|
||||
expect(divLView).toBe(sectionLView);
|
||||
});
|
||||
|
||||
it('should cache the element context on a element was pre-emptively monkey-patched', () => {
|
||||
class StructuredComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: StructuredComp,
|
||||
selectors: [['structured-comp']],
|
||||
factory: () => new StructuredComp(),
|
||||
consts: 1,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'section');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(StructuredComp);
|
||||
fixture.update();
|
||||
|
||||
const section = fixture.hostElement.querySelector('section') !as any;
|
||||
const result1 = section[MONKEY_PATCH_KEY_NAME];
|
||||
expect(Array.isArray(result1)).toBeTruthy();
|
||||
|
||||
const context = getElementContext(section) !;
|
||||
const result2 = section[MONKEY_PATCH_KEY_NAME];
|
||||
expect(Array.isArray(result2)).toBeFalsy();
|
||||
|
||||
expect(result2).toBe(context);
|
||||
expect(result2.lViewData).toBe(result1);
|
||||
});
|
||||
|
||||
it('should cache the element context on an intermediate element that isn\'t pre-emptively monkey-patched',
|
||||
() => {
|
||||
class StructuredComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: StructuredComp,
|
||||
selectors: [['structured-comp']],
|
||||
factory: () => new StructuredComp(),
|
||||
consts: 2,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'section');
|
||||
element(1, 'p');
|
||||
elementEnd();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(StructuredComp);
|
||||
fixture.update();
|
||||
|
||||
const section = fixture.hostElement.querySelector('section') !as any;
|
||||
expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
|
||||
const p = fixture.hostElement.querySelector('p') !as any;
|
||||
expect(p[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
|
||||
|
||||
const pContext = getElementContext(p) !;
|
||||
expect(pContext.native).toBe(p);
|
||||
expect(p[MONKEY_PATCH_KEY_NAME]).toBe(pContext);
|
||||
});
|
||||
|
||||
it('should be able to pull in element context data even if the element is decorated using styling',
|
||||
() => {
|
||||
class StructuredComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: StructuredComp,
|
||||
selectors: [['structured-comp']],
|
||||
factory: () => new StructuredComp(),
|
||||
consts: 1,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'section');
|
||||
elementStyling(['class-foo']);
|
||||
elementEnd();
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
elementStylingApply(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(StructuredComp);
|
||||
fixture.update();
|
||||
|
||||
const section = fixture.hostElement.querySelector('section') !as any;
|
||||
const result1 = section[MONKEY_PATCH_KEY_NAME];
|
||||
expect(Array.isArray(result1)).toBeTruthy();
|
||||
|
||||
const elementResult = result1[HEADER_OFFSET]; // first element
|
||||
expect(Array.isArray(elementResult)).toBeTruthy();
|
||||
expect(elementResult[StylingIndex.ElementPosition].native).toBe(section);
|
||||
|
||||
const context = getElementContext(section) !;
|
||||
const result2 = section[MONKEY_PATCH_KEY_NAME];
|
||||
expect(Array.isArray(result2)).toBeFalsy();
|
||||
|
||||
expect(context.native).toBe(section);
|
||||
});
|
||||
|
||||
it('should monkey-patch immediate child nodes in a content-projected region with a reference to the parent component',
|
||||
() => {
|
||||
/*
|
||||
<!-- DOM view -->
|
||||
<section>
|
||||
<projection-comp>
|
||||
welcome
|
||||
<header>
|
||||
<h1>
|
||||
<p>this content is projected</p>
|
||||
this content is projected also
|
||||
</h1>
|
||||
</header>
|
||||
</projection-comp>
|
||||
</section>
|
||||
*/
|
||||
class ProjectorComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ProjectorComp,
|
||||
selectors: [['projector-comp']],
|
||||
factory: () => new ProjectorComp(),
|
||||
consts: 4,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: ProjectorComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
projectionDef();
|
||||
text(0, 'welcome');
|
||||
elementStart(1, 'header');
|
||||
elementStart(2, 'h1');
|
||||
projection(3);
|
||||
elementEnd();
|
||||
elementEnd();
|
||||
}
|
||||
if (rf & RenderFlags.Update) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
class ParentComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: ParentComp,
|
||||
selectors: [['parent-comp']],
|
||||
directives: [ProjectorComp],
|
||||
factory: () => new ParentComp(),
|
||||
consts: 5,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: ParentComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
elementStart(0, 'section');
|
||||
elementStart(1, 'projector-comp');
|
||||
elementStart(2, 'p');
|
||||
text(3, 'this content is projected');
|
||||
elementEnd();
|
||||
text(4, 'this content is projected also');
|
||||
elementEnd();
|
||||
elementEnd();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(ParentComp);
|
||||
fixture.update();
|
||||
|
||||
const host = fixture.hostElement;
|
||||
const textNode = host.firstChild as any;
|
||||
const section = host.querySelector('section') !as any;
|
||||
const projectorComp = host.querySelector('projector-comp') !as any;
|
||||
const header = host.querySelector('header') !as any;
|
||||
const h1 = host.querySelector('h1') !as any;
|
||||
const p = host.querySelector('p') !as any;
|
||||
const pText = p.firstChild as any;
|
||||
const projectedTextNode = p.nextSibling;
|
||||
|
||||
expect(projectorComp.children).toContain(header);
|
||||
expect(h1.children).toContain(p);
|
||||
|
||||
expect(textNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
expect(section[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
expect(projectorComp[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
|
||||
expect(header[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
expect(h1[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
|
||||
expect(p[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
expect(pText[MONKEY_PATCH_KEY_NAME]).toBeFalsy();
|
||||
expect(projectedTextNode[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
||||
|
||||
const parentContext = getElementContext(section) !;
|
||||
const shadowContext = getElementContext(header) !;
|
||||
const projectedContext = getElementContext(p) !;
|
||||
|
||||
const parentComponentData = parentContext.lViewData;
|
||||
const shadowComponentData = shadowContext.lViewData;
|
||||
const projectedComponentData = projectedContext.lViewData;
|
||||
|
||||
expect(projectedComponentData).toBe(parentComponentData);
|
||||
expect(shadowComponentData).not.toBe(parentComponentData);
|
||||
});
|
||||
|
||||
it('should return `null` when an element context is retrieved that isn\'t situated in Angular',
|
||||
() => {
|
||||
const elm1 = document.createElement('div');
|
||||
const context1 = getElementContext(elm1);
|
||||
expect(context1).toBeFalsy();
|
||||
|
||||
const elm2 = document.createElement('div');
|
||||
document.body.appendChild(elm2);
|
||||
const context2 = getElementContext(elm2);
|
||||
expect(context2).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return `null` when an element context is retrieved that is a DOM node that was not created by Angular',
|
||||
() => {
|
||||
class StructuredComp {
|
||||
static ngComponentDef = defineComponent({
|
||||
type: StructuredComp,
|
||||
selectors: [['structured-comp']],
|
||||
factory: () => new StructuredComp(),
|
||||
consts: 1,
|
||||
vars: 0,
|
||||
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
||||
if (rf & RenderFlags.Create) {
|
||||
element(0, 'section');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const fixture = new ComponentFixture(StructuredComp);
|
||||
fixture.update();
|
||||
|
||||
const section = fixture.hostElement.querySelector('section') !as any;
|
||||
const manuallyCreatedElement = document.createElement('div');
|
||||
section.appendChild(manuallyCreatedElement);
|
||||
|
||||
const context = getElementContext(manuallyCreatedElement);
|
||||
expect(context).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitization', () => {
|
||||
it('should sanitize data using the provided sanitization interface', () => {
|
||||
class SanitizationComp {
|
||||
|
Reference in New Issue
Block a user