From 2961bf06c61c78695d453b05fe6d5dd8a4f91da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1ko=20Hevery?= Date: Wed, 8 Jan 2020 11:32:33 -0800 Subject: [PATCH] refactor(ivy): move `hostVars`/`hostAttrs` from instruction to `DirectiveDef` (#34683) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change moves information from instructions to declarative position: - `ɵɵallocHostVars(vars)` => `DirectiveDef.hostVars` - `ɵɵelementHostAttrs(attrs)` => `DirectiveDef.hostAttrs` When merging directives it is necessary to know about `hostVars` and `hostAttrs`. Before this change the information was stored in the `hostBindings` function. This was problematic, because in order to get to the information the `hostBindings` would have to be executed. In order for `hostBindings` to be executed the directives would have to be instantiated. This means that the directive instantiation would happen before we had knowledge about the `hostAttrs` and as a result the directive could observe in the constructor that not all of the `hostAttrs` have been applied. This further complicates the runtime as we have to apply `hostAttrs` in parts over many invocations. `ɵɵallocHostVars` was unnecessarily complicated because it would have to update the `LView` (and Blueprint) while existing directives are already executing. By moving it out of `hostBindings` function we can access it statically and we can create correct `LView` (and Blueprint) in a single pass. This change only changes how the instructions are generated, but does not change the runtime much. (We cheat by emulating the old behavior by calling `ɵɵallocHostVars` and `ɵɵelementHostAttrs`) Subsequent change will refactor the runtime to take advantage of the static information. PR Close #34683 --- aio/scripts/_payload-limits.json | 2 +- integration/_payload-limits.json | 2 +- packages/common/src/directives/ng_class.ts | 6 +- packages/common/src/directives/ng_style.ts | 6 +- .../ngcc/test/integration/ngcc_spec.ts | 1 - .../compliance/r3_compiler_compliance_spec.ts | 8 +- .../r3_view_compiler_binding_spec.ts | 46 ++----- .../r3_view_compiler_styling_spec.ts | 52 +++----- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 16 +-- .../compiler/src/render3/r3_identifiers.ts | 4 - .../compiler/src/render3/view/compiler.ts | 18 +-- .../src/render3/view/styling_builder.ts | 24 +--- .../core/src/core_render3_private_export.ts | 2 - packages/core/src/interface/type.ts | 29 +++++ packages/core/src/render3/component.ts | 10 +- packages/core/src/render3/definition.ts | 91 +++++++++++++- packages/core/src/render3/errors.ts | 2 + .../features/inherit_definition_feature.ts | 58 ++++++--- .../render3/features/ng_onchanges_feature.ts | 2 +- packages/core/src/render3/index.ts | 3 - .../render3/instructions/alloc_host_vars.ts | 3 + .../core/src/render3/instructions/element.ts | 2 + .../core/src/render3/instructions/shared.ts | 23 +++- .../core/src/render3/interfaces/definition.ts | 64 ++++++++-- packages/core/src/render3/interfaces/node.ts | 9 ++ packages/core/src/render3/jit/environment.ts | 2 - packages/core/src/render3/util/attrs_utils.ts | 113 +++++++++++++++++ .../inherit_definition_feature_spec.ts | 82 ++++++++++++ .../cyclic_import/bundle.golden_symbols.json | 18 +++ .../hello_world/bundle.golden_symbols.json | 96 ++++++++++++++ .../bundling/todo/bundle.golden_symbols.json | 15 +++ .../core/test/render3/util/attr_util_spec.ts | 119 ++++++++++++++++++ tools/public_api_guard/core/core.d.ts | 8 +- 33 files changed, 751 insertions(+), 185 deletions(-) create mode 100644 packages/core/test/render3/util/attr_util_spec.ts diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index 51c27cf64d..bbbaad4daf 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime-es2015": 3097, - "main-es2015": 439352, + "main-es2015": 438671, "polyfills-es2015": 52503 } } diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 213d83ed95..fb4397651f 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 15783, + "main-es2015": 18496, "polyfills-es2015": 36808 } } diff --git a/packages/common/src/directives/ng_class.ts b/packages/common/src/directives/ng_class.ts index fe6b8e6ff4..b3f5235a26 100644 --- a/packages/common/src/directives/ng_class.ts +++ b/packages/common/src/directives/ng_class.ts @@ -5,7 +5,7 @@ * 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 {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵclassMap, ɵɵdefineDirective} from '@angular/core'; +import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵclassMap, ɵɵdefineDirective} from '@angular/core'; import {NgClassImpl, NgClassImplProvider} from './ng_class_impl'; @@ -32,10 +32,8 @@ export const ngClassDirectiveDef__PRE_R3__ = undefined; export const ngClassDirectiveDef__POST_R3__ = ɵɵdefineDirective({ type: function() {} as any, selectors: null as any, + hostVars: 2, hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { - if (rf & ɵRenderFlags.Create) { - ɵɵallocHostVars(2); - } if (rf & ɵRenderFlags.Update) { ɵɵclassMap(ctx.getValue()); } diff --git a/packages/common/src/directives/ng_style.ts b/packages/common/src/directives/ng_style.ts index 97670a906e..c978d05b9a 100644 --- a/packages/common/src/directives/ng_style.ts +++ b/packages/common/src/directives/ng_style.ts @@ -5,7 +5,7 @@ * 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 {Directive, DoCheck, Input, ɵRenderFlags, ɵɵallocHostVars, ɵɵdefineDirective, ɵɵstyleMap} from '@angular/core'; +import {Directive, DoCheck, Input, ɵRenderFlags, ɵɵdefineDirective, ɵɵstyleMap} from '@angular/core'; import {NgStyleImpl, NgStyleImplProvider} from './ng_style_impl'; @@ -33,10 +33,8 @@ export const ngStyleFactoryDef__PRE_R3__ = undefined; export const ngStyleDirectiveDef__POST_R3__ = ɵɵdefineDirective({ type: function() {} as any, selectors: null as any, + hostVars: 2, hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { - if (rf & ɵRenderFlags.Create) { - ɵɵallocHostVars(2); - } if (rf & ɵRenderFlags.Update) { ɵɵstyleMap(ctx.getValue()); } diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 7d2ad8eee9..b54f851051 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -216,7 +216,6 @@ runInEachFileSystem(() => { const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`)); expect(jsContents).not.toMatch(/\bconst \w+\s*=/); - expect(jsContents).toMatch(/\bvar _c0 =/); }); it('should add ɵfac but not duplicate ɵprov properties on injectables', () => { diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts index abb77063b8..2935274601 100644 --- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts @@ -431,10 +431,8 @@ describe('compiler compliance', () => { const $_c1$ = function (a0, a1) { return { value: a0, params: a1 }; }; const $_c2$ = function (a0, a1) { return { collapsedWidth: a0, expandedWidth: a1 }; }; … + hostVars: 14, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(14); - } if (rf & 2) { $r3$.ɵɵupdateSyntheticHostBinding("@expansionHeight", $r3$.ɵɵpureFunction2(5, $_c1$, ctx.getExpandedState(), @@ -3499,10 +3497,8 @@ describe('compiler compliance', () => { // ... BaseClass.ɵdir = $r3$.ɵɵdefineDirective({ type: BaseClass, + hostVars: 1, hostBindings: function BaseClass_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(1); - } if (rf & 2) { $r3$.ɵɵattribute("tabindex", ctx.tabindex); } diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts index 21dc1c2a8a..bfa2910236 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_binding_spec.ts @@ -668,13 +668,11 @@ describe('compiler compliance: bindings', () => { }; const HostBindingDirDeclaration = ` - HostBindingDir.ɵdir = $r3$.ɵɵdefineDirective({ - type: HostBindingDir, - selectors: [["", "hostBindingDir", ""]], + HostBindingDir.ɵdir = $r3$.ɵɵdefineDirective({ + type: HostBindingDir, + selectors: [["", "hostBindingDir", ""]], + hostVars: 1, hostBindings: function HostBindingDir_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(1); - } if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.dirId); } @@ -717,10 +715,8 @@ describe('compiler compliance: bindings', () => { HostBindingComp.ɵcmp = $r3$.ɵɵdefineComponent({ type: HostBindingComp, selectors: [["host-binding-comp"]], + hostVars: 3, hostBindings: function HostBindingComp_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(3); - } if (rf & 2) { $r3$.ɵɵhostProperty("id", $r3$.ɵɵpureFunction1(1, $ff$, ctx.id)); } @@ -764,10 +760,8 @@ describe('compiler compliance: bindings', () => { HostAttributeDir.ɵdir = $r3$.ɵɵdefineDirective({ type: HostAttributeDir, selectors: [["", "hostAttributeDir", ""]], + hostVars: 1, hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(1); - } if (rf & 2) { $r3$.ɵɵattribute("required", ctx.required); } @@ -803,16 +797,10 @@ describe('compiler compliance: bindings', () => { }; const HostAttributeDirDeclaration = ` - const $c0$ = ["aria-label", "label"]; - … HostAttributeDir.ɵdir = $r3$.ɵɵdefineDirective({ type: HostAttributeDir, selectors: [["", "hostAttributeDir", ""]], - hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵelementHostAttrs($c0$); - } - } + hostAttrs: ["aria-label", "label"] }); `; @@ -859,32 +847,20 @@ describe('compiler compliance: bindings', () => { }; const CompAndDirDeclaration = ` - const $c0$ = ["title", "hello there from component", ${AttributeMarker.Styles}, "opacity", "1"]; - const $c1$ = ["title", "hello there from directive", ${AttributeMarker.Classes}, "one", "two", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"]; - … HostAttributeComp.ɵcmp = $r3$.ɵɵdefineComponent({ type: HostAttributeComp, selectors: [["my-host-attribute-component"]], - hostBindings: function HostAttributeComp_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵelementHostAttrs($c0$); - … - } - … - } + hostAttrs: ["title", "hello there from component", ${AttributeMarker.Styles}, "opacity", "1"], … HostAttributeDir.ɵdir = $r3$.ɵɵdefineDirective({ type: HostAttributeDir, selectors: [["", "hostAttributeDir", ""]], + hostAttrs: ["title", "hello there from directive", ${AttributeMarker.Classes}, "one", "two", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"], + hostVars: 2, hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(2); - $r3$.ɵɵelementHostAttrs($c1$); - … - } … } - `; + `; const result = compile(files, angularFiles); const source = result.source; diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 46083c897c..ecc21689f3 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -333,9 +333,9 @@ describe('compiler compliance: styling', () => { const template = ` MyAnimDir.ɵdir = $r3$.ɵɵdefineDirective({ … + hostVars: 1, hostBindings: function MyAnimDir_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - $r3$.ɵɵallocHostVars(1); $r3$.ɵɵcomponentHostSyntheticListener("@myAnim.start", function MyAnimDir_animation_myAnim_start_HostBindingHandler($event) { return ctx.onStart(); })("@myAnim.done", function MyAnimDir_animation_myAnim_done_HostBindingHandler($event) { return ctx.onDone(); }); } if (rf & 2) { $r3$.ɵɵupdateSyntheticHostBinding("@myAnim", ctx.myAnimState); @@ -1011,11 +1011,9 @@ describe('compiler compliance: styling', () => { }; const template = ` + hostAttrs: [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"], + hostVars: 6, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(6); - $r3$.ɵɵelementHostAttrs($e0_attrs$); - } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyle); @@ -1070,10 +1068,8 @@ describe('compiler compliance: styling', () => { }; const template = ` + hostVars: 8, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(8); - } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyle); @@ -1143,10 +1139,8 @@ describe('compiler compliance: styling', () => { `; const hostBindings = ` + hostVars: 6, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(6); - } if (rf & 2) { $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵstyleMap(ctx.myStyleExp); @@ -1209,29 +1203,23 @@ describe('compiler compliance: styling', () => { // NOTE: IF YOU ARE CHANGING THIS COMPILER SPEC, YOU MAY NEED TO CHANGE THE DIRECTIVE // DEF THAT'S HARD-CODED IN `ng_class.ts`. const template = ` - function ClassDirective_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(2); - } + hostVars: 2, + hostBindings: function ClassDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵclassMap(ctx.myClassMap); } } … - function WidthDirective_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(2); - } + hostVars: 2, + hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵstyleProp("width", ctx.myWidth); $r3$.ɵɵclassProp("foo", ctx.myFooClass); } } … - function HeightDirective_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(2); - } + hostVars: 2, + hostBindings: function HeightDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵstyleProp("height", ctx.myHeight); $r3$.ɵɵclassProp("bar", ctx.myBarClass); @@ -1840,13 +1828,9 @@ describe('compiler compliance: styling', () => { }; const template = ` - const $_c0$ = ["title", "foo title", ${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"]; - … + hostAttrs: ["title", "foo title", ${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"], + hostVars: 6, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(6); - $r3$.ɵɵelementHostAttrs($_c0$); - } if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title); $r3$.ɵɵstyleSanitizer($r3$.ɵɵdefaultStyleSanitizer); @@ -1885,10 +1869,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - $r3$.ɵɵallocHostVars(4); - } + hostVars: 4, + hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title); $r3$.ɵɵstyleProp("width", ctx.myWidth); @@ -2051,10 +2033,8 @@ describe('compiler compliance: styling', () => { }; const template = ` + hostVars: 9, hostBindings: function MyDir_HostBindings(rf, ctx, elIndex) { - … - $r3$.ɵɵallocHostVars(9); - … if (rf & 2) { $r3$.ɵɵhostProperty("title", ctx.title); $r3$.ɵɵupdateSyntheticHostBinding("@anim", diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 8abe51302a..061888e97d 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -2340,9 +2340,9 @@ runInEachFileSystem(os => { env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` + hostVars: 3, hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { - i0.ɵɵallocHostVars(3); i0.ɵɵlistener("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onClick($event); })("click", function FooCmp_click_HostBindingHandler($event) { return ctx.onBodyClick($event); }, false, i0.ɵɵresolveBody)("change", function FooCmp_change_HostBindingHandler($event) { return ctx.onChange(ctx.arg1, ctx.arg2, ctx.arg3); }); } if (rf & 2) { @@ -2376,7 +2376,7 @@ runInEachFileSystem(os => { `); env.driveMain(); const jsContents = env.getContents('test.js'); - expect(jsContents).toContain('i0.ɵɵelementHostAttrs(["test", test])'); + expect(jsContents).toContain('hostAttrs: ["test", test]'); }); it('should accept enum values as host bindings', () => { @@ -4262,10 +4262,8 @@ runInEachFileSystem(os => { env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` + hostVars: 6, hostBindings: function UnsafeAttrsDirective_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - i0.ɵɵallocHostVars(6); - } if (rf & 2) { i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrl)("profile", ctx.attrProfile, i0.ɵɵsanitizeResourceUrl)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle); } @@ -4312,10 +4310,8 @@ runInEachFileSystem(os => { env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` + hostVars: 6, hostBindings: function UnsafePropsDirective_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - i0.ɵɵallocHostVars(6); - } if (rf & 2) { i0.ɵɵhostProperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.propAction, i0.ɵɵsanitizeUrl)("profile", ctx.propProfile, i0.ɵɵsanitizeResourceUrl)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.propSafeTitle); } @@ -4347,10 +4343,8 @@ runInEachFileSystem(os => { env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` + hostVars: 6, hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { - if (rf & 1) { - i0.ɵɵallocHostVars(6); - } if (rf & 2) { i0.ɵɵhostProperty("src", ctx.srcProp)("href", ctx.hrefProp)("title", ctx.titleProp); i0.ɵɵattribute("src", ctx.srcAttr)("href", ctx.hrefAttr)("title", ctx.titleAttr); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index dc43f681a2..903986fd9f 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -115,8 +115,6 @@ export class Identifiers { static styleSanitizer: o.ExternalReference = {name: 'ɵɵstyleSanitizer', moduleName: CORE}; - static elementHostAttrs: o.ExternalReference = {name: 'ɵɵelementHostAttrs', moduleName: CORE}; - static containerCreate: o.ExternalReference = {name: 'ɵɵcontainer', moduleName: CORE}; static nextContext: o.ExternalReference = {name: 'ɵɵnextContext', moduleName: CORE}; @@ -129,8 +127,6 @@ export class Identifiers { static disableBindings: o.ExternalReference = {name: 'ɵɵdisableBindings', moduleName: CORE}; - static allocHostVars: o.ExternalReference = {name: 'ɵɵallocHostVars', moduleName: CORE}; - static getCurrentView: o.ExternalReference = {name: 'ɵɵgetCurrentView', moduleName: CORE}; static textInterpolate: o.ExternalReference = {name: 'ɵɵtextInterpolate', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index f0fa0e4944..e7971b8cd3 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -67,7 +67,7 @@ function baseDirectiveFields( definitionMap.set( 'hostBindings', createHostBindingsFunction( meta.host, meta.typeSourceSpan, bindingParser, constantPool, - meta.selector || '', meta.name)); + meta.selector || '', meta.name, definitionMap)); // e.g 'inputs: {a: 'a'}` definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true)); @@ -528,8 +528,8 @@ function createViewQueriesFunction( // Return a host binding function or null if one is not necessary. function createHostBindingsFunction( hostBindingsMetadata: R3HostMetadata, typeSourceSpan: ParseSourceSpan, - bindingParser: BindingParser, constantPool: ConstantPool, selector: string, - name?: string): o.Expression|null { + bindingParser: BindingParser, constantPool: ConstantPool, selector: string, name: string, + definitionMap: DefinitionMap): o.Expression|null { // Initialize hostVarsCount to number of bound host properties (interpolations illegal) const hostVarsCount = Object.keys(hostBindingsMetadata.properties).length; const elVarExp = o.variable('elIndex'); @@ -651,14 +651,7 @@ function createHostBindingsFunction( // to the host element alongside any of the provided host attributes that were // collected earlier. const hostAttrs = convertAttributesToExpressions(hostBindingsMetadata.attributes); - const hostInstruction = styleBuilder.buildHostAttrsInstruction(null, hostAttrs, constantPool); - if (hostInstruction && hostInstruction.calls.length > 0) { - createStatements.push( - chainedInstruction( - hostInstruction.reference, - hostInstruction.calls.map(call => convertStylingCall(call, bindingContext, bindingFn))) - .toStmt()); - } + styleBuilder.assignHostAttrs(hostAttrs, definitionMap); if (styleBuilder.hasBindings) { // finally each binding that was registered in the statement above will need to be added to @@ -681,8 +674,7 @@ function createHostBindingsFunction( } if (totalHostVarsCount) { - createStatements.unshift( - o.importExpr(R3.allocHostVars).callFn([o.literal(totalHostVarsCount)]).toStmt()); + definitionMap.set('hostVars', o.literal(totalHostVarsCount)); } if (createStatements.length > 0 || updateStatements.length > 0) { diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 67d546ecab..e05df038ce 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -16,7 +16,7 @@ import {Identifiers as R3} from '../r3_identifiers'; import {hyphenate, parse as parseStyle} from './style_parser'; import {ValueConverter} from './template'; -import {getInterpolationArgsLength} from './util'; +import {DefinitionMap, getInterpolationArgsLength} from './util'; const IMPORTANT_FLAG = '!important'; @@ -279,27 +279,11 @@ export class StylingBuilder { * responsible for registering initial styles (within a directive hostBindings' creation block), * as well as any of the provided attribute values, to the directive host element. */ - buildHostAttrsInstruction( - sourceSpan: ParseSourceSpan|null, attrs: o.Expression[], - constantPool: ConstantPool): StylingInstruction|null { + assignHostAttrs(attrs: o.Expression[], definitionMap: DefinitionMap): void { if (this._directiveExpr && (attrs.length || this._hasInitialValues)) { - return { - reference: R3.elementHostAttrs, - calls: [{ - sourceSpan, - allocateBindingSlots: 0, - params: () => { - // params => elementHostAttrs(attrs) - this.populateInitialStylingAttrs(attrs); - const attrArray = !attrs.some(attr => attr instanceof o.WrappedNodeExpr) ? - getConstantLiteralFromArray(constantPool, attrs) : - o.literalArr(attrs); - return [attrArray]; - } - }] - }; + this.populateInitialStylingAttrs(attrs); + definitionMap.set('hostAttrs', o.literalArr(attrs)); } - return null; } /** diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 49beffaba4..056001cf85 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -117,7 +117,6 @@ export { ɵɵreference, ɵɵenableBindings, ɵɵdisableBindings, - ɵɵallocHostVars, ɵɵelementContainerStart, ɵɵelementContainerEnd, ɵɵelementContainer, @@ -144,7 +143,6 @@ export { ɵɵstylePropInterpolate8, ɵɵstylePropInterpolateV, ɵɵclassProp, - ɵɵelementHostAttrs, ɵɵselect, ɵɵadvance, diff --git a/packages/core/src/interface/type.ts b/packages/core/src/interface/type.ts index faec83d8a1..e8b0188042 100644 --- a/packages/core/src/interface/type.ts +++ b/packages/core/src/interface/type.ts @@ -37,3 +37,32 @@ export interface Type extends Function { new (...args: any[]): T; } export type Mutable = { [P in K]: T[P]; }; + +/** + * Returns a writable type version of type. + * + * USAGE: + * Given: + * ``` + * interface Person {readonly name: string} + * ``` + * + * We would like to get a read/write version of `Person`. + * ``` + * const WritablePerson = Writable; + * ``` + * + * The result is that you can do: + * + * ``` + * const readonlyPerson: Person = {name: 'Marry'}; + * readonlyPerson.name = 'John'; // TypeError + * (readonlyPerson as WritablePerson).name = 'John'; // OK + * + * // Error: Correctly detects that `Person` did not have `age` property. + * (readonlyPerson as WritablePerson).age = 30; + * ``` + */ +export type Writable = { + -readonly[K in keyof T]: T[K]; +}; diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 2e2825fd26..0aedf5a6d9 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -114,7 +114,7 @@ export function renderComponent( const rendererFactory = opts.rendererFactory || domRendererFactory3; const sanitizer = opts.sanitizer || null; const componentDef = getComponentDef(componentType) !; - if (componentDef.type != componentType) componentDef.type = componentType; + if (componentDef.type != componentType) (componentDef as{type: Type}).type = componentType; // The first index of the first selector is the tag name. const componentTag = componentDef.selectors ![0] ![0] as string; @@ -211,7 +211,13 @@ export function createRootComponent( } const rootTNode = getPreviousOrParentTNode(); - if (tView.firstCreatePass && componentDef.hostBindings) { + // TODO(misko-next): This is a temporary work around for the fact that we moved the information + // from instruction to declaration. The workaround is to just call the instruction as if it was + // part of the `hostAttrs`. + // The check for componentDef.hostBindings is wrong since now some directives may not + // have componentDef.hostBindings but they still need to process hostVars and hostAttrs + if (tView.firstCreatePass && (componentDef.hostBindings || componentDef.hostVars !== 0 || + componentDef.hostAttrs !== null)) { const elementIndex = rootTNode.index - HEADER_OFFSET; setActiveHostElement(elementIndex); incrementActiveDirectiveId(); diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index a51ff043c0..26ad0dbf0a 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -18,14 +18,17 @@ import {stringify} from '../util/stringify'; import {EMPTY_ARRAY, EMPTY_OBJ} from './empty'; import {NG_COMP_DEF, NG_DIR_DEF, NG_FACTORY_DEF, NG_LOC_ID_DEF, NG_MOD_DEF, NG_PIPE_DEF} from './fields'; import {ComponentDef, ComponentDefFeature, ComponentTemplate, ComponentType, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, DirectiveTypesOrFactory, FactoryFn, HostBindingsFunction, PipeDef, PipeType, PipeTypesOrFactory, ViewQueriesFunction} from './interfaces/definition'; -import {TConstants} from './interfaces/node'; -// while SelectorFlags is unused here, it's required so that types don't get resolved lazily -// see: https://github.com/Microsoft/web-build-tools/issues/1050 +import {AttributeMarker, TAttributes, TConstants} from './interfaces/node'; import {CssSelectorList, SelectorFlags} from './interfaces/projection'; import {NgModuleType} from './ng_module_ref'; let _renderCompCount = 0; +// While these types are unused here, they are required so that types don't +// get resolved lazily. see: https://github.com/Microsoft/web-build-tools/issues/1050 +type _web_build_tools_issue_1050_SelectorFlags = SelectorFlags; +type _web_build_tools_issue_1050_AttributeMarker = AttributeMarker; + /** * Create a component definition object. * @@ -130,6 +133,46 @@ export function ɵɵdefineComponent(componentDefinition: { */ hostBindings?: HostBindingsFunction; + /** + * The number of bindings in this directive `hostBindings` (including pure fn bindings). + * + * Used to calculate the length of the component's LView array, so we + * can pre-fill the array and set the host binding start index. + */ + hostVars?: number; + + /** + * Assign static attribute values to a host element. + * + * This property will assign static attribute values as well as class and style + * values to a host element. Since attribute values can consist of different types of values, the + * `hostAttrs` array must include the values in the following format: + * + * attrs = [ + * // static attributes (like `title`, `name`, `id`...) + * attr1, value1, attr2, value, + * + * // a single namespace value (like `x:id`) + * NAMESPACE_MARKER, namespaceUri1, name1, value1, + * + * // another single namespace value (like `x:name`) + * NAMESPACE_MARKER, namespaceUri2, name2, value2, + * + * // a series of CSS classes that will be applied to the element (no spaces) + * CLASSES_MARKER, class1, class2, class3, + * + * // a series of CSS styles (property + value) that will be applied to the element + * STYLES_MARKER, prop1, value1, prop2, value2 + * ] + * + * All non-class and non-style attributes must be defined at the start of the list + * first before all class and style values are set. When there is a change in value + * type (like when classes and styles are introduced) a marker must be used to separate + * the entries. The marker values themselves are set via entries found in the + * [AttributeMarker] enum. + */ + hostAttrs?: TAttributes; + /** * Function to create instances of content queries associated with a given directive. */ @@ -263,6 +306,8 @@ export function ɵɵdefineComponent(componentDefinition: { consts: componentDefinition.consts || null, ngContentSelectors: componentDefinition.ngContentSelectors, hostBindings: componentDefinition.hostBindings || null, + hostVars: componentDefinition.hostVars || 0, + hostAttrs: componentDefinition.hostAttrs || null, contentQueries: componentDefinition.contentQueries || null, declaredInputs: declaredInputs, inputs: null !, // assigned in noSideEffects @@ -588,6 +633,46 @@ export const ɵɵdefineDirective = ɵɵdefineComponent as any as(directiveDef */ hostBindings?: HostBindingsFunction; + /** + * The number of bindings in this directive `hostBindings` (including pure fn bindings). + * + * Used to calculate the length of the component's LView array, so we + * can pre-fill the array and set the host binding start index. + */ + hostVars?: number; + + /** + * Assign static attribute values to a host element. + * + * This property will assign static attribute values as well as class and style + * values to a host element. Since attribute values can consist of different types of values, the + * `hostAttrs` array must include the values in the following format: + * + * attrs = [ + * // static attributes (like `title`, `name`, `id`...) + * attr1, value1, attr2, value, + * + * // a single namespace value (like `x:id`) + * NAMESPACE_MARKER, namespaceUri1, name1, value1, + * + * // another single namespace value (like `x:name`) + * NAMESPACE_MARKER, namespaceUri2, name2, value2, + * + * // a series of CSS classes that will be applied to the element (no spaces) + * CLASSES_MARKER, class1, class2, class3, + * + * // a series of CSS styles (property + value) that will be applied to the element + * STYLES_MARKER, prop1, value1, prop2, value2 + * ] + * + * All non-class and non-style attributes must be defined at the start of the list + * first before all class and style values are set. When there is a change in value + * type (like when classes and styles are introduced) a marker must be used to separate + * the entries. The marker values themselves are set via entries found in the + * [AttributeMarker] enum. + */ + hostAttrs?: TAttributes; + /** * Function to create instances of content queries associated with a given directive. */ diff --git a/packages/core/src/render3/errors.ts b/packages/core/src/render3/errors.ts index d92cb4ebc2..d901bc4089 100644 --- a/packages/core/src/render3/errors.ts +++ b/packages/core/src/render3/errors.ts @@ -55,6 +55,8 @@ export function throwErrorIfNoChangesMode( } // TODO: include debug context, see `viewDebugError` function in // `packages/core/src/view/errors.ts` for reference. + // tslint:disable-next-line + debugger; // Left intentionally for better debugger experience. throw new Error(msg); } diff --git a/packages/core/src/render3/features/inherit_definition_feature.ts b/packages/core/src/render3/features/inherit_definition_feature.ts index 13c21b41e4..ba965d4daa 100644 --- a/packages/core/src/render3/features/inherit_definition_feature.ts +++ b/packages/core/src/render3/features/inherit_definition_feature.ts @@ -6,17 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import {Type} from '../../interface/type'; +import {Type, Writable} from '../../interface/type'; +import {assertEqual} from '../../util/assert'; import {fillProperties} from '../../util/property'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {ComponentDef, ContentQueriesFunction, DirectiveDef, DirectiveDefFeature, HostBindingsFunction, RenderFlags, ViewQueriesFunction} from '../interfaces/definition'; +import {AttributeMarker, TAttributes} from '../interfaces/node'; import {isComponentDef} from '../interfaces/type_checks'; +import {mergeHostAttrs} from '../util/attrs_utils'; export function getSuperType(type: Type): Type& {ɵcmp?: ComponentDef, ɵdir?: DirectiveDef} { return Object.getPrototypeOf(type.prototype).constructor; } +type WritableDef = Writable|ComponentDef>; + /** * Merges the definition from a super class to a sub class. * @param definition The definition that is a SubClass of another directive of component @@ -26,6 +31,7 @@ export function getSuperType(type: Type): Type& export function ɵɵInheritDefinitionFeature(definition: DirectiveDef| ComponentDef): void { let superType = getSuperType(definition.type); let shouldInheritFields = true; + const inheritanceChain: WritableDef[] = [definition]; while (superType) { let superDef: DirectiveDef|ComponentDef|undefined = undefined; @@ -42,9 +48,10 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef| Comp if (superDef) { if (shouldInheritFields) { + inheritanceChain.push(superDef); // Some fields in the definition may be empty, if there were no values to put in them that // would've justified object creation. Unwrap them if necessary. - const writeableDef = definition as any; + const writeableDef = definition as WritableDef; writeableDef.inputs = maybeUnwrapEmpty(definition.inputs); writeableDef.declaredInputs = maybeUnwrapEmpty(definition.declaredInputs); writeableDef.outputs = maybeUnwrapEmpty(definition.outputs); @@ -66,14 +73,14 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef| Comp // Inherit hooks // Assume super class inheritance feature has already run. - definition.afterContentChecked = - definition.afterContentChecked || superDef.afterContentChecked; - definition.afterContentInit = definition.afterContentInit || superDef.afterContentInit; - definition.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked; - definition.afterViewInit = definition.afterViewInit || superDef.afterViewInit; - definition.doCheck = definition.doCheck || superDef.doCheck; - definition.onDestroy = definition.onDestroy || superDef.onDestroy; - definition.onInit = definition.onInit || superDef.onInit; + writeableDef.afterContentChecked = + writeableDef.afterContentChecked || superDef.afterContentChecked; + writeableDef.afterContentInit = definition.afterContentInit || superDef.afterContentInit; + writeableDef.afterViewChecked = definition.afterViewChecked || superDef.afterViewChecked; + writeableDef.afterViewInit = definition.afterViewInit || superDef.afterViewInit; + writeableDef.doCheck = definition.doCheck || superDef.doCheck; + writeableDef.onDestroy = definition.onDestroy || superDef.onDestroy; + writeableDef.onInit = definition.onInit || superDef.onInit; } // Run parent features @@ -100,6 +107,28 @@ export function ɵɵInheritDefinitionFeature(definition: DirectiveDef| Comp superType = Object.getPrototypeOf(superType); } + mergeHostAttrsAcrossInheritance(inheritanceChain); +} + +/** + * Merge the `hostAttrs` and `hostVars` from the inherited parent to the base class. + * + * @param inheritanceChain A list of `WritableDefs` starting at the top most type and listing + * sub-types in order. For each type take the `hostAttrs` and `hostVars` and merge it with the child + * type. + */ +function mergeHostAttrsAcrossInheritance(inheritanceChain: WritableDef[]) { + let hostVars: number = 0; + let hostAttrs: TAttributes|null = null; + // We process the inheritance order from the base to the leaves here. + for (let i = inheritanceChain.length - 1; i >= 0; i--) { + const def = inheritanceChain[i]; + // For each `hostVars`, we need to add the superclass amount. + def.hostVars = (hostVars += def.hostVars); + // for each `hostAttrs` we need to merge it with superclass. + def.hostAttrs = + mergeHostAttrs(def.hostAttrs, hostAttrs = mergeHostAttrs(hostAttrs, def.hostAttrs)); + } } function maybeUnwrapEmpty(value: T[]): T[]; @@ -114,8 +143,7 @@ function maybeUnwrapEmpty(value: any): any { } } -function inheritViewQuery( - definition: DirectiveDef| ComponentDef, superViewQuery: ViewQueriesFunction) { +function inheritViewQuery(definition: WritableDef, superViewQuery: ViewQueriesFunction) { const prevViewQuery = definition.viewQuery; if (prevViewQuery) { definition.viewQuery = (rf, ctx) => { @@ -128,8 +156,7 @@ function inheritViewQuery( } function inheritContentQueries( - definition: DirectiveDef| ComponentDef, - superContentQueries: ContentQueriesFunction) { + definition: WritableDef, superContentQueries: ContentQueriesFunction) { const prevContentQueries = definition.contentQueries; if (prevContentQueries) { definition.contentQueries = (rf, ctx, directiveIndex) => { @@ -142,8 +169,7 @@ function inheritContentQueries( } function inheritHostBindings( - definition: DirectiveDef| ComponentDef, - superHostBindings: HostBindingsFunction) { + definition: WritableDef, superHostBindings: HostBindingsFunction) { const prevHostBindings = definition.hostBindings; if (prevHostBindings) { definition.hostBindings = (rf: RenderFlags, ctx: any, elementIndex: number) => { diff --git a/packages/core/src/render3/features/ng_onchanges_feature.ts b/packages/core/src/render3/features/ng_onchanges_feature.ts index a815609e27..efc69de2ff 100644 --- a/packages/core/src/render3/features/ng_onchanges_feature.ts +++ b/packages/core/src/render3/features/ng_onchanges_feature.ts @@ -51,7 +51,7 @@ export function ɵɵNgOnChangesFeature(): DirectiveDefFeature { function NgOnChangesFeatureImpl(definition: DirectiveDef): void { if (definition.type.prototype.ngOnChanges) { definition.setInput = ngOnChangesSetInput; - definition.onChanges = wrapOnChanges(); + (definition as{onChanges: Function}).onChanges = wrapOnChanges(); } } diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 7baacc216e..e896ceee85 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -24,8 +24,6 @@ export { store, tick, - ɵɵallocHostVars, - ɵɵattribute, ɵɵattributeInterpolate1, ɵɵattributeInterpolate2, @@ -65,7 +63,6 @@ export { ɵɵelementContainerStart, ɵɵelementEnd, - ɵɵelementHostAttrs, ɵɵelementStart, ɵɵembeddedViewEnd, diff --git a/packages/core/src/render3/instructions/alloc_host_vars.ts b/packages/core/src/render3/instructions/alloc_host_vars.ts index 1e4c22ccff..4326ec3d47 100644 --- a/packages/core/src/render3/instructions/alloc_host_vars.ts +++ b/packages/core/src/render3/instructions/alloc_host_vars.ts @@ -12,6 +12,9 @@ import {LView, TVIEW, TView} from '../interfaces/view'; import {getCurrentDirectiveDef, getLView} from '../state'; import {NO_CHANGE} from '../tokens'; +// TODO(misko-next): delete alloc_host_vars.ts file. +// TODO(misko-next): delete `ɵɵallocHostVars` + /** * Allocates the necessary amount of slots for host vars. * diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 245769cdd5..c15f4fbae5 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -219,6 +219,7 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) { // errors... if (tNode.type === TNodeType.Element) { const native = getNativeByTNode(tNode, lView) as RElement; + // TODO(misko-next): setup attributes need to be moved out of `ɵɵelementHostAttrs` const lastAttrIndex = setUpAttributes(lView[RENDERER], native, attrs); if (tView.firstCreatePass) { const stylingNeedsToBeRendered = registerInitialStylingOnTNode(tNode, attrs, lastAttrIndex); @@ -233,6 +234,7 @@ export function ɵɵelementHostAttrs(attrs: TAttributes) { // attribute values to the element. if (stylingNeedsToBeRendered) { const renderer = lView[RENDERER]; + // TODO(misko-next): Styling initialization should move out of `ɵɵelementHostAttrs` renderInitialStyling(renderer, native, tNode, true); } } diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index e86d9138d4..9aaaf723e6 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -41,6 +41,8 @@ import {getLViewParent} from '../util/view_traversal_utils'; import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, unwrapRNode, viewAttachedToChangeDetector} from '../util/view_utils'; import {selectIndexInternal} from './advance'; +import {ɵɵallocHostVars} from './alloc_host_vars'; +import {ɵɵelementHostAttrs} from './element'; import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug'; @@ -1120,7 +1122,8 @@ export function resolveDirectives( saveNameToExportMap(tView.data !.length - 1, def, exportsMap); if (def.contentQueries !== null) tNode.flags |= TNodeFlags.hasContentQuery; - if (def.hostBindings !== null) tNode.flags |= TNodeFlags.hasHostBindings; + if (def.hostBindings !== null || def.hostAttrs !== null || def.hostVars !== 0) + tNode.flags |= TNodeFlags.hasHostBindings; // Only push a node index into the preOrderHooks array if this is the first // pre-order hook found on this node. @@ -1194,7 +1197,7 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod for (let i = start; i < end; i++) { const def = tView.data[i] as DirectiveDef; const directive = viewData[i]; - if (def.hostBindings) { + if (def.hostBindings !== null || def.hostVars !== 0 || def.hostAttrs !== null) { // It is important that this be called first before the actual instructions // are run because this way the first directive ID value is not zero. incrementActiveDirectiveId(); @@ -1214,7 +1217,18 @@ export function invokeHostBindingsInCreationMode( const previousExpandoLength = expando.length; setCurrentDirectiveDef(def); const elementIndex = tNode.index - HEADER_OFFSET; - def.hostBindings !(RenderFlags.Create, directive, elementIndex); + // TODO(misko-next): This is a temporary work around for the fact that we moved the information + // from instruction to declaration. The workaround is to just call the instruction as if it was + // part of the `hostAttrs`. + if (def.hostVars !== 0) { + ɵɵallocHostVars(def.hostVars); + } + if (def.hostAttrs !== null) { + ɵɵelementHostAttrs(def.hostAttrs); + } + if (def.hostBindings !== null) { + def.hostBindings !(RenderFlags.Create, directive, elementIndex); + } setCurrentDirectiveDef(null); // `hostBindings` function may or may not contain `allocHostVars` call // (e.g. it may not if it only contains host listeners), so we need to check whether @@ -1346,7 +1360,8 @@ export function initNodeFlags(tNode: TNode, index: number, numberOfDirectives: n function baseResolveDirective(tView: TView, viewData: LView, def: DirectiveDef) { tView.data.push(def); - const directiveFactory = def.factory || (def.factory = getFactoryDef(def.type, true)); + const directiveFactory = + def.factory || ((def as{factory: Function}).factory = getFactoryDef(def.type, true)); const nodeInjectorFactory = new NodeInjectorFactory(directiveFactory, isComponentDef(def), null); tView.blueprint.push(nodeInjectorFactory); viewData.push(nodeInjectorFactory); diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index 6b49420f87..c7105655d5 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -10,7 +10,7 @@ import {SchemaMetadata, ViewEncapsulation} from '../../core'; import {ProcessProvidersFunction} from '../../di/interface/provider'; import {Type} from '../../interface/type'; -import {TConstants} from './node'; +import {TAttributes, TConstants} from './node'; import {CssSelectorList} from './projection'; import {TView} from './view'; @@ -146,10 +146,50 @@ export interface DirectiveDef { /** * Refreshes host bindings on the associated directive. */ - hostBindings: HostBindingsFunction|null; + readonly hostBindings: HostBindingsFunction|null; + + /** + * The number of bindings in this directive `hostBindings` (including pure fn bindings). + * + * Used to calculate the length of the component's LView array, so we + * can pre-fill the array and set the host binding start index. + */ + readonly hostVars: number; + + /** + * Assign static attribute values to a host element. + * + * This property will assign static attribute values as well as class and style + * values to a host element. Since attribute values can consist of different types of values, the + * `hostAttrs` array must include the values in the following format: + * + * attrs = [ + * // static attributes (like `title`, `name`, `id`...) + * attr1, value1, attr2, value, + * + * // a single namespace value (like `x:id`) + * NAMESPACE_MARKER, namespaceUri1, name1, value1, + * + * // another single namespace value (like `x:name`) + * NAMESPACE_MARKER, namespaceUri2, name2, value2, + * + * // a series of CSS classes that will be applied to the element (no spaces) + * CLASSES_MARKER, class1, class2, class3, + * + * // a series of CSS styles (property + value) that will be applied to the element + * STYLES_MARKER, prop1, value1, prop2, value2 + * ] + * + * All non-class and non-style attributes must be defined at the start of the list + * first before all class and style values are set. When there is a change in value + * type (like when classes and styles are introduced) a marker must be used to separate + * the entries. The marker values themselves are set via entries found in the + * [AttributeMarker] enum. + */ + readonly hostAttrs: TAttributes|null; /** Token representing the directive. Used by DI. */ - type: Type; + readonly type: Type; /** Function that resolves providers and publishes them into the DI system. */ providersResolver: @@ -168,17 +208,17 @@ export interface DirectiveDef { * Factory function used to create a new directive instance. Will be null initially. * Populated when the factory is first requested by directive instantiation logic. */ - factory: FactoryFn|null; + readonly factory: FactoryFn|null; /* The following are lifecycle hooks for this component */ - onChanges: (() => void)|null; - onInit: (() => void)|null; - doCheck: (() => void)|null; - afterContentInit: (() => void)|null; - afterContentChecked: (() => void)|null; - afterViewInit: (() => void)|null; - afterViewChecked: (() => void)|null; - onDestroy: (() => void)|null; + readonly onChanges: (() => void)|null; + readonly onInit: (() => void)|null; + readonly doCheck: (() => void)|null; + readonly afterContentInit: (() => void)|null; + readonly afterContentChecked: (() => void)|null; + readonly afterViewInit: (() => void)|null; + readonly afterViewChecked: (() => void)|null; + readonly onDestroy: (() => void)|null; /** * The features applied to this directive diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 4151d324b6..1cb13c84a6 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -201,6 +201,15 @@ export const enum TNodeProviderIndexes { * items are not regular attributes and the processing should be adapted accordingly. */ export const enum AttributeMarker { + /** + * An implicit marker which indicates that the value in the array are of `attributeKey`, + * `attributeValue` format. + * + * NOTE: This is implicit as it is the type when no marker is present in array. We indicate that + * it should not be present at runtime by the negative number. + */ + ImplicitAttributes = -1, + /** * Marker indicates that the following 3 values in the attributes array are: * namespaceUri, attributeName, attributeValue diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 5e4612edb2..444869543a 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -58,7 +58,6 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵnamespaceSVG': r3.ɵɵnamespaceSVG, 'ɵɵenableBindings': r3.ɵɵenableBindings, 'ɵɵdisableBindings': r3.ɵɵdisableBindings, - 'ɵɵallocHostVars': r3.ɵɵallocHostVars, 'ɵɵelementStart': r3.ɵɵelementStart, 'ɵɵelementEnd': r3.ɵɵelementEnd, 'ɵɵelement': r3.ɵɵelement, @@ -107,7 +106,6 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵloadQuery': r3.ɵɵloadQuery, 'ɵɵcontentQuery': r3.ɵɵcontentQuery, 'ɵɵreference': r3.ɵɵreference, - 'ɵɵelementHostAttrs': r3.ɵɵelementHostAttrs, 'ɵɵclassMap': r3.ɵɵclassMap, 'ɵɵclassMapInterpolate1': r3.ɵɵclassMapInterpolate1, 'ɵɵclassMapInterpolate2': r3.ɵɵclassMapInterpolate2, diff --git a/packages/core/src/render3/util/attrs_utils.ts b/packages/core/src/render3/util/attrs_utils.ts index 0b67e9cd88..3327e3643a 100644 --- a/packages/core/src/render3/util/attrs_utils.ts +++ b/packages/core/src/render3/util/attrs_utils.ts @@ -5,12 +5,14 @@ * 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 {assertEqual} from '../../util/assert'; import {CharCode} from '../../util/char_code'; import {AttributeMarker, TAttributes} from '../interfaces/node'; import {CssSelector} from '../interfaces/projection'; import {ProceduralRenderer3, RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer'; + /** * Assigns all attribute values to the provided element via the inferred renderer. * @@ -106,3 +108,114 @@ export function isAnimationProp(name: string): boolean { // charCodeAt doesn't allocate memory to return a substring. return name.charCodeAt(0) === CharCode.AT_SIGN; } + +/** + * Merges `src` `TAttributes` into `dst` `TAttributes` removing any duplicates in the process. + * + * This merge function keeps the order of attrs same. + * + * @param dst Location of where the merged `TAttributes` should end up. + * @param src `TAttributes` which should be appended to `dst` + */ +export function mergeHostAttrs(dst: TAttributes | null, src: TAttributes | null): TAttributes|null { + if (src === null || src.length === 0) { + // do nothing + } else if (dst === null || dst.length === 0) { + // We have source, but dst is empty, just make a copy. + dst = src.slice(); + } else { + let srcMarker: AttributeMarker = AttributeMarker.ImplicitAttributes; + for (let i = 0; i < src.length; i++) { + const item = src[i]; + if (typeof item === 'number') { + srcMarker = item; + } else { + if (srcMarker === AttributeMarker.NamespaceURI) { + // Case where we need to consume `key1`, `key2`, `value` items. + } else if ( + srcMarker === AttributeMarker.ImplicitAttributes || + srcMarker === AttributeMarker.Styles) { + // Case where we have to consume `key1` and `value` only. + mergeHostAttribute(dst, srcMarker, item as string, null, src[++i] as string); + } else { + // Case where we have to consume `key1` only. + mergeHostAttribute(dst, srcMarker, item as string, null, null); + } + } + } + } + return dst; +} + +/** + * Append `key`/`value` to existing `TAttributes` taking region marker and duplicates into account. + * + * @param dst `TAttributes` to append to. + * @param marker Region where the `key`/`value` should be added. + * @param key1 Key to add to `TAttributes` + * @param key2 Key to add to `TAttributes` (in case of `AttributeMarker.NamespaceURI`) + * @param value Value to add or to overwrite to `TAttributes` Only used if `marker` is not Class. + */ +export function mergeHostAttribute( + dst: TAttributes, marker: AttributeMarker, key1: string, key2: string | null, + value: string | null): void { + let i = 0; + // Assume that new markers will be inserted at the end. + let markerInsertPosition = dst.length; + // scan until correct type. + if (marker === AttributeMarker.ImplicitAttributes) { + markerInsertPosition = -1; + } else { + while (i < dst.length) { + const dstValue = dst[i++]; + if (typeof dstValue === 'number') { + if (dstValue === marker) { + markerInsertPosition = -1; + break; + } else if (dstValue > marker) { + // We need to save this as we want the markers to be inserted in specific order. + markerInsertPosition = i - 1; + break; + } + } + } + } + + // search until you find place of insertion + while (i < dst.length) { + const item = dst[i]; + if (typeof item === 'number') { + // since `i` started as the index after the marker, we did not find it if we are at the next + // marker + break; + } else if (item === key1) { + // We already have same token + if (key2 === null) { + if (value !== null) { + dst[i + 1] = value; + } + return; + } else if (key2 === dst[i + 1]) { + dst[i + 2] = value !; + return; + } + } + // Increment counter. + i++; + if (key2 !== null) i++; + if (value !== null) i++; + } + + // insert at location. + if (markerInsertPosition !== -1) { + dst.splice(markerInsertPosition, 0, marker); + i = markerInsertPosition + 1; + } + dst.splice(i++, 0, key1); + if (key2 !== null) { + dst.splice(i++, 0, key2); + } + if (value !== null) { + dst.splice(i++, 0, value); + } +} \ No newline at end of file diff --git a/packages/core/test/acceptance/inherit_definition_feature_spec.ts b/packages/core/test/acceptance/inherit_definition_feature_spec.ts index 85d953dc74..75492c979f 100644 --- a/packages/core/test/acceptance/inherit_definition_feature_spec.ts +++ b/packages/core/test/acceptance/inherit_definition_feature_spec.ts @@ -7,6 +7,8 @@ */ import {Component, ContentChildren, Directive, EventEmitter, HostBinding, HostListener, Input, OnChanges, Output, QueryList, ViewChildren} from '@angular/core'; +import {ivyEnabled} from '@angular/core/src/ivy_switch'; +import {getDirectiveDef} from '@angular/core/src/render3/definition'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {onlyInIvy} from '@angular/private/testing'; @@ -42,6 +44,86 @@ describe('inheritance', () => { }).toThrowError('Directives cannot inherit Components'); }); + describe('multiple children', () => { + it('should ensure that multiple child classes don\'t cause multiple parent execution', () => { + // Assume this inheritance: + // Base + // | + // Super + // / \ + // Sub1 Sub2 + // + // In the above case: + // 1. Sub1 as will walk the inheritance Sub1, Super, Base + // 2. Sub2 as will walk the inheritance Sub2, Super, Base + // + // Notice that Super, Base will get walked twice. Because inheritance works by wrapping parent + // hostBindings function in a delegate which calls the hostBindings of the directive as well + // as super, we need to ensure that we don't double wrap the hostBindings function. Doing so + // would result in calling the hostBindings multiple times (unnecessarily). This would be + // especially an issue if we have a lot of sub-classes (as is common in component libraries) + const log: string[] = []; + + @Directive({selector: '[superDir]'}) + class BaseDirective { + @HostBinding('style.background-color') + get backgroundColor() { + log.push('Base.backgroundColor'); + return 'white'; + } + } + + @Directive({selector: '[superDir]'}) + class SuperDirective extends BaseDirective { + @HostBinding('style.color') + get color() { + log.push('Super.color'); + return 'blue'; + } + } + + @Directive({selector: '[subDir1]'}) + class Sub1Directive extends SuperDirective { + @HostBinding('style.height') + get height() { + log.push('Sub1.height'); + return '200px'; + } + } + + @Directive({selector: '[subDir2]'}) + class Sub2Directive extends SuperDirective { + @HostBinding('style.width') + get width() { + log.push('Sub2.width'); + return '100px'; + } + } + + @Component({template: `
`}) + class App { + } + + TestBed.configureTestingModule({ + declarations: [App, Sub1Directive, Sub2Directive, SuperDirective], + }); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(false); // Don't check for no changes (so that assertion does not need + // to worry about it.) + + expect(log).toEqual([ + 'Base.backgroundColor', 'Super.color', 'Sub1.height', // + 'Base.backgroundColor', 'Super.color', 'Sub2.width', // + ]); + if (ivyEnabled) { + expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(1); + expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(2); + expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(3); + expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(3); + } + }); + }); + describe('ngOnChanges', () => { it('should be inherited when super is a directive', () => { const log: string[] = []; diff --git a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json index 98675d41e9..9b04a6443b 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -314,6 +314,9 @@ { "name": "getContainerRenderParent" }, + { + "name": "getCurrentDirectiveDef" + }, { "name": "getDirectiveDef" }, @@ -404,6 +407,9 @@ { "name": "getStylingMapArray" }, + { + "name": "getTNode" + }, { "name": "hasActiveElementFlag" }, @@ -545,6 +551,12 @@ { "name": "objectToClassName" }, + { + "name": "prefillHostVars" + }, + { + "name": "queueHostBindingForCheck" + }, { "name": "refreshChildComponents" }, @@ -695,6 +707,9 @@ { "name": "writeStylingValueDirectly" }, + { + "name": "ɵɵallocHostVars" + }, { "name": "ɵɵdefineComponent" }, @@ -710,6 +725,9 @@ { "name": "ɵɵelementEnd" }, + { + "name": "ɵɵelementHostAttrs" + }, { "name": "ɵɵelementStart" }, diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 7bfaa2eccd..bbedae4cb2 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -104,6 +104,9 @@ { "name": "RENDERER_FACTORY" }, + { + "name": "RendererStyleFlags3" + }, { "name": "SANITIZER" }, @@ -137,12 +140,18 @@ { "name": "_renderCompCount" }, + { + "name": "addItemToStylingMap" + }, { "name": "addToViewTree" }, { "name": "allocLFrame" }, + { + "name": "allocStylingMapArray" + }, { "name": "appendChild" }, @@ -161,6 +170,9 @@ { "name": "callHooks" }, + { + "name": "concatString" + }, { "name": "createLFrame" }, @@ -227,6 +239,9 @@ { "name": "extractPipeDef" }, + { + "name": "forceStylesAsString" + }, { "name": "generateExpandoInstructionBlock" }, @@ -248,6 +263,9 @@ { "name": "getContainerRenderParent" }, + { + "name": "getCurrentDirectiveDef" + }, { "name": "getDirectiveDef" }, @@ -260,6 +278,9 @@ { "name": "getFirstNativeNode" }, + { + "name": "getInitialStylingValue" + }, { "name": "getInjectorIndex" }, @@ -275,6 +296,12 @@ { "name": "getLViewParent" }, + { + "name": "getMapProp" + }, + { + "name": "getMapValue" + }, { "name": "getNativeAnchorNode" }, @@ -317,12 +344,21 @@ { "name": "getSelectedIndex" }, + { + "name": "getStylingMapArray" + }, + { + "name": "getTNode" + }, { "name": "hasActiveElementFlag" }, { "name": "hasParentInjector" }, + { + "name": "hyphenate" + }, { "name": "includeViewProviders" }, @@ -350,6 +386,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isAnimationProp" + }, { "name": "isComponentDef" }, @@ -365,6 +404,12 @@ { "name": "isProceduralRenderer" }, + { + "name": "isStylingContext" + }, + { + "name": "isStylingValueDefined" + }, { "name": "leaveDI" }, @@ -398,6 +443,15 @@ { "name": "noSideEffects" }, + { + "name": "objectToClassName" + }, + { + "name": "prefillHostVars" + }, + { + "name": "queueHostBindingForCheck" + }, { "name": "refreshChildComponents" }, @@ -416,6 +470,9 @@ { "name": "refreshView" }, + { + "name": "registerInitialStylingOnTNode" + }, { "name": "registerPreOrderHooks" }, @@ -428,9 +485,15 @@ { "name": "renderComponent" }, + { + "name": "renderInitialStyling" + }, { "name": "renderStringify" }, + { + "name": "renderStylingMap" + }, { "name": "renderView" }, @@ -449,6 +512,12 @@ { "name": "setBindingRoot" }, + { + "name": "setClass" + }, + { + "name": "setClassName" + }, { "name": "setCurrentDirectiveDef" }, @@ -464,27 +533,54 @@ { "name": "setInjectImplementation" }, + { + "name": "setMapValue" + }, { "name": "setPreviousOrParentTNode" }, { "name": "setSelectedIndex" }, + { + "name": "setStyle" + }, + { + "name": "setStyleAttr" + }, + { + "name": "setUpAttributes" + }, { "name": "stringifyForError" }, + { + "name": "stylingMapToString" + }, { "name": "syncViewWithBlueprint" }, { "name": "unwrapRNode" }, + { + "name": "updateRawValueOnContext" + }, { "name": "viewAttachedToChangeDetector" }, + { + "name": "writeStylingValueDirectly" + }, + { + "name": "ɵɵallocHostVars" + }, { "name": "ɵɵdefineComponent" }, + { + "name": "ɵɵelementHostAttrs" + }, { "name": "ɵɵtext" } diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 316535c207..504dedae40 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -647,6 +647,9 @@ { "name": "getContextLView" }, + { + "name": "getCurrentDirectiveDef" + }, { "name": "getCurrentStyleSanitizer" }, @@ -1079,6 +1082,12 @@ { "name": "patchHostStylingFlag" }, + { + "name": "prefillHostVars" + }, + { + "name": "queueHostBindingForCheck" + }, { "name": "readPatchedData" }, @@ -1358,6 +1367,9 @@ { "name": "ɵɵadvance" }, + { + "name": "ɵɵallocHostVars" + }, { "name": "ɵɵclassProp" }, @@ -1376,6 +1388,9 @@ { "name": "ɵɵelementEnd" }, + { + "name": "ɵɵelementHostAttrs" + }, { "name": "ɵɵelementStart" }, diff --git a/packages/core/test/render3/util/attr_util_spec.ts b/packages/core/test/render3/util/attr_util_spec.ts new file mode 100644 index 0000000000..a3fb25542f --- /dev/null +++ b/packages/core/test/render3/util/attr_util_spec.ts @@ -0,0 +1,119 @@ +/** + * @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 {AttributeMarker} from '@angular/core/src/render3'; +import {TAttributes} from '@angular/core/src/render3/interfaces/node'; +import {mergeHostAttribute, mergeHostAttrs} from '@angular/core/src/render3/util/attrs_utils'; +import {describe} from '@angular/core/testing/src/testing_internal'; + +describe('attr_util', () => { + describe('mergeHostAttribute', () => { + it('should add new attributes', () => { + const attrs: TAttributes = []; + mergeHostAttribute(attrs, -1, 'Key', null, 'value'); + expect(attrs).toEqual(['Key', 'value']); + + mergeHostAttribute(attrs, -1, 'A', null, 'a'); + expect(attrs).toEqual(['Key', 'value', 'A', 'a']); + + mergeHostAttribute(attrs, -1, 'X', null, 'x'); + expect(attrs).toEqual(['Key', 'value', 'A', 'a', 'X', 'x']); + + mergeHostAttribute(attrs, -1, 'Key', null, 'new'); + expect(attrs).toEqual(['Key', 'new', 'A', 'a', 'X', 'x']); + }); + + it('should add new classes', () => { + const attrs: TAttributes = []; + mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS', null, null); + expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS']); + + mergeHostAttribute(attrs, AttributeMarker.Classes, 'A', null, null); + expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS', 'A']); + + mergeHostAttribute(attrs, AttributeMarker.Classes, 'X', null, null); + expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS', 'A', 'X']); + + mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS', null, null); + expect(attrs).toEqual([AttributeMarker.Classes, 'CLASS', 'A', 'X']); + }); + + it('should add new styles', () => { + const attrs: TAttributes = []; + mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style', null, 'v1'); + expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'v1']); + + mergeHostAttribute(attrs, AttributeMarker.Styles, 'A', null, 'v2'); + expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'v1', 'A', 'v2']); + + mergeHostAttribute(attrs, AttributeMarker.Styles, 'X', null, 'v3'); + expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'v1', 'A', 'v2', 'X', 'v3']); + + mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style', null, 'new'); + expect(attrs).toEqual([AttributeMarker.Styles, 'Style', 'new', 'A', 'v2', 'X', 'v3']); + }); + + it('should keep different types together', () => { + const attrs: TAttributes = []; + mergeHostAttribute(attrs, -1, 'Key', null, 'value'); + expect(attrs).toEqual(['Key', 'value']); + + mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS', null, null); + expect(attrs).toEqual(['Key', 'value', AttributeMarker.Classes, 'CLASS']); + + mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style', null, 'v1'); + expect(attrs).toEqual([ + 'Key', 'value', AttributeMarker.Classes, 'CLASS', AttributeMarker.Styles, 'Style', 'v1' + ]); + + mergeHostAttribute(attrs, -1, 'Key2', null, 'value2'); + expect(attrs).toEqual([ + 'Key', 'value', 'Key2', 'value2', AttributeMarker.Classes, 'CLASS', AttributeMarker.Styles, + 'Style', 'v1' + ]); + + mergeHostAttribute(attrs, AttributeMarker.Classes, 'CLASS2', null, null); + expect(attrs).toEqual([ + 'Key', 'value', 'Key2', 'value2', AttributeMarker.Classes, 'CLASS', 'CLASS2', + AttributeMarker.Styles, 'Style', 'v1' + ]); + + mergeHostAttribute(attrs, AttributeMarker.Styles, 'Style2', null, 'v2'); + expect(attrs).toEqual([ + 'Key', 'value', 'Key2', 'value2', AttributeMarker.Classes, 'CLASS', 'CLASS2', + AttributeMarker.Styles, 'Style', 'v1', 'Style2', 'v2' + ]); + + mergeHostAttribute(attrs, AttributeMarker.NamespaceURI, 'uri', 'key', 'value'); + expect(attrs).toEqual([ + 'Key', 'value', 'Key2', 'value2', AttributeMarker.NamespaceURI, 'uri', 'key', 'value', + AttributeMarker.Classes, 'CLASS', 'CLASS2', AttributeMarker.Styles, 'Style', 'v1', 'Style2', + 'v2' + ]); + mergeHostAttribute(attrs, AttributeMarker.NamespaceURI, 'uri', 'key', 'new value'); + expect(attrs).toEqual([ + 'Key', 'value', 'Key2', 'value2', AttributeMarker.NamespaceURI, 'uri', 'key', 'new value', + AttributeMarker.Classes, 'CLASS', 'CLASS2', AttributeMarker.Styles, 'Style', 'v1', 'Style2', + 'v2' + ]); + }); + }); + + describe('mergeHostAttrs', () => { + it('should ignore nulls/empty', () => { + expect(mergeHostAttrs(null, null)).toEqual(null); + expect(mergeHostAttrs([], null)).toEqual([]); + expect(mergeHostAttrs(null, [])).toEqual(null); + }); + + it('should copy if dst is null', () => { + expect(mergeHostAttrs(null, ['K', 'v'])).toEqual(['K', 'v']); + expect(mergeHostAttrs(['K', '', 'X', 'x'], ['K', 'v'])).toEqual(['K', 'v', 'X', 'x']); + }); + }); +}); \ No newline at end of file diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index e2da2d0137..efeea3b1f1 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -682,8 +682,6 @@ export interface OutputDecorator { export declare function ɵɵadvance(delta: number): void; -export declare function ɵɵallocHostVars(count: number): void; - export declare function ɵɵattribute(name: string, value: any, sanitizer?: SanitizerFn | null, namespace?: string): typeof ɵɵattribute; export declare function ɵɵattributeInterpolate1(attrName: string, prefix: string, v0: any, suffix: string, sanitizer?: SanitizerFn, namespace?: string): typeof ɵɵattributeInterpolate1; @@ -760,6 +758,8 @@ export declare function ɵɵdefineComponent(componentDefinition: { [P in keyof T]?: string; }; hostBindings?: HostBindingsFunction; + hostVars?: number; + hostAttrs?: TAttributes; contentQueries?: ContentQueriesFunction; exportAs?: string[]; template: ComponentTemplate; @@ -785,6 +785,8 @@ export declare const ɵɵdefineDirective: (directiveDefinition: { outputs?: { [P_1 in keyof T]?: string | undefined; } | undefined; features?: DirectiveDefFeature[] | undefined; hostBindings?: HostBindingsFunction | undefined; + hostVars?: number | undefined; + hostAttrs?: (string | (string | SelectorFlags)[] | AttributeMarker)[] | undefined; contentQueries?: ContentQueriesFunction | undefined; viewQuery?: ViewQueriesFunction | null | undefined; exportAs?: string[] | undefined; @@ -839,8 +841,6 @@ export declare function ɵɵelementContainerStart(index: number, attrsIndex?: nu export declare function ɵɵelementEnd(): void; -export declare function ɵɵelementHostAttrs(attrs: TAttributes): void; - export declare function ɵɵelementStart(index: number, name: string, attrsIndex?: number | null, localRefsIndex?: number): void; export declare function ɵɵembeddedViewEnd(): void;