Skip to content

Commit e407280

Browse files
crisbetokirjs
authored andcommitted
feat(core): support spread expressions in object literals
Adds support for spread expressions inside of object literals. This can be handy when constructing maps for `class` bindings.
1 parent 8154924 commit e407280

8 files changed

Lines changed: 169 additions & 2 deletions

File tree

packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/GOLDEN_PARTIAL.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,53 @@ export declare class MyModule {
588588
static ɵinj: i0.ɵɵInjectorDeclaration<MyModule>;
589589
}
590590

591+
/****************************************************************************************************
592+
* PARTIAL FILE: object_literal_spread.js
593+
****************************************************************************************************/
594+
import { Component } from '@angular/core';
595+
import * as i0 from "@angular/core";
596+
export class ObjectComp {
597+
foo = {};
598+
bar = {};
599+
baz = {};
600+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ObjectComp, deps: [], target: i0.ɵɵFactoryTarget.Component });
601+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: ObjectComp, isStandalone: true, selector: "ng-component", ngImport: i0, template: `
602+
@let simple = {...foo};
603+
@let otherProps = {a: 1, ...foo, b: 2};
604+
@let multipleSpreads = {...foo, a: 1, ...bar, ...baz, b: 2};
605+
@let objectLiteral = {a: 1, ...{b: {...{c: 3}}}};
606+
607+
<!-- Use the objects so they don't get flagged as unused. -->
608+
{{simple}} {{otherProps}} {{multipleSpreads}} {{objectLiteral}}
609+
`, isInline: true });
610+
}
611+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: ObjectComp, decorators: [{
612+
type: Component,
613+
args: [{
614+
template: `
615+
@let simple = {...foo};
616+
@let otherProps = {a: 1, ...foo, b: 2};
617+
@let multipleSpreads = {...foo, a: 1, ...bar, ...baz, b: 2};
618+
@let objectLiteral = {a: 1, ...{b: {...{c: 3}}}};
619+
620+
<!-- Use the objects so they don't get flagged as unused. -->
621+
{{simple}} {{otherProps}} {{multipleSpreads}} {{objectLiteral}}
622+
`,
623+
}]
624+
}] });
625+
626+
/****************************************************************************************************
627+
* PARTIAL FILE: object_literal_spread.d.ts
628+
****************************************************************************************************/
629+
import * as i0 from "@angular/core";
630+
export declare class ObjectComp {
631+
foo: {};
632+
bar: {};
633+
baz: {};
634+
static ɵfac: i0.ɵɵFactoryDeclaration<ObjectComp, never>;
635+
static ɵcmp: i0.ɵɵComponentDeclaration<ObjectComp, "ng-component", never, {}, {}, never, never, true, never>;
636+
}
637+
591638
/****************************************************************************************************
592639
* PARTIAL FILE: literal_nested_expression.js
593640
****************************************************************************************************/

