feat(compiler): add support for shorthand property declarations in templates (#42421)

Adds support for shorthand property declarations inside Angular templates. E.g. doing `{foo, bar}` instead of `{foo: foo, bar: bar}`.

Fixes #10277.

PR Close #42421
This commit is contained in:
Kristiyan Kostadinov
2021-06-19 08:07:20 +02:00
committed by Dylan Hunn
parent 699a8b43cb
commit cc672f05bf
17 changed files with 390 additions and 10 deletions
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AST, ASTWithSource, BindingPipe, MethodCall, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import {AST, ASTWithSource, BindingPipe, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstReference, TmplAstTemplate, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system';
@@ -482,8 +482,20 @@ export class SymbolBuilder {
expression.nameSpan :
expression.sourceSpan;
let node = findFirstMatchingNode(
this.typeCheckBlock, {withSpan, filter: (n: ts.Node): n is ts.Node => true});
let node: ts.Node|null = null;
// Property reads in templates usually map to a `PropertyAccessExpression`
// (e.g. `ctx.foo`) so try looking for one first.
if (expression instanceof PropertyRead) {
node = findFirstMatchingNode(
this.typeCheckBlock, {withSpan, filter: ts.isPropertyAccessExpression});
}
// Otherwise fall back to searching for any AST node.
if (node === null) {
node = findFirstMatchingNode(this.typeCheckBlock, {withSpan, filter: anyNodeFilter});
}
if (node === null) {
return null;
}
@@ -560,3 +572,8 @@ export class SymbolBuilder {
}
}
}
/** Filter predicate function that matches any AST node. */
function anyNodeFilter(n: ts.Node): n is ts.Node {
return true;
}
@@ -462,6 +462,50 @@ class TestComponent {
`TestComponent.html(4, 18): Property 'heihgt' does not exist on type 'TestComponent'. Did you mean 'height'?`,
]);
});
it('works for shorthand property declarations', () => {
const messages = diagnose(
`<div dir [input]="{a, b: 2}"></div>`, `
class Dir {
input: {a: string, b: number};
}
class TestComponent {
a: number;
}`,
[{
type: 'directive',
name: 'Dir',
selector: '[dir]',
exportAs: ['dir'],
inputs: {input: 'input'},
}]);
expect(messages).toEqual(
[`TestComponent.html(1, 20): Type 'number' is not assignable to type 'string'.`]);
});
it('works for shorthand property declarations referring to template variables', () => {
const messages = diagnose(
`
<span #span></span>
<div dir [input]="{span, b: 2}"></div>
`,
`
class Dir {
input: {span: string, b: number};
}
class TestComponent {}`,
[{
type: 'directive',
name: 'Dir',
selector: '[dir]',
exportAs: ['dir'],
inputs: {input: 'input'},
}]);
expect(messages).toEqual(
[`TestComponent.html(3, 30): Type 'HTMLElement' is not assignable to type 'string'.`]);
});
});
describe('method call spans', () => {
@@ -42,6 +42,15 @@ describe('type check blocks diagnostics', () => {
'(ctx).m /*3,4*/({ "foo": ((ctx).a /*11,12*/) /*11,12*/, "bar": ((ctx).b /*19,20*/) /*19,20*/ } /*5,21*/) /*3,22*/');
});
it('should annotate literal map expressions with shorthand declarations', () => {
// The additional method call is present to avoid that the object literal is emitted as
// statement, which would wrap it into parenthesis that clutter the expected output.
const TEMPLATE = '{{ m({a, b}) }}';
expect(tcbWithSpans(TEMPLATE))
.toContain(
'((ctx).m /*3,4*/({ "a": ((ctx).a /*6,7*/) /*6,7*/, "b": ((ctx).b /*9,10*/) /*9,10*/ } /*5,11*/) /*3,12*/)');
});
it('should annotate literal array expressions', () => {
const TEMPLATE = '{{ [a, b] }}';
expect(tcbWithSpans(TEMPLATE))
@@ -668,12 +668,16 @@ runInEachFileSystem(() => {
const fileName = absoluteFrom('/main.ts');
const templateString = `
{{ [1, 2, 3] }}
{{ { hello: "world" } }}`;
{{ { hello: "world" } }}
{{ { foo } }}`;
const testValues = setup([
{
fileName,
templates: {'Cmp': templateString},
source: `export class Cmp {}`,
source: `
type Foo {name: string;}
export class Cmp {foo: Foo;}
`,
},
]);
templateTypeChecker = testValues.templateTypeChecker;
@@ -701,6 +705,15 @@ runInEachFileSystem(() => {
expect(program.getTypeChecker().typeToString(symbol.tsType))
.toEqual('{ hello: string; }');
});
it('literal map shorthand property', () => {
const shorthandProp =
(interpolation.expressions[2] as LiteralMap).values[0] as PropertyRead;
const symbol = templateTypeChecker.getSymbolOfNode(shorthandProp, cmp)!;
assertExpressionSymbol(symbol);
expect(program.getTypeChecker().symbolToString(symbol.tsSymbol!)).toEqual('foo');
expect(program.getTypeChecker().typeToString(symbol.tsType)).toEqual('Foo');
});
});
describe('pipes', () => {