feat(ivy): support inputs & outputs with aliases in component decorators (#27350)
PR Close #27350
This commit is contained in:
parent
1279a503a1
commit
2bc39860bb
|
@ -30,7 +30,7 @@ import {typeWithParameters} from '../util';
|
||||||
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
|
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
|
||||||
import {StylingBuilder, StylingInstruction} from './styling';
|
import {StylingBuilder, StylingInstruction} from './styling';
|
||||||
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
|
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template';
|
||||||
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, mapToExpression, temporaryAllocator} from './util';
|
import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util';
|
||||||
|
|
||||||
const EMPTY_ARRAY: any[] = [];
|
const EMPTY_ARRAY: any[] = [];
|
||||||
|
|
||||||
|
@ -864,4 +864,4 @@ function parseNamedProperty(name: string): {propertyName: string, unit: string}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {propertyName, unit};
|
return {propertyName, unit};
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
import {ConstantPool} from '../../constant_pool';
|
import {ConstantPool} from '../../constant_pool';
|
||||||
import * as o from '../../output/output_ast';
|
import * as o from '../../output/output_ast';
|
||||||
|
import {splitAtColon} from '../../util';
|
||||||
import * as t from '../r3_ast';
|
import * as t from '../r3_ast';
|
||||||
|
|
||||||
import {R3QueryMetadata} from './api';
|
import {R3QueryMetadata} from './api';
|
||||||
import {isI18nAttribute} from './i18n/util';
|
import {isI18nAttribute} from './i18n/util';
|
||||||
|
|
||||||
|
@ -75,9 +75,13 @@ export function conditionallyCreateMapObjectLiteral(keys: {[key: string]: string
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapToExpression(map: {[key: string]: any}, quoted = false): o.Expression {
|
function mapToExpression(map: {[key: string]: any}): o.Expression {
|
||||||
return o.literalMap(
|
return o.literalMap(Object.getOwnPropertyNames(map).map(key => {
|
||||||
Object.getOwnPropertyNames(map).map(key => ({key, quoted, value: asLiteral(map[key])})));
|
// canonical syntax: `dirProp: elProp`
|
||||||
|
// if there is no `:`, use dirProp = elProp
|
||||||
|
const parts = splitAtColon(key, [key, map[key]]);
|
||||||
|
return {key: parts[0], quoted: false, value: asLiteral(parts[1])};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -958,7 +958,7 @@ export function elementProperty<T>(
|
||||||
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
|
if (isComponent(tNode)) markDirtyIfOnPush(lView, index + HEADER_OFFSET);
|
||||||
if (ngDevMode) {
|
if (ngDevMode) {
|
||||||
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) {
|
if (tNode.type === TNodeType.Element || tNode.type === TNodeType.Container) {
|
||||||
setNgReflectProperties(lView, element, tNode.type, propName, value);
|
setNgReflectProperties(lView, element, tNode.type, dataValue, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (tNode.type === TNodeType.Element) {
|
} else if (tNode.type === TNodeType.Element) {
|
||||||
|
@ -1033,20 +1033,23 @@ function setInputsForProperty(lView: LView, inputs: PropertyAliasValue, value: a
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNgReflectProperties(
|
function setNgReflectProperties(
|
||||||
lView: LView, element: RElement | RComment, type: TNodeType, propName: string, value: any) {
|
lView: LView, element: RElement | RComment, type: TNodeType, inputs: PropertyAliasValue,
|
||||||
const renderer = lView[RENDERER];
|
value: any) {
|
||||||
const attrName = normalizeDebugBindingName(propName);
|
for (let i = 0; i < inputs.length; i += 2) {
|
||||||
const debugValue = normalizeDebugBindingValue(value);
|
const renderer = lView[RENDERER];
|
||||||
if (type === TNodeType.Element) {
|
const attrName = normalizeDebugBindingName(inputs[i + 1] as string);
|
||||||
isProceduralRenderer(renderer) ?
|
const debugValue = normalizeDebugBindingValue(value);
|
||||||
renderer.setAttribute((element as RElement), attrName, debugValue) :
|
if (type === TNodeType.Element) {
|
||||||
(element as RElement).setAttribute(attrName, debugValue);
|
isProceduralRenderer(renderer) ?
|
||||||
} else if (value !== undefined) {
|
renderer.setAttribute((element as RElement), attrName, debugValue) :
|
||||||
const value = `bindings=${JSON.stringify({[attrName]: debugValue}, null, 2)}`;
|
(element as RElement).setAttribute(attrName, debugValue);
|
||||||
if (isProceduralRenderer(renderer)) {
|
} else if (value !== undefined) {
|
||||||
renderer.setValue((element as RComment), value);
|
const value = `bindings=${JSON.stringify({[attrName]: debugValue}, null, 2)}`;
|
||||||
} else {
|
if (isProceduralRenderer(renderer)) {
|
||||||
(element as RComment).textContent = value;
|
renderer.setValue((element as RComment), value);
|
||||||
|
} else {
|
||||||
|
(element as RComment).textContent = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1054,8 +1057,8 @@ function setNgReflectProperties(
|
||||||
/**
|
/**
|
||||||
* Consolidates all inputs or outputs of all directives on this logical node.
|
* Consolidates all inputs or outputs of all directives on this logical node.
|
||||||
*
|
*
|
||||||
* @param number tNodeFlags node flags
|
* @param tNodeFlags node flags
|
||||||
* @param Direction direction whether to consider inputs or outputs
|
* @param direction whether to consider inputs or outputs
|
||||||
* @returns PropertyAliases|null aggregate of all properties if any, `null` otherwise
|
* @returns PropertyAliases|null aggregate of all properties if any, `null` otherwise
|
||||||
*/
|
*/
|
||||||
function generatePropertyAliases(tNode: TNode, direction: BindingDirection): PropertyAliases|null {
|
function generatePropertyAliases(tNode: TNode, direction: BindingDirection): PropertyAliases|null {
|
||||||
|
|
|
@ -238,44 +238,42 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
expect(getDOM().getProperty(nativeEl, 'htmlFor')).toBe('foo');
|
expect(getDOM().getProperty(nativeEl, 'htmlFor')).toBe('foo');
|
||||||
});
|
});
|
||||||
|
|
||||||
fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work')
|
it('should consume directive watch expression change.', () => {
|
||||||
.it('should consume directive watch expression change.', () => {
|
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
const template = '<span>' +
|
||||||
const template = '<span>' +
|
'<div my-dir [elprop]="ctxProp"></div>' +
|
||||||
'<div my-dir [elprop]="ctxProp"></div>' +
|
'<div my-dir elprop="Hi there!"></div>' +
|
||||||
'<div my-dir elprop="Hi there!"></div>' +
|
'<div my-dir elprop="Hi {{\'there!\'}}"></div>' +
|
||||||
'<div my-dir elprop="Hi {{\'there!\'}}"></div>' +
|
'<div my-dir elprop="One more {{ctxProp}}"></div>' +
|
||||||
'<div my-dir elprop="One more {{ctxProp}}"></div>' +
|
'</span>';
|
||||||
'</span>';
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
|
|
||||||
fixture.componentInstance.ctxProp = 'Hello World!';
|
fixture.componentInstance.ctxProp = 'Hello World!';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const containerSpan = fixture.debugElement.children[0];
|
const containerSpan = fixture.debugElement.children[0];
|
||||||
|
|
||||||
expect(containerSpan.children[0].injector.get(MyDir).dirProp).toEqual('Hello World!');
|
expect(containerSpan.children[0].injector.get(MyDir).dirProp).toEqual('Hello World!');
|
||||||
expect(containerSpan.children[1].injector.get(MyDir).dirProp).toEqual('Hi there!');
|
expect(containerSpan.children[1].injector.get(MyDir).dirProp).toEqual('Hi there!');
|
||||||
expect(containerSpan.children[2].injector.get(MyDir).dirProp).toEqual('Hi there!');
|
expect(containerSpan.children[2].injector.get(MyDir).dirProp).toEqual('Hi there!');
|
||||||
expect(containerSpan.children[3].injector.get(MyDir).dirProp)
|
expect(containerSpan.children[3].injector.get(MyDir).dirProp)
|
||||||
.toEqual('One more Hello World!');
|
.toEqual('One more Hello World!');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('pipes', () => {
|
describe('pipes', () => {
|
||||||
fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work')
|
it('should support pipes in bindings', () => {
|
||||||
.it('should support pipes in bindings', () => {
|
TestBed.configureTestingModule({declarations: [MyComp, MyDir, DoublePipe]});
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, MyDir, DoublePipe]});
|
const template = '<div my-dir #dir="mydir" [elprop]="ctxProp | double"></div>';
|
||||||
const template = '<div my-dir #dir="mydir" [elprop]="ctxProp | double"></div>';
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
|
|
||||||
fixture.componentInstance.ctxProp = 'a';
|
fixture.componentInstance.ctxProp = 'a';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const dir = fixture.debugElement.children[0].references !['dir'];
|
const dir = fixture.debugElement.children[0].references !['dir'];
|
||||||
expect(dir.dirProp).toEqual('aa');
|
expect(dir.dirProp).toEqual('aa');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support nested components.', () => {
|
it('should support nested components.', () => {
|
||||||
|
@ -290,21 +288,20 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
// GH issue 328 - https://github.com/angular/angular/issues/328
|
// GH issue 328 - https://github.com/angular/angular/issues/328
|
||||||
fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work')
|
it('should support different directive types on a single node', () => {
|
||||||
.it('should support different directive types on a single node', () => {
|
TestBed.configureTestingModule({declarations: [MyComp, ChildComp, MyDir]});
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, ChildComp, MyDir]});
|
const template = '<child-cmp my-dir [elprop]="ctxProp"></child-cmp>';
|
||||||
const template = '<child-cmp my-dir [elprop]="ctxProp"></child-cmp>';
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
|
|
||||||
fixture.componentInstance.ctxProp = 'Hello World!';
|
fixture.componentInstance.ctxProp = 'Hello World!';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const tc = fixture.debugElement.children[0];
|
const tc = fixture.debugElement.children[0];
|
||||||
|
|
||||||
expect(tc.injector.get(MyDir).dirProp).toEqual('Hello World!');
|
expect(tc.injector.get(MyDir).dirProp).toEqual('Hello World!');
|
||||||
expect(tc.injector.get(ChildComp).dirProp).toEqual(null);
|
expect(tc.injector.get(ChildComp).dirProp).toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support directives where a binding attribute is not given', () => {
|
it('should support directives where a binding attribute is not given', () => {
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
||||||
|
@ -1688,21 +1685,20 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('logging property updates', () => {
|
describe('logging property updates', () => {
|
||||||
fixmeIvy('FW-587: Inputs with aliases in component decorators don\'t work')
|
it('should reflect property values as attributes', () => {
|
||||||
.it('should reflect property values as attributes', () => {
|
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
const template = '<div>' +
|
||||||
const template = '<div>' +
|
'<div my-dir [elprop]="ctxProp"></div>' +
|
||||||
'<div my-dir [elprop]="ctxProp"></div>' +
|
'</div>';
|
||||||
'</div>';
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
|
|
||||||
fixture.componentInstance.ctxProp = 'hello';
|
fixture.componentInstance.ctxProp = 'hello';
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(getDOM().getInnerHTML(fixture.nativeElement))
|
expect(getDOM().getInnerHTML(fixture.nativeElement))
|
||||||
.toContain('ng-reflect-dir-prop="hello"');
|
.toContain('ng-reflect-dir-prop="hello"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should work with prop names containing '$'`, () => {
|
it(`should work with prop names containing '$'`, () => {
|
||||||
TestBed.configureTestingModule({declarations: [ParentCmp, SomeCmpWithInput]});
|
TestBed.configureTestingModule({declarations: [ParentCmp, SomeCmpWithInput]});
|
||||||
|
@ -1726,17 +1722,15 @@ function declareTests(config?: {useJit: boolean}) {
|
||||||
.toContain('"ng\-reflect\-ng\-if"\: "true"');
|
.toContain('"ng\-reflect\-ng\-if"\: "true"');
|
||||||
});
|
});
|
||||||
|
|
||||||
// also affected by FW-587: Inputs with aliases in component decorators don't work
|
it('should indicate when toString() throws', () => {
|
||||||
fixmeIvy('FW-664: ng-reflect-* is not supported')
|
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
||||||
.it('should indicate when toString() throws', () => {
|
const template = '<div my-dir [elprop]="toStringThrow"></div>';
|
||||||
TestBed.configureTestingModule({declarations: [MyComp, MyDir]});
|
TestBed.overrideComponent(MyComp, {set: {template}});
|
||||||
const template = '<div my-dir [elprop]="toStringThrow"></div>';
|
const fixture = TestBed.createComponent(MyComp);
|
||||||
TestBed.overrideComponent(MyComp, {set: {template}});
|
|
||||||
const fixture = TestBed.createComponent(MyComp);
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(getDOM().getInnerHTML(fixture.nativeElement)).toContain('[ERROR]');
|
expect(getDOM().getInnerHTML(fixture.nativeElement)).toContain('[ERROR]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('property decorators', () => {
|
describe('property decorators', () => {
|
||||||
|
|
Loading…
Reference in New Issue