packages/compiler-cli/test/compliance/test_cases/r3_compiler_compliance/components_and_directives/value_composition/TEST_CASES.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,19 @@
238238
}
239239
]
240240
},
241+
{
242+
"description": "should support object literals with spread assignments",
243+
"inputFiles": ["object_literal_spread.ts"],
244+
"compilerOptions": {
245+
"target": "es2022"
246+
},
247+
"expectations": [
248+
{
249+
"failureMessage": "Invalid object literal binding",
250+
"files": ["object_literal_spread.js"]
251+
}
252+
]
253+
},
241254
{
242255
"description": "should support expressions nested deeply in object/array literals",
243256
"inputFiles": ["literal_nested_expression.ts"],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const $c0$ = a0 => ({ ...a0 });
2+
const $c1$ = a0 => ({ a: 1, ...a0, b: 2 });
3+
const $c2$ = (a0, a1, a2) => ({ ...a0, a: 1, ...a1, ...a2, b: 2 });
4+
const $c3$ = () => ({ c: 3 });
5+
const $c4$ = a0 => ({ b: a0 });
6+
const $c5$ = a0 => ({ a: 1, ...a0 });
7+
8+
9+
10+
$r3$.ɵɵdefineComponent({
11+
12+
decls: 1,
13+
vars: 19,
14+
template: function ObjectComp_Template(rf, ctx) {
15+
16+
if (rf & 2) {
17+
const simple_r1 = $r3$.ɵɵpureFunction1(4, $c0$, ctx.foo);
18+
const otherProps_r2 = $r3$.ɵɵpureFunction1(6, $c1$, ctx.foo);
19+
const multipleSpreads_r3 = $r3$.ɵɵpureFunction3(8, $c2$, ctx.foo, ctx.bar, ctx.baz);
20+
const objectLiteral_r4 = $r3$.ɵɵpureFunction1(17, $c5$, $r3$.ɵɵpureFunction1(15, $c4$, $r3$.ɵɵpureFunction1(13, $c0$, $r3$.ɵɵpureFunction0(12, $c3$))));
21+
22+
}
23+
},
24+
25+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {Component} from '@angular/core';
2+
3+
@Component({
4+
template: `
5+
@let simple = {...foo};
6+
@let otherProps = {a: 1, ...foo, b: 2};
7+
@let multipleSpreads = {...foo, a: 1, ...bar, ...baz, b: 2};
8+
@let objectLiteral = {a: 1, ...{b: {...{c: 3}}}};
9+
10+
<!-- Use the objects so they don't get flagged as unused. -->
11+
{{simple}} {{otherProps}} {{multipleSpreads}} {{objectLiteral}}
12+
`,
13+
})
14+
export class ObjectComp {
15+
foo = {};
16+
bar = {};
17+
baz = {};
18+
}

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3340,6 +3340,31 @@ runInEachFileSystem(() => {
33403340
expect(diags.length).toBe(0);
33413341
});
33423342

3343+
it('should type check object spread assignments in templates', () => {
3344+
env.write(
3345+
'test.ts',
3346+
`
3347+
import {Component} from '@angular/core';
3348+
3349+
@Component({
3350+
selector: 'test',
3351+
template: '@let obj = {a: 1, ...foo}; {{checkObj(obj)}}',
3352+
})
3353+
export class TestCmp {
3354+
foo = {b: 'two'};
3355+
3356+
checkObj(obj: {a: number, b: number}) {}
3357+
}
3358+
`,
3359+
);
3360+
3361+
const diags = env.driveDiagnostics();
3362+
expect(diags.length).toEqual(1);
3363+
expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText).toContain(
3364+
`Argument of type '{ b: string; a: number; }' is not assignable to parameter of type '{ a: number; b: number; }'.`,
3365+
);
3366+
});
3367+
33433368
describe('template literals', () => {
33443369
it('should treat template literals as strings', () => {
33453370
env.write(

packages/compiler/src/expression_parser/parser.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import {
3232
LiteralArray,
3333
LiteralMap,
3434
LiteralMapKey,
35+
LiteralMapPropertyKey,
36+
LiteralMapSpreadKey,
3537
LiteralPrimitive,
3638
NonNullAssert,
3739
ParenthesizedExpression,
@@ -1195,11 +1197,23 @@ class _ParseAST {
11951197
this.rbracesExpected++;
11961198
do {
11971199
const keyStart = this.inputIndex;
1200+
1201+
if (this.next.isOperator('...')) {
1202+
this.advance();
1203+
keys.push({
1204+
kind: 'spread',
1205+
span: this.span(keyStart),
1206+
sourceSpan: this.sourceSpan(keyStart),
1207+
} satisfies LiteralMapSpreadKey);
1208+
values.push(this.parsePipe());
1209+
continue;
1210+
}
1211+
11981212
const quoted = this.next.isString();
11991213
const key = this.expectIdentifierOrKeywordOrString();
12001214
const keySpan = this.span(keyStart);
12011215
const keySourceSpan = this.sourceSpan(keyStart);
1202-
const literalMapKey: LiteralMapKey = {
1216+
const literalMapKey: LiteralMapPropertyKey = {
12031217
kind: 'property',
12041218
key,
12051219
quoted,

packages/compiler/test/expression_parser/parser_spec.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,13 @@ describe('parser', () => {
202202
expectActionError('{a["b"]}', 'expected } at column 3');
203203
expectActionError('{1234}', ' expected identifier, keyword, or string at column 2');
204204
});
205+
206+
it('should parse spread assignments in object literals', () => {
207+
checkAction('{...foo}');
208+
checkAction('{one: 1, ...foo, two: 2}');
209+
checkAction('{...foo, middle: true, ...bar}');
210+
checkAction('{...{...{...{foo: 1}}}}');
211+
});
205212
});
206213

207214
describe('member access', () => {
@@ -737,14 +744,15 @@ describe('parser', () => {
737744
});
738745

739746
it('should record span for literal map keys', () => {
740-
const ast = parseBinding('{one: 1, two: "the number two", three, "four": 4}');
747+
const ast = parseBinding('{one: 1, two: "the number two", three, "four": 4, ...five}');
741748
const literal = ast.ast as LiteralMap;
742749
const getSource = (span: ParseSpan) => ast.source?.substring(span.start, span.end);
743750

744751
expect(getSource(literal.keys[0].span)).toBe('one');
745752
expect(getSource(literal.keys[1].span)).toBe('two');
746753
expect(getSource(literal.keys[2].span)).toBe('three');
747754
expect(getSource(literal.keys[3].span)).toBe('"four"');
755+
expect(getSource(literal.keys[4].span)).toBe('...');
748756
});
749757
});
750758

packages/core/test/acceptance/integration_spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2741,6 +2741,23 @@ describe('acceptance integration tests', () => {
27412741
expect(fixture.componentInstance.e!.defaultPrevented).toBe(false);
27422742
});
27432743

2744+
it('should support object spread assigments in templates', () => {
2745+
@Component({
2746+
template: '@let obj = {a: {...foo}}; Hello, {{obj.a.b}}',
2747+
})
2748+
class TestComponent {
2749+
foo = {b: 'Frodo'};
2750+
}
2751+
2752+
const fixture = TestBed.createComponent(TestComponent);
2753+
fixture.detectChanges();
2754+
expect(fixture.nativeElement.textContent).toContain('Hello, Frodo');
2755+
2756+
fixture.componentInstance.foo = {b: 'Bilbo'};
2757+
fixture.detectChanges();
2758+
expect(fixture.nativeElement.textContent).toContain('Hello, Bilbo');
2759+
});
2760+
27442761
it('should have correct operator precedence', () => {
27452762
@Component({
27462763
template: '{{1 + 10 ** -2 * 3}}',

0 commit comments

Comments
 (0)