feat(ivy): add support for resolving view data from a DOM node (#25627)

PR Close #25627
This commit is contained in:
Matias Niemelä
2018-08-22 16:57:40 -07:00
parent 317d40d879
commit 0024d68add
9 changed files with 558 additions and 13 deletions

View File

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