1a67d70bf8
Directive defs are not considered public API, so the property that contains them should be prefixed with Angular's marker for "private" ('ɵ') to discourage apps from relying on def APIs directly. This commit adds the prefix and shortens the name from ngDirectiveDef to dir. This is because property names cannot be minified by Uglify without turning on property mangling (which most apps have turned off) and are thus size-sensitive. Note that the other "defs" (ngFactoryDef, etc) will be prefixed and shortened in follow-up PRs, in an attempt to limit how large and conflict-y this change is. PR Close #33110
1190 lines
41 KiB
TypeScript
1190 lines
41 KiB
TypeScript
/**
|
|
* @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 {RendererType2} from '../../src/render/api';
|
|
import {getLContext} from '../../src/render3/context_discovery';
|
|
import {AttributeMarker, ɵɵadvance, ɵɵattribute, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵhostProperty, ɵɵproperty} from '../../src/render3/index';
|
|
import {ɵɵallocHostVars, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵprojection, ɵɵprojectionDef, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate} from '../../src/render3/instructions/all';
|
|
import {MONKEY_PATCH_KEY_NAME} from '../../src/render3/interfaces/context';
|
|
import {RenderFlags} from '../../src/render3/interfaces/definition';
|
|
import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
|
|
import {CONTEXT, HEADER_OFFSET} from '../../src/render3/interfaces/view';
|
|
import {ɵɵsanitizeUrl} from '../../src/sanitization/sanitization';
|
|
import {Sanitizer} from '../../src/sanitization/sanitizer';
|
|
import {SecurityContext} from '../../src/sanitization/security';
|
|
|
|
import {NgIf} from './common_with_def';
|
|
import {ComponentFixture, MockRendererFactory, renderToHtml} from './render_util';
|
|
|
|
describe('render3 integration test', () => {
|
|
|
|
describe('render', () => {
|
|
describe('text bindings', () => {
|
|
it('should support creation-time values in text nodes', () => {
|
|
function Template(rf: RenderFlags, value: string) {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵtext(0, value);
|
|
}
|
|
}
|
|
expect(renderToHtml(Template, 'once', 1, 1)).toEqual('once');
|
|
expect(renderToHtml(Template, 'twice', 1, 1)).toEqual('once');
|
|
expect(ngDevMode).toHaveProperties({
|
|
firstTemplatePass: 0,
|
|
tNode: 2,
|
|
tView: 2, // 1 for root view, 1 for template
|
|
rendererSetText: 1,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('tree', () => {
|
|
interface Tree {
|
|
beforeLabel?: string;
|
|
subTrees?: Tree[];
|
|
afterLabel?: string;
|
|
}
|
|
|
|
interface ParentCtx {
|
|
beforeTree: Tree;
|
|
projectedTree: Tree;
|
|
afterTree: Tree;
|
|
}
|
|
|
|
function showLabel(rf: RenderFlags, ctx: {label: string | undefined}) {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵcontainer(0);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵcontainerRefreshStart(0);
|
|
{
|
|
if (ctx.label != null) {
|
|
let rf1 = ɵɵembeddedViewStart(0, 1, 1);
|
|
if (rf1 & RenderFlags.Create) {
|
|
ɵɵtext(0);
|
|
}
|
|
if (rf1 & RenderFlags.Update) {
|
|
ɵɵtextInterpolate(ctx.label);
|
|
}
|
|
ɵɵembeddedViewEnd();
|
|
}
|
|
}
|
|
ɵɵcontainerRefreshEnd();
|
|
}
|
|
}
|
|
|
|
function showTree(rf: RenderFlags, ctx: {tree: Tree}) {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵcontainer(0);
|
|
ɵɵcontainer(1);
|
|
ɵɵcontainer(2);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵcontainerRefreshStart(0);
|
|
{
|
|
const rf0 = ɵɵembeddedViewStart(0, 1, 0);
|
|
{ showLabel(rf0, {label: ctx.tree.beforeLabel}); }
|
|
ɵɵembeddedViewEnd();
|
|
}
|
|
ɵɵcontainerRefreshEnd();
|
|
ɵɵcontainerRefreshStart(1);
|
|
{
|
|
for (let subTree of ctx.tree.subTrees || []) {
|
|
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
|
|
{ showTree(rf0, {tree: subTree}); }
|
|
ɵɵembeddedViewEnd();
|
|
}
|
|
}
|
|
ɵɵcontainerRefreshEnd();
|
|
ɵɵcontainerRefreshStart(2);
|
|
{
|
|
const rf0 = ɵɵembeddedViewStart(0, 1, 0);
|
|
{ showLabel(rf0, {label: ctx.tree.afterLabel}); }
|
|
ɵɵembeddedViewEnd();
|
|
}
|
|
ɵɵcontainerRefreshEnd();
|
|
}
|
|
}
|
|
|
|
class ChildComponent {
|
|
// TODO(issue/24571): remove '!'.
|
|
beforeTree !: Tree;
|
|
// TODO(issue/24571): remove '!'.
|
|
afterTree !: Tree;
|
|
|
|
static ngFactoryDef = () => new ChildComponent;
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
selectors: [['child']],
|
|
type: ChildComponent,
|
|
decls: 3,
|
|
vars: 0,
|
|
template: function ChildComponentTemplate(
|
|
rf: RenderFlags, ctx: {beforeTree: Tree, afterTree: Tree}) {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵprojectionDef();
|
|
ɵɵcontainer(0);
|
|
ɵɵprojection(1);
|
|
ɵɵcontainer(2);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵcontainerRefreshStart(0);
|
|
{
|
|
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
|
|
{ showTree(rf0, {tree: ctx.beforeTree}); }
|
|
ɵɵembeddedViewEnd();
|
|
}
|
|
ɵɵcontainerRefreshEnd();
|
|
ɵɵcontainerRefreshStart(2);
|
|
{
|
|
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
|
|
{ showTree(rf0, {tree: ctx.afterTree}); }
|
|
ɵɵembeddedViewEnd();
|
|
}
|
|
ɵɵcontainerRefreshEnd();
|
|
}
|
|
},
|
|
inputs: {beforeTree: 'beforeTree', afterTree: 'afterTree'}
|
|
});
|
|
}
|
|
|
|
function parentTemplate(rf: RenderFlags, ctx: ParentCtx) {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelementStart(0, 'child');
|
|
{ ɵɵcontainer(1); }
|
|
ɵɵelementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵproperty('beforeTree', ctx.beforeTree);
|
|
ɵɵproperty('afterTree', ctx.afterTree);
|
|
ɵɵcontainerRefreshStart(1);
|
|
{
|
|
const rf0 = ɵɵembeddedViewStart(0, 3, 0);
|
|
{ showTree(rf0, {tree: ctx.projectedTree}); }
|
|
ɵɵembeddedViewEnd();
|
|
}
|
|
ɵɵcontainerRefreshEnd();
|
|
}
|
|
}
|
|
|
|
it('should work with a tree', () => {
|
|
|
|
const ctx: ParentCtx = {
|
|
beforeTree: {subTrees: [{beforeLabel: 'a'}]},
|
|
projectedTree: {beforeLabel: 'p'},
|
|
afterTree: {afterLabel: 'z'}
|
|
};
|
|
const defs = [ChildComponent];
|
|
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>apz</child>');
|
|
ctx.projectedTree = {subTrees: [{}, {}, {subTrees: [{}, {}]}, {}]};
|
|
ctx.beforeTree.subTrees !.push({afterLabel: 'b'});
|
|
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abz</child>');
|
|
ctx.projectedTree.subTrees ![1].afterLabel = 'h';
|
|
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abhz</child>');
|
|
ctx.beforeTree.subTrees !.push({beforeLabel: 'c'});
|
|
expect(renderToHtml(parentTemplate, ctx, 2, 2, defs)).toEqual('<child>abchz</child>');
|
|
|
|
// To check the context easily:
|
|
// console.log(JSON.stringify(ctx));
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
describe('component styles', () => {
|
|
it('should pass in the component styles directly into the underlying renderer', () => {
|
|
class StyledComp {
|
|
static ngFactoryDef = () => new StyledComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StyledComp,
|
|
styles: ['div { color: red; }'],
|
|
decls: 1,
|
|
vars: 0,
|
|
encapsulation: 100,
|
|
selectors: [['foo']],
|
|
template: (rf: RenderFlags, ctx: StyledComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'div');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
const rendererFactory = new ProxyRenderer3Factory();
|
|
new ComponentFixture(StyledComp, {rendererFactory});
|
|
expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']);
|
|
expect(rendererFactory.lastCapturedType !.encapsulation).toEqual(100);
|
|
});
|
|
});
|
|
|
|
describe('component animations', () => {
|
|
it('should pass in the component styles directly into the underlying renderer', () => {
|
|
const animA = {name: 'a'};
|
|
const animB = {name: 'b'};
|
|
|
|
class AnimComp {
|
|
static ngFactoryDef = () => new AnimComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: AnimComp,
|
|
decls: 0,
|
|
vars: 0,
|
|
data: {
|
|
animation: [
|
|
animA,
|
|
animB,
|
|
],
|
|
},
|
|
selectors: [['foo']],
|
|
template: (rf: RenderFlags, ctx: AnimComp) => {}
|
|
});
|
|
}
|
|
const rendererFactory = new ProxyRenderer3Factory();
|
|
new ComponentFixture(AnimComp, {rendererFactory});
|
|
|
|
const capturedAnimations = rendererFactory.lastCapturedType !.data !['animation'];
|
|
expect(Array.isArray(capturedAnimations)).toBeTruthy();
|
|
expect(capturedAnimations.length).toEqual(2);
|
|
expect(capturedAnimations).toContain(animA);
|
|
expect(capturedAnimations).toContain(animB);
|
|
});
|
|
|
|
it('should include animations in the renderType data array even if the array is empty', () => {
|
|
class AnimComp {
|
|
static ngFactoryDef = () => new AnimComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: AnimComp,
|
|
decls: 0,
|
|
vars: 0,
|
|
data: {
|
|
animation: [],
|
|
},
|
|
selectors: [['foo']],
|
|
template: (rf: RenderFlags, ctx: AnimComp) => {}
|
|
});
|
|
}
|
|
const rendererFactory = new ProxyRenderer3Factory();
|
|
new ComponentFixture(AnimComp, {rendererFactory});
|
|
const data = rendererFactory.lastCapturedType !.data;
|
|
expect(data.animation).toEqual([]);
|
|
});
|
|
|
|
it('should allow [@trigger] bindings to be picked up by the underlying renderer', () => {
|
|
class AnimComp {
|
|
static ngFactoryDef = () => new AnimComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: AnimComp,
|
|
decls: 1,
|
|
vars: 1,
|
|
selectors: [['foo']],
|
|
consts: [[AttributeMarker.Bindings, '@fooAnimation']],
|
|
template: (rf: RenderFlags, ctx: AnimComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'div', 0);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵattribute('@fooAnimation', ctx.animationValue);
|
|
}
|
|
}
|
|
});
|
|
|
|
animationValue = '123';
|
|
}
|
|
|
|
const rendererFactory = new MockRendererFactory(['setAttribute']);
|
|
const fixture = new ComponentFixture(AnimComp, {rendererFactory});
|
|
|
|
const renderer = rendererFactory.lastRenderer !;
|
|
fixture.component.animationValue = '456';
|
|
fixture.update();
|
|
|
|
const spy = renderer.spies['setAttribute'];
|
|
const [elm, attr, value] = spy.calls.mostRecent().args;
|
|
|
|
expect(attr).toEqual('@fooAnimation');
|
|
expect(value).toEqual('456');
|
|
});
|
|
|
|
it('should allow creation-level [@trigger] properties to be picked up by the underlying renderer',
|
|
() => {
|
|
class AnimComp {
|
|
static ngFactoryDef = () => new AnimComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: AnimComp,
|
|
decls: 1,
|
|
vars: 1,
|
|
selectors: [['foo']],
|
|
consts: [['@fooAnimation', '']],
|
|
template: (rf: RenderFlags, ctx: AnimComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'div', 0);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const rendererFactory = new MockRendererFactory(['setProperty']);
|
|
const fixture = new ComponentFixture(AnimComp, {rendererFactory});
|
|
|
|
const renderer = rendererFactory.lastRenderer !;
|
|
fixture.update();
|
|
|
|
const spy = renderer.spies['setProperty'];
|
|
const [elm, attr, value] = spy.calls.mostRecent().args;
|
|
expect(attr).toEqual('@fooAnimation');
|
|
});
|
|
|
|
// TODO(benlesh): this test does not seem to be testing anything we could actually generate with
|
|
// these instructions. ɵɵbind should be present in the ɵɵelementProperty call in the hostBindings,
|
|
// however adding that causes an error because the slot has not been allocated. There is a
|
|
// directive called `comp-with-anim`, that seems to want to be a component, but is defined as a
|
|
// directive that is looking for a property `@fooAnim` to update.
|
|
|
|
// it('should allow host binding animations to be picked up and rendered', () => {
|
|
// class ChildCompWithAnim {
|
|
// static ngFactoryDef = () => new ChildCompWithAnim();
|
|
// static ɵdir = ɵɵdefineDirective({
|
|
// type: ChildCompWithAnim,
|
|
// selectors: [['child-comp-with-anim']],
|
|
// hostBindings: function(rf: RenderFlags, ctx: any, elementIndex: number): void {
|
|
// if (rf & RenderFlags.Update) {
|
|
// ɵɵelementProperty(0, '@fooAnim', ctx.exp);
|
|
// }
|
|
// },
|
|
// });
|
|
|
|
// exp = 'go';
|
|
// }
|
|
|
|
// class ParentComp {
|
|
// static ngFactoryDef = () => new ParentComp();
|
|
// static ɵcmp = ɵɵdefineComponent({
|
|
// type: ParentComp,
|
|
// decls: 1,
|
|
// vars: 1,
|
|
// selectors: [['foo']],
|
|
// template: (rf: RenderFlags, ctx: ParentComp) => {
|
|
// if (rf & RenderFlags.Create) {
|
|
// ɵɵelement(0, 'child-comp-with-anim');
|
|
// }
|
|
// },
|
|
// directives: [ChildCompWithAnim]
|
|
// });
|
|
// }
|
|
|
|
// const rendererFactory = new MockRendererFactory(['setProperty']);
|
|
// const fixture = new ComponentFixture(ParentComp, {rendererFactory});
|
|
|
|
// const renderer = rendererFactory.lastRenderer !;
|
|
// fixture.update();
|
|
|
|
// const spy = renderer.spies['setProperty'];
|
|
// const [elm, attr, value] = spy.calls.mostRecent().args;
|
|
// expect(attr).toEqual('@fooAnim');
|
|
// });
|
|
});
|
|
|
|
describe('element discovery', () => {
|
|
it('should only monkey-patch immediate child nodes in a component', () => {
|
|
class StructuredComp {
|
|
static ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
decls: 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 ngFactoryDef = () => new ChildComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ChildComp,
|
|
selectors: [['child-comp']],
|
|
decls: 3,
|
|
vars: 0,
|
|
template: (rf: RenderFlags, ctx: ChildComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'div');
|
|
ɵɵelement(1, 'div');
|
|
ɵɵelement(2, 'div');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
class ParentComp {
|
|
static ngFactoryDef = () => new ParentComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ParentComp,
|
|
selectors: [['parent-comp']],
|
|
directives: [ChildComp],
|
|
decls: 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]).toBeTruthy();
|
|
|
|
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 ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
directives: [NgIf],
|
|
decls: 2,
|
|
vars: 1,
|
|
consts: [['ngIf', '']],
|
|
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, 'ng-template', 0);
|
|
ɵɵelementEnd();
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵadvance(1);
|
|
ɵɵproperty('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 ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
directives: [NgIf],
|
|
decls: 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 = getLContext(section) !;
|
|
const sectionLView = sectionContext.lView !;
|
|
expect(sectionContext.nodeIndex).toEqual(HEADER_OFFSET);
|
|
expect(sectionLView.length).toBeGreaterThan(HEADER_OFFSET);
|
|
expect(sectionContext.native).toBe(section);
|
|
|
|
const div = fixture.hostElement.querySelector('div') !;
|
|
const divContext = getLContext(div) !;
|
|
const divLView = divContext.lView !;
|
|
expect(divContext.nodeIndex).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 ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
decls: 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 = getLContext(section) !;
|
|
const result2 = section[MONKEY_PATCH_KEY_NAME];
|
|
expect(Array.isArray(result2)).toBeFalsy();
|
|
|
|
expect(result2).toBe(context);
|
|
expect(result2.lView).toBe(result1);
|
|
});
|
|
|
|
it('should cache the element context on an intermediate element that isn\'t pre-emptively monkey-patched',
|
|
() => {
|
|
class StructuredComp {
|
|
static ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
decls: 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 = getLContext(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 ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
decls: 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 elementResult = result1[HEADER_OFFSET]; // first element
|
|
expect(elementResult).toBe(section);
|
|
|
|
const context = getLContext(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 ngFactoryDef = () => new ProjectorComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ProjectorComp,
|
|
selectors: [['projector-comp']],
|
|
decls: 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 ngFactoryDef = () => new ParentComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ParentComp,
|
|
selectors: [['parent-comp']],
|
|
directives: [ProjectorComp],
|
|
decls: 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]).toBeTruthy();
|
|
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 = getLContext(section) !;
|
|
const shadowContext = getLContext(header) !;
|
|
const projectedContext = getLContext(p) !;
|
|
|
|
const parentComponentData = parentContext.lView;
|
|
const shadowComponentData = shadowContext.lView;
|
|
const projectedComponentData = projectedContext.lView;
|
|
|
|
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 = getLContext(elm1);
|
|
expect(context1).toBeFalsy();
|
|
|
|
const elm2 = document.createElement('div');
|
|
document.body.appendChild(elm2);
|
|
const context2 = getLContext(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 ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
decls: 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 = getLContext(manuallyCreatedElement);
|
|
expect(context).toBeFalsy();
|
|
});
|
|
|
|
it('should by default monkey-patch the bootstrap component with context details', () => {
|
|
class StructuredComp {
|
|
static ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
decls: 0,
|
|
vars: 0,
|
|
template: (rf: RenderFlags, ctx: StructuredComp) => {}
|
|
});
|
|
}
|
|
|
|
const fixture = new ComponentFixture(StructuredComp);
|
|
fixture.update();
|
|
|
|
const hostElm = fixture.hostElement;
|
|
const component = fixture.component;
|
|
|
|
const componentLView = (component as any)[MONKEY_PATCH_KEY_NAME];
|
|
expect(Array.isArray(componentLView)).toBeTruthy();
|
|
|
|
const hostLView = (hostElm as any)[MONKEY_PATCH_KEY_NAME];
|
|
expect(hostLView).toBe(componentLView);
|
|
|
|
const context1 = getLContext(hostElm) !;
|
|
expect(context1.lView).toBe(hostLView);
|
|
expect(context1.native).toEqual(hostElm);
|
|
|
|
const context2 = getLContext(component) !;
|
|
expect(context2).toBe(context1);
|
|
expect(context2.lView).toBe(hostLView);
|
|
expect(context2.native).toEqual(hostElm);
|
|
});
|
|
|
|
it('should by default monkey-patch the directives with LView so that they can be examined',
|
|
() => {
|
|
let myDir1Instance: MyDir1|null = null;
|
|
let myDir2Instance: MyDir2|null = null;
|
|
let myDir3Instance: MyDir2|null = null;
|
|
|
|
class MyDir1 {
|
|
static ngFactoryDef = () => myDir1Instance = new MyDir1();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir1, selectors: [['', 'my-dir-1', '']]});
|
|
}
|
|
|
|
class MyDir2 {
|
|
static ngFactoryDef = () => myDir2Instance = new MyDir2();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir2, selectors: [['', 'my-dir-2', '']]});
|
|
}
|
|
|
|
class MyDir3 {
|
|
static ngFactoryDef = () => myDir3Instance = new MyDir2();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir3, selectors: [['', 'my-dir-3', '']]});
|
|
}
|
|
|
|
class StructuredComp {
|
|
static ngFactoryDef = () => new StructuredComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: StructuredComp,
|
|
selectors: [['structured-comp']],
|
|
directives: [MyDir1, MyDir2, MyDir3],
|
|
decls: 2,
|
|
vars: 0,
|
|
consts: [['my-dir-1', '', 'my-dir-2', ''], ['my-dir-3']],
|
|
template: (rf: RenderFlags, ctx: StructuredComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'div', 0);
|
|
ɵɵelement(1, 'div', 1);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const fixture = new ComponentFixture(StructuredComp);
|
|
fixture.update();
|
|
|
|
const hostElm = fixture.hostElement;
|
|
const div1 = hostElm.querySelector('div:first-child') !as any;
|
|
const div2 = hostElm.querySelector('div:last-child') !as any;
|
|
const context = getLContext(hostElm) !;
|
|
const componentView = context.lView[context.nodeIndex];
|
|
|
|
expect(componentView).toContain(myDir1Instance);
|
|
expect(componentView).toContain(myDir2Instance);
|
|
expect(componentView).toContain(myDir3Instance);
|
|
|
|
expect(Array.isArray((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy();
|
|
expect(Array.isArray((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy();
|
|
expect(Array.isArray((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME])).toBeTruthy();
|
|
|
|
const d1Context = getLContext(myDir1Instance) !;
|
|
const d2Context = getLContext(myDir2Instance) !;
|
|
const d3Context = getLContext(myDir3Instance) !;
|
|
|
|
expect(d1Context.lView).toEqual(componentView);
|
|
expect(d2Context.lView).toEqual(componentView);
|
|
expect(d3Context.lView).toEqual(componentView);
|
|
|
|
expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d1Context);
|
|
expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d2Context);
|
|
expect((myDir3Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(d3Context);
|
|
|
|
expect(d1Context.nodeIndex).toEqual(HEADER_OFFSET);
|
|
expect(d1Context.native).toBe(div1);
|
|
expect(d1Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]);
|
|
|
|
expect(d2Context.nodeIndex).toEqual(HEADER_OFFSET);
|
|
expect(d2Context.native).toBe(div1);
|
|
expect(d2Context.directives as any[]).toEqual([myDir1Instance, myDir2Instance]);
|
|
|
|
expect(d3Context.nodeIndex).toEqual(HEADER_OFFSET + 1);
|
|
expect(d3Context.native).toBe(div2);
|
|
expect(d3Context.directives as any[]).toEqual([myDir3Instance]);
|
|
});
|
|
|
|
it('should monkey-patch the exact same context instance of the DOM node, component and any directives on the same element',
|
|
() => {
|
|
let myDir1Instance: MyDir1|null = null;
|
|
let myDir2Instance: MyDir2|null = null;
|
|
let childComponentInstance: ChildComp|null = null;
|
|
|
|
class MyDir1 {
|
|
static ngFactoryDef = () => myDir1Instance = new MyDir1();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir1, selectors: [['', 'my-dir-1', '']]});
|
|
}
|
|
|
|
class MyDir2 {
|
|
static ngFactoryDef = () => myDir2Instance = new MyDir2();
|
|
static ɵdir = ɵɵdefineDirective({type: MyDir2, selectors: [['', 'my-dir-2', '']]});
|
|
}
|
|
|
|
class ChildComp {
|
|
static ngFactoryDef = () => childComponentInstance = new ChildComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ChildComp,
|
|
selectors: [['child-comp']],
|
|
decls: 1,
|
|
vars: 0,
|
|
template: (rf: RenderFlags, ctx: ChildComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'div');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
class ParentComp {
|
|
static ngFactoryDef = () => new ParentComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ParentComp,
|
|
selectors: [['parent-comp']],
|
|
directives: [ChildComp, MyDir1, MyDir2],
|
|
decls: 1,
|
|
vars: 0,
|
|
consts: [['my-dir-1', '', 'my-dir-2', '']],
|
|
template: (rf: RenderFlags, ctx: ParentComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'child-comp', 0);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
const fixture = new ComponentFixture(ParentComp);
|
|
fixture.update();
|
|
|
|
const childCompHostElm = fixture.hostElement.querySelector('child-comp') !as any;
|
|
|
|
const lView = childCompHostElm[MONKEY_PATCH_KEY_NAME];
|
|
expect(Array.isArray(lView)).toBeTruthy();
|
|
expect((myDir1Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView);
|
|
expect((myDir2Instance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView);
|
|
expect((childComponentInstance as any)[MONKEY_PATCH_KEY_NAME]).toBe(lView);
|
|
|
|
const childNodeContext = getLContext(childCompHostElm) !;
|
|
expect(childNodeContext.component).toBeFalsy();
|
|
expect(childNodeContext.directives).toBeFalsy();
|
|
assertMonkeyPatchValueIsLView(myDir1Instance);
|
|
assertMonkeyPatchValueIsLView(myDir2Instance);
|
|
assertMonkeyPatchValueIsLView(childComponentInstance);
|
|
|
|
expect(getLContext(myDir1Instance)).toBe(childNodeContext);
|
|
expect(childNodeContext.component).toBeFalsy();
|
|
expect(childNodeContext.directives !.length).toEqual(2);
|
|
assertMonkeyPatchValueIsLView(myDir1Instance, false);
|
|
assertMonkeyPatchValueIsLView(myDir2Instance, false);
|
|
assertMonkeyPatchValueIsLView(childComponentInstance);
|
|
|
|
expect(getLContext(myDir2Instance)).toBe(childNodeContext);
|
|
expect(childNodeContext.component).toBeFalsy();
|
|
expect(childNodeContext.directives !.length).toEqual(2);
|
|
assertMonkeyPatchValueIsLView(myDir1Instance, false);
|
|
assertMonkeyPatchValueIsLView(myDir2Instance, false);
|
|
assertMonkeyPatchValueIsLView(childComponentInstance);
|
|
|
|
expect(getLContext(childComponentInstance)).toBe(childNodeContext);
|
|
expect(childNodeContext.component).toBeTruthy();
|
|
expect(childNodeContext.directives !.length).toEqual(2);
|
|
assertMonkeyPatchValueIsLView(myDir1Instance, false);
|
|
assertMonkeyPatchValueIsLView(myDir2Instance, false);
|
|
assertMonkeyPatchValueIsLView(childComponentInstance, false);
|
|
|
|
function assertMonkeyPatchValueIsLView(value: any, yesOrNo = true) {
|
|
expect(Array.isArray((value as any)[MONKEY_PATCH_KEY_NAME])).toBe(yesOrNo);
|
|
}
|
|
});
|
|
|
|
it('should monkey-patch sub components with the view data and then replace them with the context result once a lookup occurs',
|
|
() => {
|
|
class ChildComp {
|
|
static ngFactoryDef = () => new ChildComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ChildComp,
|
|
selectors: [['child-comp']],
|
|
decls: 3,
|
|
vars: 0,
|
|
template: (rf: RenderFlags, ctx: ChildComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'div');
|
|
ɵɵelement(1, 'div');
|
|
ɵɵelement(2, 'div');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
class ParentComp {
|
|
static ngFactoryDef = () => new ParentComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: ParentComp,
|
|
selectors: [['parent-comp']],
|
|
directives: [ChildComp],
|
|
decls: 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]).toBeTruthy();
|
|
|
|
const context = getLContext(child) !;
|
|
expect(child[MONKEY_PATCH_KEY_NAME]).toBeTruthy();
|
|
|
|
const componentData = context.lView[context.nodeIndex];
|
|
const component = componentData[CONTEXT];
|
|
expect(component instanceof ChildComp).toBeTruthy();
|
|
expect(component[MONKEY_PATCH_KEY_NAME]).toBe(context.lView);
|
|
|
|
const componentContext = getLContext(component) !;
|
|
expect(component[MONKEY_PATCH_KEY_NAME]).toBe(componentContext);
|
|
expect(componentContext.nodeIndex).toEqual(context.nodeIndex);
|
|
expect(componentContext.native).toEqual(context.native);
|
|
expect(componentContext.lView).toEqual(context.lView);
|
|
});
|
|
});
|
|
|
|
describe('sanitization', () => {
|
|
it('should sanitize data using the provided sanitization interface', () => {
|
|
class SanitizationComp {
|
|
static ngFactoryDef = () => new SanitizationComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: SanitizationComp,
|
|
selectors: [['sanitize-this']],
|
|
decls: 1,
|
|
vars: 1,
|
|
template: (rf: RenderFlags, ctx: SanitizationComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'a');
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵproperty('href', ctx.href, ɵɵsanitizeUrl);
|
|
}
|
|
}
|
|
});
|
|
|
|
private href = '';
|
|
|
|
updateLink(href: any) { this.href = href; }
|
|
}
|
|
|
|
const sanitizer = new LocalSanitizer((value) => { return 'http://bar'; });
|
|
|
|
const fixture = new ComponentFixture(SanitizationComp, {sanitizer});
|
|
fixture.component.updateLink('http://foo');
|
|
fixture.update();
|
|
|
|
const anchor = fixture.hostElement.querySelector('a') !;
|
|
expect(anchor.getAttribute('href')).toEqual('http://bar');
|
|
|
|
fixture.component.updateLink(sanitizer.bypassSecurityTrustUrl('http://foo'));
|
|
fixture.update();
|
|
|
|
expect(anchor.getAttribute('href')).toEqual('http://foo');
|
|
});
|
|
|
|
it('should sanitize HostBindings data using provided sanitization interface', () => {
|
|
let hostBindingDir: UnsafeUrlHostBindingDir;
|
|
class UnsafeUrlHostBindingDir {
|
|
// @HostBinding()
|
|
cite: any = 'http://cite-dir-value';
|
|
|
|
static ngFactoryDef = () => hostBindingDir = new UnsafeUrlHostBindingDir();
|
|
static ɵdir = ɵɵdefineDirective({
|
|
type: UnsafeUrlHostBindingDir,
|
|
selectors: [['', 'unsafeUrlHostBindingDir', '']],
|
|
hostBindings: (rf: RenderFlags, ctx: any) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵallocHostVars(1);
|
|
}
|
|
if (rf & RenderFlags.Update) {
|
|
ɵɵhostProperty('cite', ctx.cite, ɵɵsanitizeUrl);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
class SimpleComp {
|
|
static ngFactoryDef = () => new SimpleComp();
|
|
static ɵcmp = ɵɵdefineComponent({
|
|
type: SimpleComp,
|
|
selectors: [['sanitize-this']],
|
|
decls: 1,
|
|
vars: 0,
|
|
consts: [['unsafeUrlHostBindingDir', '']],
|
|
template: (rf: RenderFlags, ctx: SimpleComp) => {
|
|
if (rf & RenderFlags.Create) {
|
|
ɵɵelement(0, 'blockquote', 0);
|
|
}
|
|
},
|
|
directives: [UnsafeUrlHostBindingDir]
|
|
});
|
|
}
|
|
|
|
const sanitizer = new LocalSanitizer((value) => 'http://bar');
|
|
|
|
const fixture = new ComponentFixture(SimpleComp, {sanitizer});
|
|
hostBindingDir !.cite = 'http://foo';
|
|
fixture.update();
|
|
|
|
const anchor = fixture.hostElement.querySelector('blockquote') !;
|
|
expect(anchor.getAttribute('cite')).toEqual('http://bar');
|
|
|
|
hostBindingDir !.cite = sanitizer.bypassSecurityTrustUrl('http://foo');
|
|
fixture.update();
|
|
|
|
expect(anchor.getAttribute('cite')).toEqual('http://foo');
|
|
});
|
|
});
|
|
|
|
class LocalSanitizedValue {
|
|
constructor(public value: any) {}
|
|
toString() { return this.value; }
|
|
}
|
|
|
|
class LocalSanitizer implements Sanitizer {
|
|
constructor(private _interceptor: (value: string|null|any) => string) {}
|
|
|
|
sanitize(context: SecurityContext, value: LocalSanitizedValue|string|null): string|null {
|
|
if (value instanceof LocalSanitizedValue) {
|
|
return value.toString();
|
|
}
|
|
return this._interceptor(value);
|
|
}
|
|
|
|
bypassSecurityTrustHtml(value: string) {}
|
|
bypassSecurityTrustStyle(value: string) {}
|
|
bypassSecurityTrustScript(value: string) {}
|
|
bypassSecurityTrustResourceUrl(value: string) {}
|
|
|
|
bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); }
|
|
}
|
|
|
|
class ProxyRenderer3Factory implements RendererFactory3 {
|
|
lastCapturedType: RendererType2|null = null;
|
|
|
|
createRenderer(hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 {
|
|
this.lastCapturedType = rendererType;
|
|
return domRendererFactory3.createRenderer(hostElement, rendererType);
|
|
}
|
|
}
|