diff --git a/packages/core/src/render3/object_literal.ts b/packages/core/src/render3/object_literal.ts index a99ffd025d..be39f34117 100644 --- a/packages/core/src/render3/object_literal.ts +++ b/packages/core/src/render3/object_literal.ts @@ -11,7 +11,7 @@ import {NO_CHANGE, bind, getTView} from './instructions'; /** - * Updates an expression in an object literal if the expression has changed. + * Updates an expression in an object or array literal if the expression has changed. * Used in objectLiteral instructions. * * @param obj Object to update @@ -27,16 +27,17 @@ function updateBinding(obj: any, key: string | number, exp: any): boolean { return false; } -/** Updates two expressions in an object literal if they have changed. */ -function updateBinding2(obj: any, key1: number, exp1: any, key2: number, exp2: any): boolean { +/** Updates two expressions in an object or array literal if they have changed. */ +function updateBinding2( + obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any): boolean { let different = updateBinding(obj, key1, exp1); return updateBinding(obj, key2, exp2) || different; } -/** Updates four expressions in an object literal if they have changed. */ +/** Updates four expressions in an object or array literal if they have changed. */ function updateBinding4( - obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, exp3: any, - key4: number, exp4: any): boolean { + obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any, + key3: string | number, exp3: any, key4: string | number, exp4: any): boolean { let different = updateBinding2(obj, key1, exp1, key2, exp2); return updateBinding2(obj, key3, exp3, key4, exp4) || different; } @@ -62,7 +63,7 @@ function copyObject(obj: any): any { } /** - * Updates the expression in the given object if it has changed and returns a copy of the object. + * Updates the expression in the given object or array if it has changed and returns a copy. * Or if the expression hasn't changed, returns NO_CHANGE. * * @param objIndex Index of object blueprint in objectLiterals @@ -83,8 +84,7 @@ export function objectLiteral1(objIndex: number, obj: any, key: string | number, } /** - * Updates the expressions in the given object if they have changed and returns a copy of the - * object. + * Updates the expressions in the given object or array if they have changed and returns a copy. * Or if no expressions have changed, returns NO_CHANGE. * * @param objIndex @@ -96,14 +96,14 @@ export function objectLiteral1(objIndex: number, obj: any, key: string | number, * @returns A copy of the array or NO_CHANGE */ export function objectLiteral2( - objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any): any { + objIndex: number, obj: any, key1: string | number, exp1: any, key2: string | number, + exp2: any): any { obj = getMutableBlueprint(objIndex, obj); return updateBinding2(obj, key1, exp1, key2, exp2) ? copyObject(obj) : NO_CHANGE; } /** - * Updates the expressions in the given object if they have changed and returns a copy of the - * object. + * Updates the expressions in the given object or array if they have changed and returns a copy. * Or if no expressions have changed, returns NO_CHANGE. * * @param objIndex @@ -117,16 +117,15 @@ export function objectLiteral2( * @returns A copy of the object or NO_CHANGE */ export function objectLiteral3( - objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, - exp3: any): any { + objIndex: number, obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any, + key3: string | number, exp3: any): any { obj = getMutableBlueprint(objIndex, obj); let different = updateBinding2(obj, key1, exp1, key2, exp2); return updateBinding(obj, key3, exp3) || different ? copyObject(obj) : NO_CHANGE; } /** - * Updates the expressions in the given object if they have changed and returns a copy of the - * object. + * Updates the expressions in the given object or array if they have changed and returns a copy. * Or if no expressions have changed, returns NO_CHANGE. * * @param objIndex @@ -142,16 +141,15 @@ export function objectLiteral3( * @returns A copy of the object or NO_CHANGE */ export function objectLiteral4( - objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, - exp3: any, key4: number, exp4: any): any { + objIndex: number, obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any, + key3: string | number, exp3: any, key4: string | number, exp4: any): any { obj = getMutableBlueprint(objIndex, obj); return updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4) ? copyObject(obj) : NO_CHANGE; } /** - * Updates the expressions in the given object if they have changed and returns a copy of the - * object. + * Updates the expressions in the given object or array if they have changed and returns a copy. * Or if no expressions have changed, returns NO_CHANGE. * * @param objIndex @@ -169,16 +167,16 @@ export function objectLiteral4( * @returns A copy of the object or NO_CHANGE */ export function objectLiteral5( - objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, - exp3: any, key4: number, exp4: any, key5: number, exp5: any): any { + objIndex: number, obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any, + key3: string | number, exp3: any, key4: string | number, exp4: any, key5: string | number, + exp5: any): any { obj = getMutableBlueprint(objIndex, obj); let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4); return updateBinding(obj, key5, exp5) || different ? copyObject(obj) : NO_CHANGE; } /** - * Updates the expressions in the given object if they have changed and returns a copy of the - * object. + * Updates the expressions in the given object or array if they have changed and returns a copy. * Or if no expressions have changed, returns NO_CHANGE. * * @param objIndex @@ -198,16 +196,16 @@ export function objectLiteral5( * @returns A copy of the object or NO_CHANGE */ export function objectLiteral6( - objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, - exp3: any, key4: number, exp4: any, key5: number, exp5: any, key6: number, exp6: any): any { + objIndex: number, obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any, + key3: string | number, exp3: any, key4: string | number, exp4: any, key5: string | number, + exp5: any, key6: string | number, exp6: any): any { obj = getMutableBlueprint(objIndex, obj); let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4); return updateBinding2(obj, key5, exp5, key6, exp6) || different ? copyObject(obj) : NO_CHANGE; } /** - * Updates the expressions in the given object if they have changed and returns a copy of the - * object. + * Updates the expressions in the given object or array if they have changed and returns a copy. * Or if no expressions have changed, returns NO_CHANGE. * * @param objIndex @@ -229,9 +227,9 @@ export function objectLiteral6( * @returns A copy of the object or NO_CHANGE */ export function objectLiteral7( - objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, - exp3: any, key4: number, exp4: any, key5: number, exp5: any, key6: number, exp6: any, - key7: number, exp7: any): any { + objIndex: number, obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any, + key3: string | number, exp3: any, key4: string | number, exp4: any, key5: string | number, + exp5: any, key6: string | number, exp6: any, key7: string | number, exp7: any): any { obj = getMutableBlueprint(objIndex, obj); let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4); different = updateBinding2(obj, key5, exp5, key6, exp6) || different; @@ -239,8 +237,7 @@ export function objectLiteral7( } /** - * Updates the expressions in the given object if they have changed and returns a copy of the - * object. + * Updates the expressions in the given object or array if they have changed and returns a copy. * Or if no expressions have changed, returns NO_CHANGE. * * @param objIndex @@ -264,9 +261,10 @@ export function objectLiteral7( * @returns A copy of the object or NO_CHANGE */ export function objectLiteral8( - objIndex: number, obj: any, key1: number, exp1: any, key2: number, exp2: any, key3: number, - exp3: any, key4: number, exp4: any, key5: number, exp5: any, key6: number, exp6: any, - key7: number, exp7: any, key8: number, exp8: any): any { + objIndex: number, obj: any, key1: string | number, exp1: any, key2: string | number, exp2: any, + key3: string | number, exp3: any, key4: string | number, exp4: any, key5: string | number, + exp5: any, key6: string | number, exp6: any, key7: string | number, exp7: any, + key8: string | number, exp8: any): any { obj = getMutableBlueprint(objIndex, obj); let different = updateBinding4(obj, key1, exp1, key2, exp2, key3, exp3, key4, exp4); return updateBinding4(obj, key5, exp5, key6, exp6, key7, exp7, key8, exp8) || different ? diff --git a/packages/core/test/render3/compiler_canonical_spec.ts b/packages/core/test/render3/compiler_canonical_spec.ts index c1263e08d8..701b720bf0 100644 --- a/packages/core/test/render3/compiler_canonical_spec.ts +++ b/packages/core/test/render3/compiler_canonical_spec.ts @@ -173,38 +173,39 @@ describe('compiler specification', () => { expect(log).toEqual(['ChildComponent', 'SomeDirective']); }); - describe('memoization', () => { - @Component({ - selector: 'my-comp', - template: ` -

{{ names[0] }}

-

{{ names[1] }}

- ` - }) - class MyComp { - @Input() names: string[]; + describe('value composition', () => { - static ngComponentDef = r3.defineComponent({ - type: MyComp, - tag: 'my-comp', - factory: function MyComp_Factory() { return new MyComp(); }, - template: function MyComp_Template(ctx: MyComp, cm: boolean) { - if (cm) { - r3.E(0, 'p'); - r3.T(1); - r3.e(); - r3.E(2, 'p'); - r3.T(3); - r3.e(); - } - r3.t(1, r3.b(ctx.names[0])); - r3.t(3, r3.b(ctx.names[1])); - }, - inputs: {names: 'names'} - }); - } + it('should support array literals', () => { - it('should memoize array literals', () => { + @Component({ + selector: 'my-comp', + template: ` +

{{ names[0] }}

+

{{ names[1] }}

+ ` + }) + class MyComp { + @Input() names: string[]; + + static ngComponentDef = r3.defineComponent({ + type: MyComp, + tag: 'my-comp', + factory: function MyComp_Factory() { return new MyComp(); }, + template: function MyComp_Template(ctx: MyComp, cm: boolean) { + if (cm) { + r3.E(0, 'p'); + r3.T(1); + r3.e(); + r3.E(2, 'p'); + r3.T(3); + r3.e(); + } + r3.t(1, r3.b(ctx.names[0])); + r3.t(3, r3.b(ctx.names[1])); + }, + inputs: {names: 'names'} + }); + } @Component({ selector: 'my-app', @@ -241,6 +242,152 @@ describe('compiler specification', () => { expect(e0_literal).toEqual(['Nancy', null]); }); + it('should support object literals', () => { + @Component({ + selector: 'object-comp', + template: ` +

{{ config.duration }}

+

{{ config.animation }}

+ ` + }) + class ObjectComp { + config: {[key: string]: any}; + + static ngComponentDef = r3.defineComponent({ + type: ObjectComp, + tag: 'object-comp', + factory: function ObjectComp_Factory() { return new ObjectComp(); }, + template: function ObjectComp_Template(ctx: ObjectComp, cm: boolean) { + if (cm) { + r3.E(0, 'p'); + r3.T(1); + r3.e(); + r3.E(2, 'p'); + r3.T(3); + r3.e(); + } + r3.t(1, r3.b(ctx.config.duration)); + r3.t(3, r3.b(ctx.config.animation)); + }, + inputs: {config: 'config'} + }); + } + + @Component({ + selector: 'my-app', + template: ` + + ` + }) + class MyApp { + name = 'slide'; + + // NORMATIVE + static ngComponentDef = r3.defineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: MyApp, cm: boolean) { + if (cm) { + r3.E(0, ObjectComp); + r3.e(); + } + r3.p(0, 'config', r3.o1(0, e0_literal, 'animation', ctx.name)); + ObjectComp.ngComponentDef.h(1, 0); + r3.r(1, 0); + } + }); + // /NORMATIVE + } + + // NORMATIVE + const e0_literal = {duration: 500, animation: null}; + // /NORMATIVE + + expect(renderComp(MyApp)).toEqual(`

500

slide

`); + expect(e0_literal).toEqual({duration: 500, animation: null}); + }); + + it('should support expressions nested deeply in object/array literals', () => { + @Component({ + selector: 'nested-comp', + template: ` +

{{ config.animation }}

+

{{config.actions[0].opacity }}

+

{{config.actions[1].duration }}

+ ` + }) + class NestedComp { + config: {[key: string]: any}; + + static ngComponentDef = r3.defineComponent({ + type: NestedComp, + tag: 'nested-comp', + factory: function NestedComp_Factory() { return new NestedComp(); }, + template: function NestedComp_Template(ctx: NestedComp, cm: boolean) { + if (cm) { + r3.E(0, 'p'); + r3.T(1); + r3.e(); + r3.E(2, 'p'); + r3.T(3); + r3.e(); + r3.E(4, 'p'); + r3.T(5); + r3.e(); + } + r3.t(1, r3.b(ctx.config.animation)); + r3.t(3, r3.b(ctx.config.actions[0].opacity)); + r3.t(5, r3.b(ctx.config.actions[1].duration)); + }, + inputs: {config: 'config'} + }); + } + + @Component({ + selector: 'my-app', + template: ` + + + ` + }) + class MyApp { + name = 'slide'; + duration = 100; + + // NORMATIVE + static ngComponentDef = r3.defineComponent({ + type: MyApp, + tag: 'my-app', + factory: function MyApp_Factory() { return new MyApp(); }, + template: function MyApp_Template(ctx: MyApp, cm: boolean) { + if (cm) { + r3.E(0, NestedComp); + r3.e(); + } + r3.p( + 0, 'config', + r3.o2( + 2, e0_literal_2, 'animation', ctx.name, 'actions', + r3.o1(1, e0_literal_1, 1, r3.o1(0, e0_literal, 'duration', ctx.duration)))); + NestedComp.ngComponentDef.h(1, 0); + r3.r(1, 0); + } + }); + // /NORMATIVE + } + + // NORMATIVE + const e0_literal = {opacity: 1, duration: null}; + const c0 = {opacity: 0, duration: 0}; + const e0_literal_1 = [c0, null]; + const e0_literal_2 = {animation: null, actions: null}; + // /NORMATIVE + + expect(renderComp(MyApp)) + .toEqual(`

slide

0

100

`); + }); + }); it('should support content projection', () => { diff --git a/packages/core/test/render3/object_literal_spec.ts b/packages/core/test/render3/object_literal_spec.ts index a4709b555b..783cfdf815 100644 --- a/packages/core/test/render3/object_literal_spec.ts +++ b/packages/core/test/render3/object_literal_spec.ts @@ -250,4 +250,72 @@ describe('array literals', () => { expect(e0_literal).toEqual({duration: 500, animation: null}); }); + it('should support expressions nested deeply in object/array literals', () => { + let nestedComp: NestedComp; + + class NestedComp { + config: {[key: string]: any}; + + static ngComponentDef = defineComponent({ + type: NestedComp, + tag: 'nested-comp', + factory: function NestedComp_Factory() { return nestedComp = new NestedComp(); }, + template: function NestedComp_Template(ctx: NestedComp, cm: boolean) {}, + inputs: {config: 'config'} + }); + } + + /** + * + * + */ + function Template(ctx: any, cm: boolean) { + if (cm) { + E(0, NestedComp); + e(); + } + p(0, 'config', o2(2, e0_literal_2, 'animation', ctx.name, 'actions', + o1(1, e0_literal_1, 1, o1(0, e0_literal, 'duration', ctx.duration)))); + NestedComp.ngComponentDef.h(1, 0); + r(1, 0); + } + + const e0_literal = {opacity: 1, duration: null}; + const e0_literal_1 = [{opacity: 0, duration: 0}, null]; + const e0_literal_2 = {animation: null, actions: null}; + + renderToHtml(Template, {name: 'slide', duration: 100}); + expect(nestedComp !.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] + }); + const firstConfig = nestedComp !.config; + + renderToHtml(Template, {name: 'slide', duration: 100}); + expect(nestedComp !.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 100}] + }); + expect(nestedComp !.config).toBe(firstConfig); + + renderToHtml(Template, {name: 'slide', duration: 50}); + expect(nestedComp !.config).toEqual({ + animation: 'slide', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] + }); + expect(nestedComp !.config).not.toBe(firstConfig); + + renderToHtml(Template, {name: 'tap', duration: 50}); + expect(nestedComp !.config).toEqual({ + animation: 'tap', + actions: [{opacity: 0, duration: 0}, {opacity: 1, duration: 50}] + }); + + expect(e0_literal).toEqual({opacity: 1, duration: null}); + expect(e0_literal_1).toEqual([{opacity: 0, duration: 0}, null]); + expect(e0_literal_2).toEqual({animation: null, actions: null}); + }); + + });