From 5aabe93abe28a485a7fc20c72e1bbe201511cec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1ko=20Hevery?= Date: Tue, 17 Dec 2019 15:40:37 -0800 Subject: [PATCH] refactor(ivy): Switch styling to new reconcile algorithm (#34616) NOTE: This change must be reverted with previous deletes so that it code remains in build-able state. This change deletes old styling code and replaces it with a simplified styling algorithm. The mental model for the new algorithm is: - Create a linked list of styling bindings in the order of priority. All styling bindings ere executed in compiled order and than a linked list of bindings is created in priority order. - Flush the style bindings at the end of `advance()` instruction. This implies that there are two flush events. One at the end of template `advance` instruction in the template. Second one at the end of `hostBindings` `advance` instruction when processing host bindings (if any). - Each binding instructions effectively updates the string to represent the string at that location. Because most of the bindings are additive, this is a cheap strategy in most cases. In rare cases the strategy requires removing tokens from the styling up to this point. (We expect that to be rare case)S Because, the bindings are presorted in the order of priority, it is safe to resume the processing of the concatenated string from the last change binding. PR Close #34616 --- aio/scripts/_payload-limits.json | 4 +- integration/_payload-limits.json | 10 +- modules/benchmarks/src/styling/ng2/styling.ts | 2 +- .../src/tree/render3_function/index.ts | 7 +- packages/common/src/directives/ng_class.ts | 7 + packages/common/src/directives/ng_style.ts | 6 + packages/common/src/private_export.ts | 6 + .../common/test/directives/ng_class_spec.ts | 26 +- .../r3_view_compiler_binding_spec.ts | 2 +- .../r3_view_compiler_styling_spec.ts | 40 +- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 2 +- .../src/render3/view/styling_builder.ts | 7 +- .../test/render3/style_parser_spec.ts | 5 + packages/core/src/render3/assert.ts | 5 + packages/core/src/render3/bindings.ts | 19 +- packages/core/src/render3/component.ts | 42 +- packages/core/src/render3/component_ref.ts | 21 +- packages/core/src/render3/di.ts | 5 +- .../core/src/render3/instructions/advance.ts | 7 +- .../instructions/class_map_interpolation.ts | 24 +- .../core/src/render3/instructions/element.ts | 49 +- .../render3/instructions/element_container.ts | 4 +- .../src/render3/instructions/lview_debug.ts | 80 +- .../core/src/render3/instructions/property.ts | 19 +- .../core/src/render3/instructions/shared.ts | 73 +- .../instructions/style_prop_interpolation.ts | 23 +- .../core/src/render3/instructions/styling.ts | 317 +++- packages/core/src/render3/interfaces/node.ts | 183 +-- .../core/src/render3/interfaces/styling.ts | 5 + packages/core/src/render3/interfaces/view.ts | 12 +- packages/core/src/render3/state.ts | 117 +- .../core/src/render3/styling/class_differ.ts | 34 +- .../core/src/render3/styling/reconcile.ts | 73 +- .../src/render3/styling/static_styling.ts | 4 +- .../src/render3/styling/style_binding_list.ts | 84 +- .../core/src/render3/styling/style_differ.ts | 11 +- .../src/render3/styling/styling_parser.ts | 44 +- packages/core/src/render3/tokens.ts | 5 +- .../core/src/sanitization/sanitization.ts | 14 +- packages/core/src/util/stringify.ts | 3 +- .../test/acceptance/change_detection_spec.ts | 14 +- .../test/acceptance/discover_utils_spec.ts | 11 +- .../core/test/acceptance/host_binding_spec.ts | 9 +- .../inherit_definition_feature_spec.ts | 8 +- .../core/test/acceptance/integration_spec.ts | 36 +- .../test/acceptance/renderer_factory_spec.ts | 1 - packages/core/test/acceptance/styling_spec.ts | 1324 +++++++++++++---- .../cyclic_import/bundle.golden_symbols.json | 96 +- .../hello_world/bundle.golden_symbols.json | 91 +- .../bundling/todo/bundle.golden_symbols.json | 354 ++--- .../core/test/render3/component_ref_spec.ts | 26 +- .../render3/instructions/lview_debug_spec.ts | 147 ++ .../core/test/render3/instructions_spec.ts | 41 +- .../render3/styling_next/class_differ_spec.ts | 28 +- .../render3/styling_next/reconcile_spec.ts | 39 +- .../styling_next/style_binding_list_spec.ts | 80 +- .../render3/styling_next/style_differ_spec.ts | 47 +- packages/core/testing/src/styling.ts | 84 ++ tools/public_api_guard/common/common.d.ts | 5 + tools/public_api_guard/core/core.d.ts | 10 +- 60 files changed, 2439 insertions(+), 1413 deletions(-) create mode 100644 packages/core/test/render3/instructions/lview_debug_spec.ts create mode 100644 packages/core/testing/src/styling.ts diff --git a/aio/scripts/_payload-limits.json b/aio/scripts/_payload-limits.json index bbbaad4daf..a616f87284 100755 --- a/aio/scripts/_payload-limits.json +++ b/aio/scripts/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 2987, - "main-es2015": 462235, + "main-es2015": 456890, "polyfills-es2015": 52503 } } @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime-es2015": 3097, - "main-es2015": 438671, + "main-es2015": 425216, "polyfills-es2015": 52503 } } diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 63c5ec4713..a66545d810 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -12,7 +12,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 18214, + "main-es2015": 16787, "polyfills-es2015": 36808 } } @@ -30,7 +30,7 @@ "master": { "uncompressed": { "runtime-es2015": 1485, - "main-es2015": 139487, + "main-es2015": 137226, "polyfills-es2015": 37494 } } @@ -39,7 +39,7 @@ "master": { "uncompressed": { "runtime-es2015": 2289, - "main-es2015": 268796, + "main-es2015": 254857, "polyfills-es2015": 36808, "5-es2015": 751 } @@ -49,7 +49,7 @@ "master": { "uncompressed": { "runtime-es2015": 2289, - "main-es2015": 228770, + "main-es2015": 226519, "polyfills-es2015": 36808, "5-es2015": 779 } @@ -60,7 +60,7 @@ "uncompressed": { "bundle": "TODO(i): temporarily increase the payload size limit from 105779 - this is due to a closure issue related to ESM reexports that still needs to be investigated", "bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.", - "bundle": 176433 + "bundle": 175498 } } } diff --git a/modules/benchmarks/src/styling/ng2/styling.ts b/modules/benchmarks/src/styling/ng2/styling.ts index af43821de8..b669654ae7 100644 --- a/modules/benchmarks/src/styling/ng2/styling.ts +++ b/modules/benchmarks/src/styling/ng2/styling.ts @@ -18,7 +18,7 @@ import {BrowserModule} from '@angular/platform-browser'; - + diff --git a/modules/benchmarks/src/tree/render3_function/index.ts b/modules/benchmarks/src/tree/render3_function/index.ts index 4a9f9394c4..32be3be0b8 100644 --- a/modules/benchmarks/src/tree/render3_function/index.ts +++ b/modules/benchmarks/src/tree/render3_function/index.ts @@ -25,7 +25,7 @@ export class TreeFunction { type: TreeFunction, selectors: [['tree']], decls: 5, - vars: 1, + vars: 2, template: function(rf: ɵRenderFlags, ctx: TreeFunction) { // bit of a hack TreeTpl(rf, ctx.data); @@ -34,6 +34,7 @@ export class TreeFunction { }); } +const TreeFunctionCmpDef = TreeFunction.ɵcmp as{decls: number, vars: number}; export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { if (rf & ɵRenderFlags.Create) { ɵɵelementStart(0, 'tree'); @@ -54,7 +55,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { ɵɵcontainerRefreshStart(3); { if (ctx.left != null) { - let rf0 = ɵɵembeddedViewStart(0, 5, 1); + let rf0 = ɵɵembeddedViewStart(0, 5, 2); { TreeTpl(rf0, ctx.left); } ɵɵembeddedViewEnd(); } @@ -63,7 +64,7 @@ export function TreeTpl(rf: ɵRenderFlags, ctx: TreeNode) { ɵɵcontainerRefreshStart(4); { if (ctx.right != null) { - let rf0 = ɵɵembeddedViewStart(0, 5, 1); + let rf0 = ɵɵembeddedViewStart(0, TreeFunctionCmpDef.decls, TreeFunctionCmpDef.vars); { TreeTpl(rf0, ctx.right); } ɵɵembeddedViewEnd(); } diff --git a/packages/common/src/directives/ng_class.ts b/packages/common/src/directives/ng_class.ts index 63ddbe198e..382d871fc1 100644 --- a/packages/common/src/directives/ng_class.ts +++ b/packages/common/src/directives/ng_class.ts @@ -157,4 +157,11 @@ export class NgClass implements DoCheck { }); } } + + // TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs + // We need to export this to make angular/flex-layout happy + // https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9 + setClass(value: string) { this.klass = value; } + setNgClass(value: any) { this.ngClass = value; } + applyChanges() { this.ngDoCheck(); } } diff --git a/packages/common/src/directives/ng_style.ts b/packages/common/src/directives/ng_style.ts index 73dab1a154..52d4dc45d7 100644 --- a/packages/common/src/directives/ng_style.ts +++ b/packages/common/src/directives/ng_style.ts @@ -85,4 +85,10 @@ export class NgStyle implements DoCheck { changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue)); changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue)); } + + // TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs + // We need to export this to make angular/flex-layout happy + // https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9 + setNgStyle(value: any) { this.ngStyle = value; } + applyChanges() { this.ngDoCheck(); } } diff --git a/packages/common/src/private_export.ts b/packages/common/src/private_export.ts index 038833ee04..8e07f8a780 100644 --- a/packages/common/src/private_export.ts +++ b/packages/common/src/private_export.ts @@ -6,5 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ +// TODO(misko): Delete this code after angula/flex-layout stops depending on private APIs +// We need to export this to make angular/flex-layout happy +// https://github.com/angular/flex-layout/blob/ec7b57eb6adf59ecfdfff1de5ccf1ab2f6652ed3/src/lib/extended/class/class.ts#L9 +export {NgClass as ɵNgClassImpl, NgClass as ɵNgClassR2Impl} from './directives/ng_class'; +export {NgStyle as ɵNgStyleR2Impl} from './directives/ng_style'; + export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter'; export {BrowserPlatformLocation as ɵBrowserPlatformLocation} from './location/platform_location'; \ No newline at end of file diff --git a/packages/common/test/directives/ng_class_spec.ts b/packages/common/test/directives/ng_class_spec.ts index d661c87cad..d31771932d 100644 --- a/packages/common/test/directives/ng_class_spec.ts +++ b/packages/common/test/directives/ng_class_spec.ts @@ -196,7 +196,7 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing'; fixture = createTestComponent(`
`); expect(() => fixture !.detectChanges()) .toThrowError( - /NgClass can only toggle CSS classes expressed as strings, got: \[object Object\]/); + /NgClass can only toggle CSS classes expressed as strings, got \[object Object\]/); }); }); @@ -372,6 +372,27 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing'; detectChangesAndExpectClassName('color-red'); }); + it('should allow classes with trailing and leading spaces in [ngClass]', () => { + @Component({ + template: ` +
+
+ ` + }) + class Cmp { + applyClasses = true; + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const leading = fixture.nativeElement.querySelector('[leading-space]'); + const trailing = fixture.nativeElement.querySelector('[trailing-space]'); + expect(leading.className).toBe('foo'); + expect(trailing.className).toBe('foo'); + }); + }); }); } @@ -379,8 +400,7 @@ import {ComponentFixture, TestBed, async} from '@angular/core/testing'; @Component({selector: 'test-cmp', template: ''}) class TestComponent { condition: boolean = true; - // TODO(issue/24571): remove '!'. - items !: any[]; + items: any[]|undefined; arrExpr: string[] = ['foo']; setExpr: Set = new Set(); objExpr: {[klass: string]: any}|null = {'foo': true, 'bar': false}; 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 bfa2910236..5d84cef4e1 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 @@ -856,7 +856,7 @@ describe('compiler compliance: bindings', () => { type: HostAttributeDir, selectors: [["", "hostAttributeDir", ""]], hostAttrs: ["title", "hello there from directive", ${AttributeMarker.Classes}, "one", "two", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"], - hostVars: 2, + hostVars: 4, hostBindings: function HostAttributeDir_HostBindings(rf, ctx, elIndex) { … } 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 ee8b92c419..16f38ed057 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 @@ -1006,15 +1006,8 @@ describe('compiler compliance: styling', () => { const template = ` hostAttrs: [${AttributeMarker.Classes}, "foo", "baz", ${AttributeMarker.Styles}, "width", "200px", "height", "500px"], - hostVars: 6, + hostVars: 8, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - if (rf & 1) { - $r3$.ɵɵallocHostVars(8); - $r3$.ɵɵelementHostAttrs($e0_attrs$); - } ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction if (rf & 2) { $r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵclassMap(ctx.myClass); @@ -1068,14 +1061,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - hostVars: 8, + hostVars: 12, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - if (rf & 1) { - $r3$.ɵɵallocHostVars(12); - } ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction if (rf & 2) { $r3$.ɵɵstyleMap(ctx.myStyle, $r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵclassMap(ctx.myClasses); @@ -1143,14 +1130,8 @@ describe('compiler compliance: styling', () => { `; const hostBindings = ` - hostVars: 6, + hostVars: 8, hostBindings: function MyComponent_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - if (rf & 1) { - $r3$.ɵɵallocHostVars(8); - } ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction if (rf & 2) { $r3$.ɵɵstyleMap(ctx.myStyleExp, $r3$.ɵɵdefaultStyleSanitizer); $r3$.ɵɵclassMap(ctx.myClassExp); @@ -1212,6 +1193,7 @@ 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 = ` + … hostVars: 2, hostBindings: function ClassDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { @@ -1219,7 +1201,7 @@ describe('compiler compliance: styling', () => { } } … - hostVars: 2, + hostVars: 4, hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵstyleProp("width", ctx.myWidth); @@ -1227,7 +1209,7 @@ describe('compiler compliance: styling', () => { } } … - hostVars: 2, + hostVars: 4, hostBindings: function HeightDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵstyleProp("height", ctx.myHeight); @@ -1917,7 +1899,7 @@ describe('compiler compliance: styling', () => { }; const template = ` - hostVars: 4, + hostVars: 6, hostBindings: function WidthDirective_HostBindings(rf, ctx, elIndex) { if (rf & 2) { $r3$.ɵɵhostProperty("id", ctx.id)("title", ctx.title); @@ -2079,14 +2061,8 @@ describe('compiler compliance: styling', () => { }; const template = ` - hostVars: 9, + hostVars: 10, hostBindings: function MyDir_HostBindings(rf, ctx, elIndex) { -<<<<<<< HEAD -======= - … - $r3$.ɵɵallocHostVars(10); - … ->>>>>>> 3a14b06a3b... refactor(ivy): generate 2 slots per styling instruction 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 061888e97d..14c85acb07 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -2340,7 +2340,7 @@ runInEachFileSystem(os => { env.driveMain(); const jsContents = env.getContents('test.js'); const hostBindingsFn = ` - hostVars: 3, + hostVars: 4, hostBindings: function FooCmp_HostBindings(rf, ctx, elIndex) { if (rf & 1) { 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); }); diff --git a/packages/compiler/src/render3/view/styling_builder.ts b/packages/compiler/src/render3/view/styling_builder.ts index 96a701eb3a..e4b86bc076 100644 --- a/packages/compiler/src/render3/view/styling_builder.ts +++ b/packages/compiler/src/render3/view/styling_builder.ts @@ -524,9 +524,10 @@ function isStyleSanitizable(prop: string): boolean { // Note that browsers support both the dash case and // camel case property names when setting through JS. return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' || - prop === 'border-image' || prop === 'borderImage' || prop === 'filter' || - prop === 'list-style' || prop === 'listStyle' || prop === 'list-style-image' || - prop === 'listStyleImage' || prop === 'clip-path' || prop === 'clipPath'; + prop === 'border-image' || prop === 'borderImage' || prop === 'border-image-source' || + prop === 'borderImageSource' || prop === 'filter' || prop === 'list-style' || + prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' || + prop === 'clip-path' || prop === 'clipPath'; } /** diff --git a/packages/compiler/test/render3/style_parser_spec.ts b/packages/compiler/test/render3/style_parser_spec.ts index 9fe365bca9..4f44f6aebb 100644 --- a/packages/compiler/test/render3/style_parser_spec.ts +++ b/packages/compiler/test/render3/style_parser_spec.ts @@ -21,6 +21,11 @@ describe('style parsing', () => { expect(result).toEqual(['width', '100px', 'height', '200px', 'opacity', '0']); }); + it('should allow empty values', () => { + const result = parseStyle('width:;height: ;'); + expect(result).toEqual(['width', '', 'height', '']); + }); + it('should trim values and properties', () => { const result = parseStyle('width :333px ; height:666px ; opacity: 0.5;'); expect(result).toEqual(['width', '333px', 'height', '666px', 'opacity', '0.5']); diff --git a/packages/core/src/render3/assert.ts b/packages/core/src/render3/assert.ts index e822917971..d639f88f2f 100644 --- a/packages/core/src/render3/assert.ts +++ b/packages/core/src/render3/assert.ts @@ -73,6 +73,11 @@ export function assertFirstCreatePass(tView: TView, errMessage?: string) { tView.firstCreatePass, true, errMessage || 'Should only be called in first create pass.'); } +export function assertFirstUpdatePass(tView: TView, errMessage?: string) { + assertEqual( + tView.firstUpdatePass, true, errMessage || 'Should only be called in first update pass.'); +} + /** * This is a basic sanity check that an object is probably a directive def. DirectiveDef is * an interface, so we can't do a direct instanceof check. diff --git a/packages/core/src/render3/bindings.ts b/packages/core/src/render3/bindings.ts index bb99394e49..e9e3edefc5 100644 --- a/packages/core/src/render3/bindings.ts +++ b/packages/core/src/render3/bindings.ts @@ -30,7 +30,19 @@ export function getBinding(lView: LView, bindingIndex: number): any { return lView[bindingIndex]; } -/** Updates binding if changed, then returns whether it was updated. */ +/** + * Updates binding if changed, then returns whether it was updated. + * + * This function also checks the `CheckNoChangesMode` and throws if changes are made. + * Some changes (Objects/iterables) during `CheckNoChangesMode` are exempt to comply with VE + * behavior. + * + * @param lView current `LView` + * @param bindingIndex The binding in the `LView` to check + * @param value New value to check against `lView[bindingIndex]` + * @returns `true` if the bindings has changed. (Throws if binding has changed during + * `CheckNoChangesMode`) + */ export function bindingUpdated(lView: LView, bindingIndex: number, value: any): boolean { ngDevMode && assertNotSame(value, NO_CHANGE, 'Incoming value should never be NO_CHANGE.'); ngDevMode && @@ -50,6 +62,11 @@ export function bindingUpdated(lView: LView, bindingIndex: number, value: any): throwErrorIfNoChangesMode( oldValue === NO_CHANGE, details.oldValue, details.newValue, details.propName); } + // There was a change, but the `devModeEqual` decided that the change is exempt from an error. + // For this reason we exit as if no change. The early exit is needed to prevent the changed + // value to be written into `LView` (If we would write the new value that we would not see it + // as change on next CD.) + return false; } lView[bindingIndex] = value; return true; diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 5b51fd0478..5ec1ec3c75 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -20,9 +20,10 @@ import {CLEAN_PROMISE, addHostBindingsToExpandoInstructions, addToViewTree, crea import {ComponentDef, ComponentType, RenderFlags} from './interfaces/definition'; import {TElementNode, TNode, TNodeType} from './interfaces/node'; import {PlayerHandler} from './interfaces/player'; -import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; +import {RElement, Renderer3, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer'; import {CONTEXT, HEADER_OFFSET, LView, LViewFlags, RootContext, RootContextFlags, TVIEW, TViewType} from './interfaces/view'; -import {enterView, getPreviousOrParentTNode, incrementActiveDirectiveId, leaveView, setActiveHostElement} from './state'; +import {enterView, getPreviousOrParentTNode, leaveView, setActiveHostElement} from './state'; +import {writeDirectClass, writeDirectStyle} from './styling/reconcile'; import {computeStaticStyling} from './styling/static_styling'; import {setUpAttributes} from './util/attrs_utils'; import {publishDefaultGlobalUtils} from './util/global_utils'; @@ -119,8 +120,9 @@ export function renderComponent( // The first index of the first selector is the tag name. const componentTag = componentDef.selectors ![0] ![0] as string; + const hostRenderer = rendererFactory.createRenderer(null, null); const hostRNode = - locateHostElement(rendererFactory, opts.host || componentTag, componentDef.encapsulation); + locateHostElement(hostRenderer, opts.host || componentTag, componentDef.encapsulation); const rootFlags = componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot : LViewFlags.CheckAlways | LViewFlags.IsRoot; const rootContext = createRootContext(opts.scheduler, opts.playerHandler); @@ -137,7 +139,7 @@ export function renderComponent( try { if (rendererFactory.begin) rendererFactory.begin(); const componentView = createRootComponentView( - hostRNode, componentDef, rootView, rendererFactory, renderer, sanitizer); + hostRNode, componentDef, rootView, rendererFactory, renderer, null, sanitizer); component = createRootComponent( componentView, componentDef, rootView, rootContext, opts.hostFeatures || null); @@ -160,14 +162,15 @@ export function renderComponent( * @param rNode Render host element. * @param def ComponentDef * @param rootView The parent view where the host node is stored - * @param renderer The current renderer + * @param hostRenderer The current renderer * @param sanitizer The sanitizer, if provided * * @returns Component view created */ export function createRootComponentView( rNode: RElement | null, def: ComponentDef, rootView: LView, - rendererFactory: RendererFactory3, renderer: Renderer3, sanitizer?: Sanitizer | null): LView { + rendererFactory: RendererFactory3, hostRenderer: Renderer3, addVersion: string | null, + sanitizer: Sanitizer | null): LView { const tView = rootView[TVIEW]; ngDevMode && assertDataInRange(rootView, 0 + HEADER_OFFSET); rootView[0 + HEADER_OFFSET] = rNode; @@ -176,13 +179,27 @@ export function createRootComponentView( if (mergedAttrs !== null) { computeStaticStyling(tNode, mergedAttrs); if (rNode !== null) { - setUpAttributes(renderer, rNode, mergedAttrs); + setUpAttributes(hostRenderer, rNode, mergedAttrs); + if (tNode.classes !== null) { + writeDirectClass(hostRenderer, rNode, tNode.classes); + } + if (tNode.styles !== null) { + writeDirectStyle(hostRenderer, rNode, tNode.styles); + } } } + const viewRenderer = rendererFactory.createRenderer(rNode, def); + if (rNode !== null && addVersion) { + ngDevMode && ngDevMode.rendererSetAttribute++; + isProceduralRenderer(hostRenderer) ? + hostRenderer.setAttribute(rNode, 'ng-version', addVersion) : + rNode.setAttribute('ng-version', addVersion); + } + const componentView = createLView( rootView, getOrCreateTComponentView(def), null, def.onPush ? LViewFlags.Dirty : LViewFlags.CheckAlways, rootView[HEADER_OFFSET], tNode, - rendererFactory, renderer, sanitizer); + rendererFactory, viewRenderer, sanitizer); if (tView.firstCreatePass) { diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, rootView), tView, def.type); @@ -219,26 +236,17 @@ export function createRootComponent( } const rootTNode = getPreviousOrParentTNode(); - // 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 !== null || componentDef.hostAttrs !== null)) { const elementIndex = rootTNode.index - HEADER_OFFSET; setActiveHostElement(elementIndex); - incrementActiveDirectiveId(); const rootTView = rootLView[TVIEW]; addHostBindingsToExpandoInstructions(rootTView, componentDef); growHostVarsSpace(rootTView, rootLView, componentDef.hostVars); invokeHostBindingsInCreationMode(componentDef, component, rootTNode); - - setActiveHostElement(null); } - return component; } diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 6bd001f66c..2b9b21c286 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -19,7 +19,6 @@ import {RendererFactory2} from '../render/api'; import {Sanitizer} from '../sanitization/sanitizer'; import {VERSION} from '../version'; import {NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR} from '../view/provider'; - import {assertComponentType} from './assert'; import {LifecycleHooksFeature, createRootComponent, createRootComponentView, createRootContext} from './component'; import {getComponentDef} from './definition'; @@ -27,7 +26,7 @@ import {NodeInjector} from './di'; import {assignTViewNodeToLView, createLView, createTView, elementCreate, locateHostElement, renderView} from './instructions/shared'; import {ComponentDef} from './interfaces/definition'; import {TContainerNode, TElementContainerNode, TElementNode} from './interfaces/node'; -import {RNode, RendererFactory3, domRendererFactory3, isProceduralRenderer} from './interfaces/renderer'; +import {RNode, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; import {LView, LViewFlags, TVIEW, TViewType} from './interfaces/view'; import {stringifyCSSSelectorList} from './node_selector_matcher'; import {enterView, leaveView} from './state'; @@ -132,8 +131,9 @@ export class ComponentFactory extends viewEngine_ComponentFactory { rootViewInjector.get(RendererFactory2, domRendererFactory3) as RendererFactory3; const sanitizer = rootViewInjector.get(Sanitizer, null); + const hostRenderer = rendererFactory.createRenderer(null, this.componentDef); const hostRNode = rootSelectorOrNode ? - locateHostElement(rendererFactory, rootSelectorOrNode, this.componentDef.encapsulation) : + locateHostElement(hostRenderer, rootSelectorOrNode, this.componentDef.encapsulation) : // Determine a tag name used for creating host elements when this component is created // dynamically. Default to 'div' if this component did not specify any tag name in its // selector. @@ -152,20 +152,13 @@ export class ComponentFactory extends viewEngine_ComponentFactory { /^#root-ng-internal-isolated-\d+/.test(rootSelectorOrNode); const rootContext = createRootContext(); - const renderer = rendererFactory.createRenderer(hostRNode, this.componentDef); - - if (rootSelectorOrNode && hostRNode) { - ngDevMode && ngDevMode.rendererSetAttribute++; - isProceduralRenderer(renderer) ? - renderer.setAttribute(hostRNode, 'ng-version', VERSION.full) : - hostRNode.setAttribute('ng-version', VERSION.full); - } // Create the root view. Uses empty TView and ContentTemplate. const rootTView = createTView(TViewType.Root, -1, null, 1, 0, null, null, null, null, null); const rootLView = createLView( - null, rootTView, rootContext, rootFlags, null, null, rendererFactory, renderer, sanitizer, - rootViewInjector); + null, rootTView, rootContext, rootFlags, null, null, rendererFactory, hostRenderer, + sanitizer, rootViewInjector); + const addVersion = rootSelectorOrNode && hostRNode ? VERSION.full : null; // rootView is the parent when bootstrapping // TODO(misko): it looks like we are entering view here but we don't really need to as @@ -179,7 +172,7 @@ export class ComponentFactory extends viewEngine_ComponentFactory { try { const componentView = createRootComponentView( - hostRNode, this.componentDef, rootLView, rendererFactory, renderer); + hostRNode, this.componentDef, rootLView, rendererFactory, hostRenderer, addVersion, null); tElementNode = getTNode(0, rootLView) as TElementNode; diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 57b8cb8058..72dd5d85c8 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -29,7 +29,6 @@ import {enterDI, leaveDI} from './state'; import {isNameOnlyAttributeMarker} from './util/attrs_utils'; import {getParentInjectorIndex, getParentInjectorView, hasParentInjector} from './util/injector_utils'; import {stringifyForError} from './util/misc_utils'; -import {getInitialStylingValue} from './util/styling_utils'; @@ -269,10 +268,10 @@ export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): str tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer); ngDevMode && assertDefined(tNode, 'expecting tNode'); if (attrNameToInject === 'class') { - return getInitialStylingValue(tNode.classes); + return tNode.classes; } if (attrNameToInject === 'style') { - return getInitialStylingValue(tNode.styles); + return tNode.styles; } const attrs = tNode.attrs; diff --git a/packages/core/src/render3/instructions/advance.ts b/packages/core/src/render3/instructions/advance.ts index 307d9ece05..3ba2dcab3d 100644 --- a/packages/core/src/render3/instructions/advance.ts +++ b/packages/core/src/render3/instructions/advance.ts @@ -8,7 +8,7 @@ import {assertDataInRange, assertGreaterThan} from '../../util/assert'; import {executeCheckHooks, executeInitAndCheckHooks} from '../hooks'; import {FLAGS, HEADER_OFFSET, InitPhaseState, LView, LViewFlags, TVIEW} from '../interfaces/view'; -import {ActiveElementFlags, executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, hasActiveElementFlag, setSelectedIndex} from '../state'; +import {executeElementExitFn, getCheckNoChangesMode, getLView, getSelectedIndex, setSelectedIndex} from '../state'; @@ -46,6 +46,7 @@ export function ɵɵadvance(delta: number): void { * @codeGenApi */ export function ɵɵselect(index: number): void { + // TODO(misko): Remove this function as it is no longer being used. selectIndexInternal(getLView(), index, getCheckNoChangesMode()); } @@ -53,9 +54,7 @@ export function selectIndexInternal(lView: LView, index: number, checkNoChangesM ngDevMode && assertGreaterThan(index, -1, 'Invalid index'); ngDevMode && assertDataInRange(lView, index + HEADER_OFFSET); - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } + executeElementExitFn(); // Flush the initial hooks for elements in the view that have been added up to this point. // PERF WARNING: do NOT extract this to a separate function without running benchmarks diff --git a/packages/core/src/render3/instructions/class_map_interpolation.ts b/packages/core/src/render3/instructions/class_map_interpolation.ts index d1a59cf1b8..5387faf798 100644 --- a/packages/core/src/render3/instructions/class_map_interpolation.ts +++ b/packages/core/src/render3/instructions/class_map_interpolation.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {getLView, getSelectedIndex} from '../state'; - +import {getLView} from '../state'; +import {CLASS_MAP_STYLING_KEY} from '../styling/style_binding_list'; import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation'; -import {classMapInternal} from './styling'; +import {checkStylingMap} from './styling'; @@ -37,7 +37,7 @@ import {classMapInternal} from './styling'; export function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation1(lView, prefix, v0, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -67,7 +67,7 @@ export function ɵɵclassMapInterpolate2( prefix: string, v0: any, i0: string, v1: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -100,7 +100,7 @@ export function ɵɵclassMapInterpolate3( prefix: string, v0: any, i0: string, v1: any, i1: string, v2: any, suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -136,7 +136,7 @@ export function ɵɵclassMapInterpolate4( suffix: string): void { const lView = getLView(); const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -175,7 +175,7 @@ export function ɵɵclassMapInterpolate5( const lView = getLView(); const interpolatedValue = interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -216,7 +216,7 @@ export function ɵɵclassMapInterpolate6( const lView = getLView(); const interpolatedValue = interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -259,7 +259,7 @@ export function ɵɵclassMapInterpolate7( const lView = getLView(); const interpolatedValue = interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -305,7 +305,7 @@ export function ɵɵclassMapInterpolate8( const lView = getLView(); const interpolatedValue = interpolation8( lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } /** @@ -334,5 +334,5 @@ export function ɵɵclassMapInterpolate8( export function ɵɵclassMapInterpolateV(values: any[]): void { const lView = getLView(); const interpolatedValue = interpolationV(lView, values); - classMapInternal(getSelectedIndex(), interpolatedValue); + checkStylingMap(CLASS_MAP_STYLING_KEY, interpolatedValue, true); } diff --git a/packages/core/src/render3/instructions/element.ts b/packages/core/src/render3/instructions/element.ts index 39ec60c5a7..c851ad43dd 100644 --- a/packages/core/src/render3/instructions/element.ts +++ b/packages/core/src/render3/instructions/element.ts @@ -10,20 +10,20 @@ import {assertDataInRange, assertDefined, assertEqual} from '../../util/assert'; import {assertFirstCreatePass, assertHasParent} from '../assert'; import {attachPatchData} from '../context_discovery'; import {registerPostOrderHooks} from '../hooks'; -import {TAttributes, TElementNode, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; +import {TAttributes, TElementNode, TNode, TNodeType, hasClassInput, hasStyleInput} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {StylingMapArray, TStylingContext} from '../interfaces/styling'; import {isContentQueryHost, isDirectiveHost} from '../interfaces/type_checks'; import {HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from '../interfaces/view'; import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; -import {decreaseElementDepthCount, getBindingIndex, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, getSelectedIndex, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state'; +import {decreaseElementDepthCount, getBindingIndex, getElementDepthCount, getIsParent, getLView, getNamespace, getPreviousOrParentTNode, increaseElementDepthCount, setIsNotParent, setPreviousOrParentTNode} from '../state'; +import {writeDirectClass, writeDirectStyle} from '../styling/reconcile'; +import {computeStaticStyling} from '../styling/static_styling'; import {setUpAttributes} from '../util/attrs_utils'; -import {getInitialStylingValue, hasClassInput, hasStyleInput, selectClassBasedInputName} from '../util/styling_utils'; -import {getConstant, getNativeByTNode, getTNode} from '../util/view_utils'; +import {getConstant} from '../util/view_utils'; +import {setDirectiveInputsWhichShadowsStyling} from './property'; +import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, matchingSchemas, resolveDirectives, saveResolvedLocalsInData} from './shared'; -import {createDirectivesInstances, elementCreate, executeContentQueries, getOrCreateTNode, matchingSchemas, renderInitialStyling, resolveDirectives, saveResolvedLocalsInData, setInputsForProperty} from './shared'; -import {registerInitialStylingOnTNode} from './styling'; function elementStartFirstCreatePass( index: number, tView: TView, lView: LView, native: RElement, name: string, @@ -40,7 +40,7 @@ function elementStartFirstCreatePass( ngDevMode && warnAboutUnknownElement(lView, native, tNode, hasDirectives); if (tNode.mergedAttrs !== null) { - registerInitialStylingOnTNode(tNode, tNode.mergedAttrs, 0); + computeStaticStyling(tNode, tNode.mergedAttrs); } if (tView.queries !== null) { @@ -88,8 +88,13 @@ export function ɵɵelementStart( if (mergedAttrs !== null) { setUpAttributes(renderer, native, mergedAttrs); } - if ((tNode.flags & TNodeFlags.hasInitialStyling) === TNodeFlags.hasInitialStyling) { - renderInitialStyling(renderer, native, tNode, false); + const classes = tNode.classes; + if (classes !== null) { + writeDirectClass(renderer, native, classes); + } + const styles = tNode.styles; + if (styles !== null) { + writeDirectStyle(renderer, native, styles); } appendChild(native, tNode, lView); @@ -143,17 +148,15 @@ export function ɵɵelementEnd(): void { } } - if (hasClassInput(tNode)) { - const inputName: string = selectClassBasedInputName(tNode.inputs !); - setDirectiveStylingInput(tNode.classes, lView, tNode.inputs ![inputName], inputName); + if (tNode.classes !== null && hasClassInput(tNode)) { + setDirectiveInputsWhichShadowsStyling(tNode, lView, tNode.classes, true); } - if (hasStyleInput(tNode)) { - setDirectiveStylingInput(tNode.styles, lView, tNode.inputs !['style'], 'style'); + if (tNode.styles !== null && hasStyleInput(tNode)) { + setDirectiveInputsWhichShadowsStyling(tNode, lView, tNode.styles, false); } } - /** * Creates an empty element using {@link elementStart} and {@link elementEnd} * @@ -170,20 +173,6 @@ export function ɵɵelement( ɵɵelementEnd(); } -function setDirectiveStylingInput( - context: TStylingContext | StylingMapArray | string | null, lView: LView, - stylingInputs: (string | number)[], propName: string) { - // older versions of Angular treat the input as `null` in the - // event that the value does not exist at all. For this reason - // we can't have a styling value be an empty string. - const value = (context && getInitialStylingValue(context)) || null; - - // Ivy does an extra `[class]` write with a falsy value since the value - // is applied during creation mode. This is a deviation from VE and should - // be (Jira Issue = FW-1467). - setInputsForProperty(lView, stylingInputs, propName, value); -} - function warnAboutUnknownElement( hostView: LView, element: RElement, tNode: TNode, hasDirectives: boolean): void { const schemas = hostView[TVIEW].schemas; diff --git a/packages/core/src/render3/instructions/element_container.ts b/packages/core/src/render3/instructions/element_container.ts index 8456ad1ff9..509a5888f9 100644 --- a/packages/core/src/render3/instructions/element_container.ts +++ b/packages/core/src/render3/instructions/element_container.ts @@ -15,10 +15,10 @@ import {HEADER_OFFSET, LView, RENDERER, TVIEW, TView, T_HOST} from '../interface import {assertNodeType} from '../node_assert'; import {appendChild} from '../node_manipulation'; import {getBindingIndex, getIsParent, getLView, getPreviousOrParentTNode, setIsNotParent, setPreviousOrParentTNode} from '../state'; +import {computeStaticStyling} from '../styling/static_styling'; import {getConstant} from '../util/view_utils'; import {createDirectivesInstances, executeContentQueries, getOrCreateTNode, resolveDirectives, saveResolvedLocalsInData} from './shared'; -import {registerInitialStylingOnTNode} from './styling'; function elementContainerStartFirstCreatePass( index: number, tView: TView, lView: LView, attrsIndex?: number | null, @@ -33,7 +33,7 @@ function elementContainerStartFirstCreatePass( // While ng-container doesn't necessarily support styling, we use the style context to identify // and execute directives on the ng-container. if (attrs !== null) { - registerInitialStylingOnTNode(tNode, attrs, 0); + computeStaticStyling(tNode, attrs); } const localRefs = getConstant(tViewConsts, localRefsIndex); diff --git a/packages/core/src/render3/instructions/lview_debug.ts b/packages/core/src/render3/instructions/lview_debug.ts index e6166ee6c3..791ef2bd43 100644 --- a/packages/core/src/render3/instructions/lview_debug.ts +++ b/packages/core/src/render3/instructions/lview_debug.ts @@ -14,15 +14,13 @@ import {initNgDevMode} from '../../util/ng_dev_mode'; import {ACTIVE_INDEX, ActiveIndexFlag, CONTAINER_HEADER_OFFSET, LContainer, MOVED_VIEWS, NATIVE} from '../interfaces/container'; import {DirectiveDefList, PipeDefList, ViewQueriesFunction} from '../interfaces/definition'; import {COMMENT_MARKER, ELEMENT_MARKER, I18nMutateOpCode, I18nMutateOpCodes, I18nUpdateOpCode, I18nUpdateOpCodes, TIcu} from '../interfaces/i18n'; -import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node'; +import {PropertyAliases, TConstants, TContainerNode, TElementNode, TNode as ITNode, TNodeFlags, TNodeProviderIndexes, TNodeType, TViewNode} from '../interfaces/node'; import {SelectorFlags} from '../interfaces/projection'; import {TQueries} from '../interfaces/query'; import {RComment, RElement, RNode} from '../interfaces/renderer'; -import {TStylingContext, TStylingRange} from '../interfaces/styling'; +import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDuplicate, getTStylingRangePrev, getTStylingRangePrevDuplicate} from '../interfaces/styling'; import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_VIEW, ExpandoInstructions, FLAGS, HEADER_OFFSET, HOST, HookData, INJECTOR, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, SANITIZER, TData, TVIEW, TView as ITView, TView, TViewType, T_HOST} from '../interfaces/view'; -import {DebugNodeStyling, NodeStylingDebug} from '../styling/styling_debug'; import {attachDebugObject} from '../util/debug_utils'; -import {isStylingContext} from '../util/styling_utils'; import {getLContainerActiveIndex, getTNode, unwrapRNode} from '../util/view_utils'; const NG_DEV_MODE = ((typeof ngDevMode === 'undefined' || !!ngDevMode) && initNgDevMode()); @@ -140,7 +138,7 @@ export const TViewConstructor = class TView implements ITView { public components: number[]|null, // public directiveRegistry: DirectiveDefList|null, // public pipeRegistry: PipeDefList|null, // - public firstChild: TNode|null, // + public firstChild: ITNode|null, // public schemas: SchemaMetadata[]|null, // public consts: TConstants|null, // ) {} @@ -152,7 +150,7 @@ export const TViewConstructor = class TView implements ITView { } }; -export const TNodeConstructor = class TNode implements ITNode { +class TNode implements ITNode { constructor( public tView_: TView, // public type: TNodeType, // @@ -176,8 +174,8 @@ export const TNodeConstructor = class TNode implements ITNode { public child: ITNode|null, // public parent: TElementNode|TContainerNode|null, // public projection: number|(ITNode|RNode[])[]|null, // - public styles: TStylingContext|null, // - public classes: TStylingContext|null, // + public styles: string|null, // + public classes: string|null, // public classBindings: TStylingRange, // public styleBindings: TStylingRange, // ) {} @@ -206,7 +204,6 @@ export const TNodeConstructor = class TNode implements ITNode { if (this.flags & TNodeFlags.hasClassInput) flags.push('TNodeFlags.hasClassInput'); if (this.flags & TNodeFlags.hasContentQuery) flags.push('TNodeFlags.hasContentQuery'); if (this.flags & TNodeFlags.hasStyleInput) flags.push('TNodeFlags.hasStyleInput'); - if (this.flags & TNodeFlags.hasInitialStyling) flags.push('TNodeFlags.hasInitialStyling'); if (this.flags & TNodeFlags.hasHostBindings) flags.push('TNodeFlags.hasHostBindings'); if (this.flags & TNodeFlags.isComponentHost) flags.push('TNodeFlags.isComponentHost'); if (this.flags & TNodeFlags.isDirectiveHost) flags.push('TNodeFlags.isDirectiveHost'); @@ -233,9 +230,54 @@ export const TNodeConstructor = class TNode implements ITNode { buf.push(''); return buf.join(''); } -}; -function processTNodeChildren(tNode: TNode | null, buf: string[]) { + get styleBindings_(): DebugStyleBindings { return toDebugStyleBinding(this, false); } + get classBindings_(): DebugStyleBindings { return toDebugStyleBinding(this, true); } +} +export const TNodeDebug = TNode; +export type TNodeDebug = TNode; + +export interface DebugStyleBindings extends Array { + [0]: string|null; +} +export interface DebugStyleBinding { + key: TStylingKey; + index: number; + isTemplate: boolean; + prevDuplicate: boolean; + nextDuplicate: boolean; + prevIndex: number; + nextIndex: number; +} + +function toDebugStyleBinding(tNode: TNode, isClassBased: boolean): DebugStyleBindings { + const tData = tNode.tView_.data; + const bindings: DebugStyleBindings = [] as any; + const range = isClassBased ? tNode.classBindings : tNode.styleBindings; + const prev = getTStylingRangePrev(range); + const next = getTStylingRangeNext(range); + let isTemplate = next !== 0; + let cursor = isTemplate ? next : prev; + while (cursor !== 0) { + const itemKey = tData[cursor] as TStylingKey; + const itemRange = tData[cursor + 1] as TStylingRange; + bindings.unshift({ + key: itemKey, + index: cursor, + isTemplate: isTemplate, + prevDuplicate: getTStylingRangePrevDuplicate(itemRange), + nextDuplicate: getTStylingRangeNextDuplicate(itemRange), + nextIndex: getTStylingRangeNext(itemRange), + prevIndex: getTStylingRangePrev(itemRange), + }); + if (cursor === prev) isTemplate = false; + cursor = getTStylingRangePrev(itemRange); + } + bindings.unshift(isClassBased ? tNode.classes : tNode.styles); + return bindings; +} + +function processTNodeChildren(tNode: ITNode | null, buf: string[]) { while (tNode) { buf.push((tNode as any as{template_: string}).template_); tNode = tNode.next; @@ -389,8 +431,6 @@ export class LViewDebug { export interface DebugNode { html: string|null; native: Node; - styles: DebugNodeStyling|null; - classes: DebugNodeStyling|null; nodes: DebugNode[]|null; component: LViewDebug|null; } @@ -401,10 +441,10 @@ export interface DebugNode { * @param tNode * @param lView */ -export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|null { +export function toDebugNodes(tNode: ITNode | null, lView: LView): DebugNode[]|null { if (tNode) { const debugNodes: DebugNode[] = []; - let tNodeCursor: TNode|null = tNode; + let tNodeCursor: ITNode|null = tNode; while (tNodeCursor) { debugNodes.push(buildDebugNode(tNodeCursor, lView, tNodeCursor.index)); tNodeCursor = tNodeCursor.next; @@ -415,19 +455,13 @@ export function toDebugNodes(tNode: TNode | null, lView: LView): DebugNode[]|nul } } -export function buildDebugNode(tNode: TNode, lView: LView, nodeIndex: number): DebugNode { +export function buildDebugNode(tNode: ITNode, lView: LView, nodeIndex: number): DebugNode { const rawValue = lView[nodeIndex]; const native = unwrapRNode(rawValue); const componentLViewDebug = toDebug(readLViewValue(rawValue)); - const styles = isStylingContext(tNode.styles) ? - new NodeStylingDebug(tNode.styles as any as TStylingContext, tNode, lView, false) : - null; - const classes = isStylingContext(tNode.classes) ? - new NodeStylingDebug(tNode.classes as any as TStylingContext, tNode, lView, true) : - null; return { html: toHtml(native), - native: native as any, styles, classes, + native: native as any, nodes: toDebugNodes(tNode.child, lView), component: componentLViewDebug, }; diff --git a/packages/core/src/render3/instructions/property.ts b/packages/core/src/render3/instructions/property.ts index 524fb644fa..7abe5bf75c 100644 --- a/packages/core/src/render3/instructions/property.ts +++ b/packages/core/src/render3/instructions/property.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ import {bindingUpdated} from '../bindings'; +import {TNode} from '../interfaces/node'; import {SanitizerFn} from '../interfaces/sanitization'; -import {TVIEW} from '../interfaces/view'; +import {LView, TVIEW} from '../interfaces/view'; import {getLView, getSelectedIndex, nextBindingIndex} from '../state'; - -import {elementPropertyInternal, storePropertyBindingMetadata} from './shared'; +import {elementPropertyInternal, setInputsForProperty, storePropertyBindingMetadata} from './shared'; /** @@ -42,3 +42,16 @@ export function ɵɵproperty( } return ɵɵproperty; } + +/** + * Given `
` and `MyDir` with `@Input('style')` we need to write to + * directive input. + */ +export function setDirectiveInputsWhichShadowsStyling( + tNode: TNode, lView: LView, value: any, isClassBased: boolean) { + const inputs = tNode.inputs !; + const property = isClassBased ? 'class' : 'style'; + // We support both 'class' and `className` hence the fallback. + const stylingInputs = inputs[property] || (isClassBased && inputs['className']); + setInputsForProperty(lView, stylingInputs, property, value); +} diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index f8c87782f9..998b70bf7e 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -31,16 +31,14 @@ import {isComponentDef, isComponentHost, isContentQueryHost, isLContainer, isRoo import {CHILD_HEAD, CHILD_TAIL, CLEANUP, CONTEXT, DECLARATION_COMPONENT_VIEW, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, INJECTOR, InitPhaseState, LView, LViewFlags, NEXT, PARENT, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TData, TVIEW, TView, TViewType, T_HOST} from '../interfaces/view'; import {assertNodeOfPossibleTypes} from '../node_assert'; import {isNodeMatchingSelectorList} from '../node_selector_matcher'; -import {ActiveElementFlags, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, hasActiveElementFlag, incrementActiveDirectiveId, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; -import {renderStylingMap, writeStylingValueDirectly} from '../styling/bindings'; +import {clearActiveHostElement, enterView, executeElementExitFn, getBindingsEnabled, getCheckNoChangesMode, getIsParent, getPreviousOrParentTNode, getSelectedIndex, leaveView, leaveViewProcessExit, setActiveHostElement, setBindingIndex, setBindingRoot, setCheckNoChangesMode, setCurrentQueryIndex, setPreviousOrParentTNode, setSelectedIndex} from '../state'; import {NO_CHANGE} from '../tokens'; import {isAnimationProp, mergeHostAttrs} from '../util/attrs_utils'; import {INTERPOLATION_DELIMITER, renderStringify, stringifyForError} from '../util/misc_utils'; -import {getInitialStylingValue} from '../util/styling_utils'; import {getLViewParent} from '../util/view_traversal_utils'; import {getComponentLViewByIndex, getNativeByIndex, getNativeByTNode, getTNode, isCreationMode, readPatchedLView, resetPreOrderHookFlags, viewAttachedToChangeDetector} from '../util/view_utils'; import {selectIndexInternal} from './advance'; -import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeConstructor, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug'; +import {LCleanup, LViewBlueprint, MatchesArray, TCleanup, TNodeDebug, TNodeInitialInputs, TNodeLocalNames, TViewComponents, TViewConstructor, attachLContainerDebug, attachLViewDebug, cloneToLViewFromTViewBlueprint, cloneToTViewData} from './lview_debug'; @@ -102,15 +100,6 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie } else { // If it's not a number, it's a host binding function that needs to be executed. if (instruction !== null) { - // Each directive gets a uniqueId value that is the same for both - // create and update calls when the hostBindings function is called. The - // directive uniqueId is not set anywhere--it is just incremented between - // each hostBindings call and is useful for helping instruction code - // uniquely determine which directive is currently active when executed. - // 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(); - setBindingIndex(bindingRootIndex); const hostCtx = lView[currentDirectiveIndex]; instruction(RenderFlags.Update, hostCtx, currentElementIndex); @@ -123,10 +112,11 @@ export function setHostBindingsByExecutingExpandoInstructions(tView: TView, lVie // iterate over those directives which actually have `hostBindings`. currentDirectiveIndex++; } + setBindingRoot(bindingRootIndex); } } } finally { - setActiveHostElement(selectedIndex); + clearActiveHostElement(); } } @@ -493,9 +483,13 @@ export function refreshView( incrementInitPhaseFlags(lView, InitPhaseState.AfterViewInitHooksToBeRun); } } - - } finally { if (tView.firstUpdatePass === true) { + // We need to make sure that we only flip the flag on successful `refreshView` only + // Don't do this in `finally` block. + // If we did this in `finally` block then an exception could block the execution of styling + // instructions which in turn would be unable to insert themselves into the styling linked + // list. The result of this would be that if the exception would not be throw on subsequent CD + // the styling would be unable to process it data and reflect to the DOM. tView.firstUpdatePass = false; } @@ -508,7 +502,7 @@ export function refreshView( if (!checkNoChangesMode) { lView[FLAGS] &= ~(LViewFlags.Dirty | LViewFlags.FirstLViewPass); } - + } finally { leaveViewProcessExit(); } } @@ -538,7 +532,7 @@ function executeTemplate( lView: LView, templateFn: ComponentTemplate, rf: RenderFlags, context: T) { const prevSelectedIndex = getSelectedIndex(); try { - setActiveHostElement(null); + clearActiveHostElement(); if (rf & RenderFlags.Update && lView.length > HEADER_OFFSET) { // When we're updating, inherently select 0 so we don't // have to generate that instruction for most update blocks. @@ -546,9 +540,7 @@ function executeTemplate( } templateFn(rf, context); } finally { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } + executeElementExitFn(); setSelectedIndex(prevSelectedIndex); } } @@ -742,10 +734,8 @@ function assertHostNodeExists(rElement: RElement, elementOrSelector: RElement | * @param encapsulation View Encapsulation defined for component that requests host element. */ export function locateHostElement( - rendererFactory: RendererFactory3, elementOrSelector: RElement | string, + renderer: Renderer3, elementOrSelector: RElement | string, encapsulation: ViewEncapsulation): RElement { - const renderer = rendererFactory.createRenderer(null, null); - if (isProceduralRenderer(renderer)) { // When using native Shadow DOM, do not clear host element to allow native slot projection const preserveContent = encapsulation === ViewEncapsulation.ShadowDom; @@ -814,7 +804,7 @@ export function createTNode( adjustedIndex: number, tagName: string | null, attrs: TAttributes | null): TNode { ngDevMode && ngDevMode.tNode++; let injectorIndex = tParent ? tParent.injectorIndex : -1; - return ngDevMode ? new TNodeConstructor( + return ngDevMode ? new TNodeDebug( tView, // tView_: TView type, // type: TNodeType adjustedIndex, // index: number @@ -837,8 +827,8 @@ export function createTNode( null, // child: ITNode|null tParent, // parent: TElementNode|TContainerNode|null null, // projection: number|(ITNode|RNode[])[]|null - null, // styles: TStylingContext|null - null, // classes: TStylingContext|null + null, // styles: string|null + null, // classes: string|null 0 as any, // classBindings: TStylingRange; 0 as any, // styleBindings: TStylingRange; ) : @@ -1264,7 +1254,7 @@ function instantiateAllDirectives( } } -function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNode) { +function invokeDirectivesHostBindings(tView: TView, lView: LView, tNode: TNode) { const start = tNode.directiveStart; const end = tNode.directiveEnd; const expando = tView.expandoInstructions !; @@ -1272,25 +1262,27 @@ function invokeDirectivesHostBindings(tView: TView, viewData: LView, tNode: TNod const elementIndex = tNode.index - HEADER_OFFSET; try { setActiveHostElement(elementIndex); - for (let i = start; i < end; i++) { const def = tView.data[i] as DirectiveDef; - const directive = viewData[i]; + const directive = lView[i]; 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(); invokeHostBindingsInCreationMode(def, directive, tNode); } else if (firstCreatePass) { expando.push(null); } } } finally { - setActiveHostElement(null); + clearActiveHostElement(); } } -// TODO(COMMIT): jsdoc +/** + * Invoke the host bindings in creation mode. + * + * @param def `DirectiveDef` which may contain the `hostBindings` function. + * @param directive Instance of directive. + * @param tNode Associated `TNode`. + */ export function invokeHostBindingsInCreationMode( def: DirectiveDef, directive: any, tNode: TNode) { if (def.hostBindings !== null) { @@ -1300,11 +1292,11 @@ export function invokeHostBindingsInCreationMode( } /** -* Generates a new block in TView.expandoInstructions for this node. -* -* Each expando block starts with the element index (turned negative so we can distinguish -* it from the hostVar count) and the directive count. See more in VIEW_DATA.md. -*/ + * Generates a new block in TView.expandoInstructions for this node. + * + * Each expando block starts with the element index (turned negative so we can distinguish + * it from the hostVar count) and the directive count. See more in VIEW_DATA.md. + */ export function generateExpandoInstructionBlock( tView: TView, tNode: TNode, directiveCount: number): void { ngDevMode && assertEqual( @@ -1993,4 +1985,3 @@ export function textBindingInternal(lView: LView, index: number, value: string): const renderer = lView[RENDERER]; isProceduralRenderer(renderer) ? renderer.setValue(element, value) : element.textContent = value; } - diff --git a/packages/core/src/render3/instructions/style_prop_interpolation.ts b/packages/core/src/render3/instructions/style_prop_interpolation.ts index 0bdadb76e1..a5d3ca7580 100644 --- a/packages/core/src/render3/instructions/style_prop_interpolation.ts +++ b/packages/core/src/render3/instructions/style_prop_interpolation.ts @@ -5,11 +5,10 @@ * 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 {getLView, getSelectedIndex} from '../state'; +import {getLView,} from '../state'; import {interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV} from './interpolation'; -import {stylePropInternal} from './styling'; - +import {checkStylingProperty} from './styling'; /** @@ -43,7 +42,7 @@ export function ɵɵstylePropInterpolate1( valueSuffix?: string | null): typeof ɵɵstylePropInterpolate1 { const lView = getLView(); const interpolatedValue = interpolation1(lView, prefix, v0, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate1; } @@ -80,7 +79,7 @@ export function ɵɵstylePropInterpolate2( valueSuffix?: string | null): typeof ɵɵstylePropInterpolate2 { const lView = getLView(); const interpolatedValue = interpolation2(lView, prefix, v0, i0, v1, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate2; } @@ -119,7 +118,7 @@ export function ɵɵstylePropInterpolate3( valueSuffix?: string | null): typeof ɵɵstylePropInterpolate3 { const lView = getLView(); const interpolatedValue = interpolation3(lView, prefix, v0, i0, v1, i1, v2, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate3; } @@ -160,7 +159,7 @@ export function ɵɵstylePropInterpolate4( v3: any, suffix: string, valueSuffix?: string | null): typeof ɵɵstylePropInterpolate4 { const lView = getLView(); const interpolatedValue = interpolation4(lView, prefix, v0, i0, v1, i1, v2, i2, v3, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate4; } @@ -205,7 +204,7 @@ export function ɵɵstylePropInterpolate5( const lView = getLView(); const interpolatedValue = interpolation5(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate5; } @@ -252,7 +251,7 @@ export function ɵɵstylePropInterpolate6( const lView = getLView(); const interpolatedValue = interpolation6(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate6; } @@ -302,7 +301,7 @@ export function ɵɵstylePropInterpolate7( const lView = getLView(); const interpolatedValue = interpolation7(lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate7; } @@ -354,7 +353,7 @@ export function ɵɵstylePropInterpolate8( const lView = getLView(); const interpolatedValue = interpolation8( lView, prefix, v0, i0, v1, i1, v2, i2, v3, i3, v4, i4, v5, i5, v6, i6, v7, suffix); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolate8; } @@ -392,6 +391,6 @@ export function ɵɵstylePropInterpolateV( prop: string, values: any[], valueSuffix?: string | null): typeof ɵɵstylePropInterpolateV { const lView = getLView(); const interpolatedValue = interpolationV(lView, values); - stylePropInternal(getSelectedIndex(), prop, interpolatedValue as string, valueSuffix); + checkStylingProperty(prop, interpolatedValue, valueSuffix, false); return ɵɵstylePropInterpolateV; } diff --git a/packages/core/src/render3/instructions/styling.ts b/packages/core/src/render3/instructions/styling.ts index 5a592831fa..2418a15dbb 100644 --- a/packages/core/src/render3/instructions/styling.ts +++ b/packages/core/src/render3/instructions/styling.ts @@ -7,22 +7,22 @@ */ import {SafeValue} from '../../sanitization/bypass'; import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; -import {throwErrorIfNoChangesMode} from '../errors'; -import {setInputsForProperty} from '../instructions/shared'; -import {AttributeMarker, TAttributes, TNode, TNodeFlags, TNodeType} from '../interfaces/node'; +import {assertEqual, assertGreaterThan, assertLessThan} from '../../util/assert'; +import {concatStringsWithSpace} from '../../util/stringify'; +import {assertFirstUpdatePass} from '../assert'; +import {bindingUpdated} from '../bindings'; +import {TNode, TNodeFlags, TNodeType} from '../interfaces/node'; import {RElement} from '../interfaces/renderer'; -import {StylingMapArray, StylingMapArrayIndex, TStylingContext} from '../interfaces/styling'; -import {isDirectiveHost} from '../interfaces/type_checks'; -import {LView, RENDERER, TVIEW} from '../interfaces/view'; -import {getActiveDirectiveId, getCheckNoChangesMode, getCurrentStyleSanitizer, getLView, getSelectedIndex, incrementBindingIndex, nextBindingIndex, resetCurrentStyleSanitizer, setCurrentStyleSanitizer, setElementExitFn} from '../state'; -import {applyStylingMapDirectly, applyStylingValueDirectly, flushStyling, setClass, setStyle, updateClassViaContext, updateStyleViaContext} from '../styling/bindings'; -import {activateStylingMapFeature} from '../styling/map_based_bindings'; -import {attachStylingDebugObject} from '../styling/styling_debug'; +import {SanitizerFn} from '../interfaces/sanitization'; +import {TStylingKey, TStylingMapKey, TStylingSanitizationKey, TStylingSuffixKey, getTStylingRangeTail} from '../interfaces/styling'; +import {HEADER_OFFSET, RENDERER, TVIEW, TView} from '../interfaces/view'; +import {getCheckNoChangesMode, getClassBindingChanged, getCurrentStyleSanitizer, getLView, getSelectedIndex, getStyleBindingChanged, incrementBindingIndex, isActiveHostElement, markStylingBindingDirty, setCurrentStyleSanitizer, setElementExitFn} from '../state'; +import {writeAndReconcileClass, writeAndReconcileStyle} from '../styling/reconcile'; +import {CLASS_MAP_STYLING_KEY, IGNORE_DUE_TO_INPUT_SHADOW, STYLE_MAP_STYLING_KEY, flushStyleBinding, insertTStylingBinding} from '../styling/style_binding_list'; import {NO_CHANGE} from '../tokens'; -import {renderStringify} from '../util/misc_utils'; -import {addItemToStylingMap, allocStylingMapArray, allocTStylingContext, allowDirectStyling, concatString, forceClassesAsString, forceStylesAsString, getInitialStylingValue, getStylingMapArray, getValue, hasClassInput, hasStyleInput, hasValueChanged, hasValueChangedUnwrapSafeValue, isHostStylingActive, isStylingContext, isStylingMapArray, isStylingValueDefined, normalizeIntoStylingMap, patchConfig, selectClassBasedInputName, setValue, stylingMapToString} from '../util/styling_utils'; -import {getNativeByTNode, getTNode} from '../util/view_utils'; +import {unwrapRNode} from '../util/view_utils'; +import {setDirectiveInputsWhichShadowsStyling} from './property'; @@ -68,9 +68,9 @@ export function ɵɵstyleSanitizer(sanitizer: StyleSanitizeFn | null): void { * @codeGenApi */ export function ɵɵstyleProp( - prop: string, value: string | number | SafeValue | null, + prop: string, value: string | number | SafeValue | null | undefined, suffix?: string | null): typeof ɵɵstyleProp { - stylePropInternal(getSelectedIndex(), prop, value, suffix); + checkStylingProperty(prop, value, suffix, false); return ɵɵstyleProp; } @@ -89,32 +89,9 @@ export function ɵɵstyleProp( * * @codeGenApi */ -export function ɵɵclassProp(className: string, value: boolean | null): typeof ɵɵclassProp { - // if a value is interpolated then it may render a `NO_CHANGE` value. - // in this case we do not need to do anything, but the binding index - // still needs to be incremented because all styling binding values - // are stored inside of the lView. - const bindingIndex = nextBindingIndex(); - const lView = getLView(); - const elementIndex = getSelectedIndex(); - const tNode = getTNode(elementIndex, lView); - const firstUpdatePass = lView[TVIEW].firstUpdatePass; - - // we check for this in the instruction code so that the context can be notified - // about prop or map bindings so that the direct apply check can decide earlier - // if it allows for context resolution to be bypassed. - if (firstUpdatePass) { - patchConfig(tNode, TNodeFlags.hasClassPropBindings); - patchHostStylingFlag(tNode, isHostStyling(), true); - } - - const updated = stylingProp(tNode, firstUpdatePass, lView, bindingIndex, className, value, true); - if (ngDevMode) { - ngDevMode.classProp++; - if (updated) { - ngDevMode.classPropCacheMiss++; - } - } +export function ɵɵclassProp( + className: string, value: boolean | null | undefined): typeof ɵɵclassProp { + checkStylingProperty(className, value, null, true); return ɵɵclassProp; } @@ -138,39 +115,10 @@ export function ɵɵclassProp(className: string, value: boolean | null): typeof * * @codeGenApi */ -export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | null): void { - const index = getSelectedIndex(); - const lView = getLView(); - const tNode = getTNode(index, lView); - const firstUpdatePass = lView[TVIEW].firstUpdatePass; - const context = getStylesContext(tNode); - const hasDirectiveInput = hasStyleInput(tNode); - - // if a value is interpolated then it may render a `NO_CHANGE` value. - // in this case we do not need to do anything, but the binding index - // still needs to be incremented because all styling binding values - // are stored inside of the lView. - const bindingIndex = incrementBindingIndex(2); - const hostBindingsMode = isHostStyling(); - - // inputs are only evaluated from a template binding into a directive, therefore, - // there should not be a situation where a directive host bindings function - // evaluates the inputs (this should only happen in the template function) - if (!hostBindingsMode && hasDirectiveInput && styles !== NO_CHANGE) { - updateDirectiveInputValue(context, lView, tNode, bindingIndex, styles, false, firstUpdatePass); - styles = NO_CHANGE; - } - - // we check for this in the instruction code so that the context can be notified - // about prop or map bindings so that the direct apply check can decide earlier - // if it allows for context resolution to be bypassed. - if (firstUpdatePass) { - patchConfig(tNode, TNodeFlags.hasStyleMapBindings); - patchHostStylingFlag(tNode, isHostStyling(), false); - } - - stylingMap( - context, tNode, firstUpdatePass, lView, bindingIndex, styles, false, hasDirectiveInput); +export function ɵɵstyleMap( + styles: {[styleName: string]: any} | Map| string | null | + undefined): void { + checkStylingMap(STYLE_MAP_STYLING_KEY, styles, false); } /** @@ -191,7 +139,224 @@ export function ɵɵstyleMap(styles: {[styleName: string]: any} | NO_CHANGE | nu * * @codeGenApi */ -export function ɵɵclassMap(classes: {[className: string]: any} | NO_CHANGE | string | null): void { - classMapInternal(getSelectedIndex(), classes); +export function ɵɵclassMap( + classes: {[className: string]: boolean | null | undefined} | + Map| Set| string[] | string | null | undefined): void { + checkStylingMap(CLASS_MAP_STYLING_KEY, classes, true); } + +/** + * Common code between `ɵɵclassProp` and `ɵɵstyleProp`. + * + * @param prop property name. + * @param value binding value. + * @param suffixOrSanitizer suffix or sanitization function + * @param isClassBased `true` if `class` change (`false` if `style`) + */ +export function checkStylingProperty( + prop: string, value: any | NO_CHANGE, + suffixOrSanitizer: SanitizerFn | string | null | undefined, isClassBased: boolean): void { + const lView = getLView(); + const tView = lView[TVIEW]; + // Styling instructions use 2 slots per binding. + // 1. one for the value / TStylingKey + // 2. one for the intermittent-value / TStylingRange + const bindingIndex = incrementBindingIndex(2); + if (tView.firstUpdatePass) { + // This is a work around. Once PR#34480 lands the sanitizer is passed explicitly and this line + // can be removed. + let styleSanitizer: StyleSanitizeFn|null; + if (suffixOrSanitizer == null) { + if (styleSanitizer = getCurrentStyleSanitizer()) { + suffixOrSanitizer = styleSanitizer as any; + } + } + stylingPropertyFirstUpdatePass(tView, prop, suffixOrSanitizer, bindingIndex, isClassBased); + } + if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) { + markStylingBindingDirty(bindingIndex, isClassBased); + setElementExitFn(flushStylingOnElementExit); + } +} + +/** +* Common code between `ɵɵclassMap` and `ɵɵstyleMap`. +* +* @param tStylingMapKey See `STYLE_MAP_STYLING_KEY` and `CLASS_MAP_STYLING_KEY`. +* @param value binding value. +* @param isClassBased `true` if `class` change (`false` if `style`) +*/ +export function checkStylingMap( + tStylingMapKey: TStylingMapKey, value: any | NO_CHANGE, isClassBased: boolean): void { + const lView = getLView(); + const tView = lView[TVIEW]; + const bindingIndex = incrementBindingIndex(2); + if (tView.firstUpdatePass) { + stylingPropertyFirstUpdatePass(tView, tStylingMapKey, null, bindingIndex, isClassBased); + } + if (value !== NO_CHANGE && bindingUpdated(lView, bindingIndex, value)) { + // `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the + // if so as not to read unnecessarily. + const tNode = tView.data[getSelectedIndex() + HEADER_OFFSET] as TNode; + if (hasStylingInputShadow(tNode, isClassBased) && !isInHostBindings(tView, bindingIndex)) { + // VE concatenates the static portion with the dynamic portion. + // We are doing the same. + let staticPrefix = isClassBased ? tNode.classes : tNode.styles; + ngDevMode && isClassBased === false && staticPrefix !== null && + assertEqual( + staticPrefix.endsWith(';'), true, 'Expecting static portion to end with \';\''); + if (typeof value === 'string') { + value = concatStringsWithSpace(staticPrefix, value as string); + } + // Given `
` such that `my-dir` has `@Input('style')`. + // This takes over the `[style]` binding. (Same for `[class]`) + setDirectiveInputsWhichShadowsStyling(tNode, lView, value, isClassBased); + } else { + markStylingBindingDirty(bindingIndex, isClassBased); + setElementExitFn(flushStylingOnElementExit); + } + } +} + +/** + * Determines when the binding is in `hostBindings` section + * + * @param tView Current `TView` + * @param bindingIndex index of binding which we would like if it is in `hostBindings` + */ +function isInHostBindings(tView: TView, bindingIndex: number): boolean { + // All host bindings are placed after the expando section. + return bindingIndex >= tView.expandoStartIndex; +} + +/** +* Collects the necessary information to insert the binding into a linked list of style bindings +* using `insertTStylingBinding`. +* +* @param tView `TView` where the binding linked list will be stored. +* @param prop Property/key of the binding. +* @param suffix Optional suffix or Sanitization function. +* @param bindingIndex Index of binding associated with the `prop` +* @param isClassBased `true` if `class` change (`false` if `style`) +*/ +function stylingPropertyFirstUpdatePass( + tView: TView, prop: TStylingMapKey, suffix: null, bindingIndex: number, + isClassBased: boolean): void; +function stylingPropertyFirstUpdatePass( + tView: TView, prop: string, suffix: SanitizerFn | string | null | undefined, + bindingIndex: number, isClassBased: boolean): void; +function stylingPropertyFirstUpdatePass( + tView: TView, prop: string | TStylingMapKey, + suffixOrSanitization: SanitizerFn | string | null | undefined, bindingIndex: number, + isClassBased: boolean): void { + ngDevMode && assertFirstUpdatePass(tView); + const tData = tView.data; + if (tData[bindingIndex + 1] === null) { + // The above check is necessary because we don't clear first update pass until first successful + // (no exception) template execution. This prevents the styling instruction from double adding + // itself to the list. + // `getSelectedIndex()` should be here (rather than in instruction) so that it is guarded by the + // if so as not to read unnecessarily. + const tNode = tData[getSelectedIndex() + HEADER_OFFSET] as TNode; + if (hasStylingInputShadow(tNode, isClassBased) && typeof prop === 'object' && + !isInHostBindings(tView, bindingIndex)) { + // typeof prop === 'object' implies that we are either `STYLE_MAP_STYLING_KEY` or + // `CLASS_MAP_STYLING_KEY` which means that we are either `[style]` or `[class]` binding. + // If there is a directive which uses `@Input('style')` or `@Input('class')` than + // we need to neutralize this binding since that directive is shadowing it. + // We turn this into a noop using `IGNORE_DUE_TO_INPUT_SHADOW` + prop = IGNORE_DUE_TO_INPUT_SHADOW; + } + const tStylingKey: TStylingKey = suffixOrSanitization == null ? prop : ({ + key: prop as string, extra: suffixOrSanitization + } as TStylingSuffixKey | TStylingSanitizationKey); + insertTStylingBinding( + tData, tNode, tStylingKey, bindingIndex, isActiveHostElement(), isClassBased); + } +} + +/** + * Tests if the `TNode` has input shadow. + * + * An input shadow is when a directive steals (shadows) the input by using `@Input('style')` or + * `@Input('class')` as input. + * + * @param tNode `TNode` which we would like to see if it has shadow. +* @param isClassBased `true` if `class` (`false` if `style`) + */ +export function hasStylingInputShadow(tNode: TNode, isClassBased: boolean) { + return (tNode.flags & (isClassBased ? TNodeFlags.hasClassInput : TNodeFlags.hasStyleInput)) !== 0; +} + +/** +* Flushes styling into DOM element from the bindings. +* +* The function starts at `LFrame.stylingBindingChanged` and computes new styling information from +* the bindings progressing towards the tail of the list. At the end the resulting style is written +* into the DOM Element. +* +* This function is invoked from: +* 1. Template `advance` instruction. +* 2. HostBinding instruction. +*/ +function flushStylingOnElementExit() { + ngDevMode && assertEqual( + getStyleBindingChanged() > 0 || getClassBindingChanged() > 0, true, + 'Only expected to be here if binding has changed.'); + ngDevMode && + assertEqual( + getCheckNoChangesMode(), false, 'Should never get here during check no changes mode'); + const lView = getLView(); + const tView = lView[TVIEW]; + const tData = tView.data; + const elementIndex = getSelectedIndex() + HEADER_OFFSET; + const tNode = tData[elementIndex] as TNode; + const renderer = lView[RENDERER]; + const element = unwrapRNode(lView[elementIndex]) as RElement; + + const classBindingIndex = getClassBindingChanged(); + if (classBindingIndex > 0) { + const classLastWrittenValueIndex = getTStylingRangeTail(tNode.classBindings) + 1; + ngDevMode && + assertGreaterThan( + classLastWrittenValueIndex, 1, + 'Ignoring `class` binding because there is no `class` metadata associated with the element. ' + + '(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)'); + ngDevMode && + assertLessThan(classLastWrittenValueIndex, lView.length, 'Reading past end of LView'); + const lastValue: string|NO_CHANGE = lView[classLastWrittenValueIndex]; + const newValue = flushStyleBinding(tData, tNode, lView, classBindingIndex, true); + if (lastValue !== newValue) { + if (tNode.type === TNodeType.Element) { + writeAndReconcileClass( + renderer, element, lastValue === NO_CHANGE ? tNode.classes || '' : lastValue as string, + newValue); + } + lView[classLastWrittenValueIndex] = newValue; + } + } + + const styleBindingIndex = getStyleBindingChanged(); + if (styleBindingIndex > 0) { + const styleLastWrittenValueIndex = getTStylingRangeTail(tNode.styleBindings) + 1; + ngDevMode && + assertGreaterThan( + styleLastWrittenValueIndex, 1, + 'Ignoring `style` binding because there is no `style` metadata associated with the element. ' + + '(Was exception thrown during `firstUpdatePass` which prevented the metadata creation?)'); + ngDevMode && + assertLessThan(styleLastWrittenValueIndex, lView.length, 'Reading past end of LView'); + const lastValue: string|NO_CHANGE = lView[styleLastWrittenValueIndex]; + const newValue = flushStyleBinding(tData, tNode, lView, styleBindingIndex, false); + if (lastValue !== newValue) { + if (tNode.type === TNodeType.Element) { + writeAndReconcileStyle( + renderer, element, lastValue === NO_CHANGE ? tNode.styles || '' : lastValue as string, + newValue); + } + lView[styleLastWrittenValueIndex] = newValue; + } + } + ngDevMode && ngDevMode.flushStyling++; +} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/node.ts b/packages/core/src/render3/interfaces/node.ts index 5b0a20cfda..3992291252 100644 --- a/packages/core/src/render3/interfaces/node.ts +++ b/packages/core/src/render3/interfaces/node.ts @@ -77,111 +77,6 @@ export const enum TNodeFlags { * that actually have directives with host bindings. */ hasHostBindings = 0x80, - - /** Bit #9 - This bit is set if the node has initial styling */ - hasInitialStyling = 0x100, - - /** - * Bit #10 - Whether or not there are class-based map bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('class') x` - */ - hasClassMapBindings = 0x200, - - /** - * Bit #11 - Whether or not there are any class-based prop bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('class.name') x` - */ - hasClassPropBindings = 0x400, - - /** - * Bit #12 - whether or not there are any active [class] and [class.name] bindings - */ - hasClassPropAndMapBindings = hasClassMapBindings | hasClassPropBindings, - - /** - * Bit #13 - Whether or not the context contains one or more class-based template bindings. - * - * Examples include: - * 1. `
` - * 2. `
` - */ - hasTemplateClassBindings = 0x800, - - /** - * Bit #14 - Whether or not the context contains one or more class-based host bindings. - * - * Examples include: - * 1. `@HostBinding('class') x` - * 2. `@HostBinding('class.name') x` - */ - hasHostClassBindings = 0x1000, - - /** - * Bit #15 - Whether or not there are two or more sources for a class property in the context. - * - * Examples include: - * 1. prop + prop: `
` - * 2. map + prop: `
` - * 3. map + map: `
` - */ - hasDuplicateClassBindings = 0x2000, - - /** - * Bit #16 - Whether or not there are style-based map bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('style') x` - */ - hasStyleMapBindings = 0x4000, - - /** - * Bit #17 - Whether or not there are any style-based prop bindings present. - * - * Examples include: - * 1. `
` - * 2. `@HostBinding('style.prop') x` - */ - hasStylePropBindings = 0x8000, - - /** - * Bit #18 - whether or not there are any active [style] and [style.prop] bindings - */ - hasStylePropAndMapBindings = hasStyleMapBindings | hasStylePropBindings, - - /** - * Bit #19 - Whether or not the context contains one or more style-based template bindings. - * - * Examples include: - * 1. `
` - * 2. `
` - */ - hasTemplateStyleBindings = 0x10000, - - /** - * Bit #20 - Whether or not the context contains one or more style-based host bindings. - * - * Examples include: - * 1. `@HostBinding('style') x` - * 2. `@HostBinding('style.prop') x` - */ - hasHostStyleBindings = 0x20000, - - /** - * Bit #21 - Whether or not there are two or more sources for a style property in the context. - * - * Examples include: - * 1. prop + prop: `
` - * 2. map + prop: `
` - * 3. map + map: `
` - */ - hasDuplicateStyleBindings = 0x40000, } /** @@ -588,20 +483,8 @@ export interface TNode { * This field will be populated if and when: * * - There are one or more initial styles on an element (e.g. `
`) - * - There are one or more style bindings on an element (e.g. `
`) - * - * If and when there are only initial styles (no bindings) then an instance of `StylingMapArray` - * will be used here. Otherwise an instance of `TStylingContext` will be created when there - * are one or more style bindings on an element. - * - * During element creation this value is likely to be populated with an instance of - * `StylingMapArray` and only when the bindings are evaluated (which happens during - * update mode) then it will be converted to a `TStylingContext` if any style bindings - * are encountered. If and when this happens then the existing `StylingMapArray` value - * will be placed into the initial styling slot in the newly created `TStylingContext`. */ - // TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR. - styles: StylingMapArray|TStylingContext|string|null; + styles: string|null; /** * A collection of all class bindings and/or static class values for an element. @@ -609,20 +492,8 @@ export interface TNode { * This field will be populated if and when: * * - There are one or more initial classes on an element (e.g. `
`) - * - There are one or more class bindings on an element (e.g. `
`) - * - * If and when there are only initial classes (no bindings) then an instance of `StylingMapArray` - * will be used here. Otherwise an instance of `TStylingContext` will be created when there - * are one or more class bindings on an element. - * - * During element creation this value is likely to be populated with an instance of - * `StylingMapArray` and only when the bindings are evaluated (which happens during - * update mode) then it will be converted to a `TStylingContext` if any class bindings - * are encountered. If and when this happens then the existing `StylingMapArray` value - * will be placed into the initial styling slot in the newly created `TStylingContext`. */ - // TODO(misko): `Remove StylingMapArray|TStylingContext|null` in follow up PR. - classes: StylingMapArray|TStylingContext|string|null; + classes: string|null; /** * Stores the head/tail index of the class bindings. @@ -842,3 +713,53 @@ export type TNodeWithLocalRefs = TContainerNode | TElementNode | TElementContain * - `` - `tplRef` should point to the `TemplateRef` instance; */ export type LocalRefExtractor = (tNode: TNodeWithLocalRefs, currentView: LView) => any; + +/** + * Returns `true` if the `TNode` has a directive which has `@Input()` for `class` binding. + * + * ``` + *
+ * ``` + * and + * ``` + * @Directive({ + * }) + * class MyDirective { + * @Input() + * class: string; + * } + * ``` + * + * In the above case it is necessary to write the reconciled styling information into the + * directive's input. + * + * @param tNode + */ +export function hasClassInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasClassInput) !== 0; +} + +/** + * Returns `true` if the `TNode` has a directive which has `@Input()` for `style` binding. + * + * ``` + *
+ * ``` + * and + * ``` + * @Directive({ + * }) + * class MyDirective { + * @Input() + * class: string; + * } + * ``` + * + * In the above case it is necessary to write the reconciled styling information into the + * directive's input. + * + * @param tNode + */ +export function hasStyleInput(tNode: TNode) { + return (tNode.flags & TNodeFlags.hasStyleInput) !== 0; +} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/styling.ts b/packages/core/src/render3/interfaces/styling.ts index cba639d879..fffdb5a8e2 100644 --- a/packages/core/src/render3/interfaces/styling.ts +++ b/packages/core/src/render3/interfaces/styling.ts @@ -642,3 +642,8 @@ export function getTStylingRangeNextDuplicate(tStylingRange: TStylingRange): boo export function setTStylingRangeNextDuplicate(tStylingRange: TStylingRange): TStylingRange { return ((tStylingRange as any as number) | StylingRange.NEXT_DUPLICATE) as any; } + +export function getTStylingRangeTail(tStylingRange: TStylingRange): number { + const next = getTStylingRangeNext(tStylingRange); + return next === 0 ? getTStylingRangePrev(tStylingRange) : next; +} \ No newline at end of file diff --git a/packages/core/src/render3/interfaces/view.ts b/packages/core/src/render3/interfaces/view.ts index fb4e3ffb5e..9a7e4ee0f8 100644 --- a/packages/core/src/render3/interfaces/view.ts +++ b/packages/core/src/render3/interfaces/view.ts @@ -471,7 +471,17 @@ export interface TView { /** Whether or not this template has been processed in creation mode. */ firstCreatePass: boolean; - /** Whether or not the first update for this template has been processed. */ + /** + * Whether or not this template has been processed in update mode (e.g. change detected) + * + * `firstUpdatePass` is used by styling to set up `TData` to contain metadata about the styling + * instructions. (Mainly to build up a linked list of styling priority order.) + * + * Typically this function gets cleared after first execution. If exception is thrown then this + * flag can remain turned un until there is first successful (no exception) pass. This means that + * individual styling instructions keep track of if they have already been added to the linked + * list to prevent double adding. + */ firstUpdatePass: boolean; /** Static data equivalent of LView.data[]. Contains TNodes, PipeDefInternal or TI18n. */ diff --git a/packages/core/src/render3/state.ts b/packages/core/src/render3/state.ts index cad0ce8c71..4aaa4f751d 100644 --- a/packages/core/src/render3/state.ts +++ b/packages/core/src/render3/state.ts @@ -7,8 +7,7 @@ */ import {StyleSanitizeFn} from '../sanitization/style_sanitizer'; -import {assertDefined, assertEqual} from '../util/assert'; - +import {assertDefined, assertEqual, assertGreaterThan} from '../util/assert'; import {assertLViewOrUndefined} from './assert'; import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {TNode} from './interfaces/node'; @@ -44,7 +43,7 @@ interface LFrame { /** * Used to set the parent property when nodes are created and track query results. * - * This is used in conjection with `isParent`. + * This is used in conjunction with `isParent`. */ previousOrParentTNode: TNode; @@ -94,11 +93,6 @@ interface LFrame { currentSanitizer: StyleSanitizeFn|null; - /** - * Used when processing host bindings. - */ - currentDirectiveDef: DirectiveDef|ComponentDef|null; - /** * The root index from which pure function instructions should calculate their binding * indices. In component views, this is TView.bindingStartIndex. In a host binding @@ -111,6 +105,18 @@ interface LFrame { * We iterate over the list of Queries and increment current query index at every step. */ currentQueryIndex: number; + + /** + * Stores the index of the style binding which changed first. + * + * A change in styling binding implies that all bindings starting with this index need to be + * recomputed. See: `flushStylingOnElementExit` and `markStylingBindingDirty` functions for + * details. + * + * If this value is set then `flushStylingOnElementExit` needs to execute during the `advance` + * instruction to update the styling. + */ + stylingBindingChanged: number; } /** @@ -259,17 +265,15 @@ export function getLView(): LView { * function or when all host bindings are processed for an element). */ export const enum ActiveElementFlags { - Initial = 0b00, - RunExitFn = 0b01, + HostMode = 0b1, + RunExitFn = 0b1, Size = 1, } -/** - * Sets a flag is for the active element. - */ -function setActiveElementFlag(flag: ActiveElementFlags) { - instructionState.lFrame.selectedIndex |= flag; +export function isActiveHostElement(): boolean { + return (instructionState.lFrame.selectedIndex & ActiveElementFlags.HostMode) === + ActiveElementFlags.HostMode; } /** @@ -279,17 +283,23 @@ function setActiveElementFlag(flag: ActiveElementFlags) { * @param elementIndex the element index value for the host element where * the directive/component instance lives */ -export function setActiveHostElement(elementIndex: number | null) { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } - setSelectedIndex(elementIndex === null ? -1 : elementIndex); - instructionState.lFrame.activeDirectiveId = 0; +export function setActiveHostElement(elementIndex: number) { + executeElementExitFn(); + setSelectedIndex(elementIndex); + instructionState.lFrame.selectedIndex |= ActiveElementFlags.HostMode; +} + +export function clearActiveHostElement() { + executeElementExitFn(); + setSelectedIndex(-1); } export function executeElementExitFn() { - instructionState.elementExitFn !(); - instructionState.lFrame.selectedIndex &= ~ActiveElementFlags.RunExitFn; + const lFrame = instructionState.lFrame; + if (lFrame.stylingBindingChanged !== 0) { + instructionState.elementExitFn !(); + lFrame.stylingBindingChanged = 0; + } } /** @@ -306,7 +316,6 @@ export function executeElementExitFn() { * @param fn */ export function setElementExitFn(fn: () => void): void { - setActiveElementFlag(ActiveElementFlags.RunExitFn); if (instructionState.elementExitFn === null) { instructionState.elementExitFn = fn; } @@ -354,6 +363,7 @@ export function getContextLView(): LView { } export function getCheckNoChangesMode(): boolean { + // TODO(misko): remove this from the LView since it is ngDevMode=true mode only. return instructionState.checkNoChangesMode; } @@ -430,8 +440,6 @@ export function enterDI(newView: LView, tNode: TNode) { newLFrame.elementDepthCount = DEV_MODE_VALUE; newLFrame.currentNamespace = DEV_MODE_VALUE; newLFrame.currentSanitizer = DEV_MODE_VALUE; - newLFrame.currentDirectiveDef = DEV_MODE_VALUE; - newLFrame.activeDirectiveId = DEV_MODE_VALUE; newLFrame.bindingRootIndex = DEV_MODE_VALUE; newLFrame.currentQueryIndex = DEV_MODE_VALUE; } @@ -471,11 +479,10 @@ export function enterView(newView: LView, tNode: TNode | null): void { newLFrame.elementDepthCount = 0; newLFrame.currentNamespace = null; newLFrame.currentSanitizer = null; - newLFrame.currentDirectiveDef = null; - newLFrame.activeDirectiveId = 0; newLFrame.bindingRootIndex = -1; newLFrame.bindingIndex = newView === null ? -1 : newView[TVIEW].bindingStartIndex; newLFrame.currentQueryIndex = 0; + newLFrame.stylingBindingChanged = 0; } /** @@ -498,22 +505,19 @@ function createLFrame(parent: LFrame | null): LFrame { elementDepthCount: 0, // currentNamespace: null, // currentSanitizer: null, // - currentDirectiveDef: null, // - activeDirectiveId: 0, // bindingRootIndex: -1, // bindingIndex: -1, // currentQueryIndex: 0, // parent: parent !, // child: null, // + stylingBindingChanged: 0, // }; parent !== null && (parent.child = lFrame); // link the new LFrame for reuse. return lFrame; } export function leaveViewProcessExit() { - if (hasActiveElementFlag(ActiveElementFlags.RunExitFn)) { - executeElementExitFn(); - } + executeElementExitFn(); leaveView(); } @@ -539,7 +543,7 @@ function walkUpViews(nestingLevel: number, currentView: LView): LView { } /** - * Gets the most recent index passed to {@link select} + * Gets the currently selected element index. * * Used with {@link property} instruction (and more in the future) to identify the index in the * current `LView` to act on. @@ -616,3 +620,48 @@ export function getCurrentStyleSanitizer() { const lFrame = instructionState.lFrame; return lFrame === null ? null : lFrame.currentSanitizer; } + +/** + * Used for encoding both Class and Style index into `LFrame.stylingBindingChanged`. + */ +const enum BindingChanged { + CLASS_SHIFT = 16, + STYLE_MASK = 0xFFFF, +} + +/** + * Store the first binding location from where the style flushing should start. + * + * This function stores the first binding location. Any subsequent binding changes are ignored as + * they are downstream from this change and will be picked up once the flushing starts traversing + * forward. + * + * Because flushing for template and flushing for host elements are separate, we don't need to worry + * about the fact that they will be out of order. + * + * @param bindingIndex Index of binding location. This will be a binding location from which the + * flushing of styling should start. + * @param isClassBased `true` if `class` change (`false` if `style`) + */ +export function markStylingBindingDirty(bindingIndex: number, isClassBased: boolean) { + ngDevMode && assertGreaterThan(bindingIndex, 0, 'expected valid binding index changed'); + ngDevMode && + assertEqual( + getCheckNoChangesMode(), false, 'Should never get here during check no changes mode'); + const lFrame = instructionState.lFrame; + const stylingBindingChanged = lFrame.stylingBindingChanged; + const stylingBindingChangedExtracted = isClassBased ? + stylingBindingChanged >> BindingChanged.CLASS_SHIFT : + stylingBindingChanged & BindingChanged.STYLE_MASK; + if (stylingBindingChangedExtracted === 0) { + lFrame.stylingBindingChanged = stylingBindingChanged | + (isClassBased ? bindingIndex << BindingChanged.CLASS_SHIFT : bindingIndex); + } +} + +export function getClassBindingChanged() { + return instructionState.lFrame.stylingBindingChanged >> BindingChanged.CLASS_SHIFT; +} +export function getStyleBindingChanged() { + return instructionState.lFrame.stylingBindingChanged & BindingChanged.STYLE_MASK; +} \ No newline at end of file diff --git a/packages/core/src/render3/styling/class_differ.ts b/packages/core/src/render3/styling/class_differ.ts index 09375e08b1..24338ecc82 100644 --- a/packages/core/src/render3/styling/class_differ.ts +++ b/packages/core/src/render3/styling/class_differ.ts @@ -6,11 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ +import {assertNotEqual} from '../../util/assert'; import {CharCode} from '../../util/char_code'; - +import {concatStringsWithSpace} from '../../util/stringify'; import {consumeWhitespace, getLastParsedKey, parseClassName, parseClassNameNext} from './styling_parser'; + /** * Computes the diff between two class-list strings. * @@ -92,17 +94,6 @@ export function processClassToken( } } -/** - * Removes a class from a `className` string. - * - * @param className A string containing classes (whitespace separated) - * @param classToRemove A class name to remove from the `className` - * @returns a new class-list which does not have `classToRemove` - */ -export function removeClass(className: string, classToRemove: string): string { - return toggleClass(className, classToRemove, false); -} - /** * Toggles a class in `className` string. * @@ -121,17 +112,23 @@ export function toggleClass(className: string, classToToggle: string, toggle: bo start = classIndexOf(className, classToToggle, start); if (start === -1) { if (toggle === true) { - className = className === '' ? classToToggle : className + ' ' + classToToggle; + className = concatStringsWithSpace(className, classToToggle); } break; } - const removeLength = classToToggle.length; if (toggle === true) { // we found it and we should have it so just return return className; } else { + const length = classToToggle.length; // Cut out the class which should be removed. - const endWhitespace = consumeWhitespace(className, start + removeLength, end); + const endWhitespace = consumeWhitespace(className, start + length, end); + if (endWhitespace === end) { + // If we are the last token then we need back search trailing whitespace. + while (start > 0 && className.charCodeAt(start - 1) <= CharCode.SPACE) { + start--; + } + } className = className.substring(0, start) + className.substring(endWhitespace, end); end = className.length; } @@ -151,15 +148,16 @@ export function toggleClass(className: string, classToToggle: string, toggle: bo */ export function classIndexOf( className: string, classToSearch: string, startingIndex: number): number { + ngDevMode && assertNotEqual(classToSearch, '', 'can not look for "" string.'); let end = className.length; while (true) { const foundIndex = className.indexOf(classToSearch, startingIndex); if (foundIndex === -1) return foundIndex; if (foundIndex === 0 || className.charCodeAt(foundIndex - 1) <= CharCode.SPACE) { // Ensure that it has leading whitespace - const removeLength = classToSearch.length; - if (foundIndex + removeLength === end || - className.charCodeAt(foundIndex + removeLength) <= CharCode.SPACE) { + const length = classToSearch.length; + if (foundIndex + length === end || + className.charCodeAt(foundIndex + length) <= CharCode.SPACE) { // Ensure that it has trailing whitespace return foundIndex; } diff --git a/packages/core/src/render3/styling/reconcile.ts b/packages/core/src/render3/styling/reconcile.ts index cd8ef0057b..9d6c78bfd3 100644 --- a/packages/core/src/render3/styling/reconcile.ts +++ b/packages/core/src/render3/styling/reconcile.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {assertDefined, assertString} from '../../util/assert'; import {ProceduralRenderer3, RElement, Renderer3, isProceduralRenderer} from '../interfaces/renderer'; import {computeClassChanges} from './class_differ'; import {computeStyleChanges} from './style_differ'; @@ -43,14 +44,11 @@ import {computeStyleChanges} from './style_differ'; */ export function writeAndReconcileClass( renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void { + ngDevMode && assertDefined(element, 'Expecting DOM element'); + ngDevMode && assertString(expectedValue, '\'oldValue\' should be a string'); + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); if (element.className === expectedValue) { - // This is the simple/fast case where no one has written into element without our knowledge. - if (isProceduralRenderer(renderer)) { - renderer.setAttribute(element, 'class', newValue); - } else { - element.className = newValue; - } - ngDevMode && ngDevMode.rendererSetClassName++; + writeDirectClass(renderer, element, newValue); } else { // The expected value is not the same as last value. Something changed the DOM element without // our knowledge so we need to do reconciliation instead. @@ -58,6 +56,32 @@ export function writeAndReconcileClass( } } +/** + * Write `className` to `RElement`. + * + * This function does direct write without any reconciliation. Used for writing initial values, so + * that static styling values do not pull in the style parser. + * + * @param renderer Renderer to use + * @param element The element which needs to be updated. + * @param newValue The new class list to write. + */ +export function writeDirectClass(renderer: Renderer3, element: RElement, newValue: string) { + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); + if (isProceduralRenderer(renderer)) { + if (newValue === '') { + // There are tests in `google3` which expect `element.getAttribute('class')` to be `null`. + // TODO(commit): add test case + renderer.removeAttribute(element, 'class'); + } else { + renderer.setAttribute(element, 'class', newValue); + } + } else { + element.className = newValue; + } + ngDevMode && ngDevMode.rendererSetClassName++; +} + /** * Writes new `cssText` value in the DOM node. * @@ -88,15 +112,12 @@ export function writeAndReconcileClass( */ export function writeAndReconcileStyle( renderer: Renderer3, element: RElement, expectedValue: string, newValue: string): void { - const style = (element as HTMLElement).style; - if (style != null && style.cssText === expectedValue) { - // This is the simple/fast case where no one has written into element without our knowledge. - if (isProceduralRenderer(renderer)) { - renderer.setAttribute(element, 'style', newValue); - } else { - style.cssText = newValue; - } - ngDevMode && ngDevMode.rendererCssText++; + ngDevMode && assertDefined(element, 'Expecting DOM element'); + ngDevMode && assertString(expectedValue, '\'expectedValue\' should be a string'); + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); + const style = expectedValue === null ? null : (element as HTMLElement).style; + if (expectedValue === null || style != null && (style !.cssText === expectedValue)) { + writeDirectStyle(renderer, element, newValue); } else { // The expected value is not the same as last value. Something changed the DOM element without // our knowledge so we need to do reconciliation instead. @@ -104,6 +125,26 @@ export function writeAndReconcileStyle( } } +/** + * Write `cssText` to `RElement`. + * + * This function does direct write without any reconciliation. Used for writing initial values, so + * that static styling values do not pull in the style parser. + * + * @param renderer Renderer to use + * @param element The element which needs to be updated. + * @param newValue The new class list to write. + */ +export function writeDirectStyle(renderer: Renderer3, element: RElement, newValue: string) { + ngDevMode && assertString(newValue, '\'newValue\' should be a string'); + if (isProceduralRenderer(renderer)) { + renderer.setAttribute(element, 'style', newValue); + } else { + (element as HTMLElement).style.cssText = newValue; + } + ngDevMode && ngDevMode.rendererSetStyle++; +} + /** * Writes to `classNames` by computing the difference between `oldValue` and `newValue` and using * `classList.add` and `classList.remove`. diff --git a/packages/core/src/render3/styling/static_styling.ts b/packages/core/src/render3/styling/static_styling.ts index 845b16ae22..8944d8aae7 100644 --- a/packages/core/src/render3/styling/static_styling.ts +++ b/packages/core/src/render3/styling/static_styling.ts @@ -23,8 +23,8 @@ import {getLView} from '../state'; export function computeStaticStyling(tNode: TNode, attrs: TAttributes): void { ngDevMode && assertFirstCreatePass( getLView()[TVIEW], 'Expecting to be called in first template pass only'); - let styles: string|null = tNode.styles as string | null; - let classes: string|null = tNode.classes as string | null; + let styles: string|null = tNode.styles; + let classes: string|null = tNode.classes; let mode: AttributeMarker|0 = 0; for (let i = 0; i < attrs.length; i++) { const value = attrs[i]; diff --git a/packages/core/src/render3/styling/style_binding_list.ts b/packages/core/src/render3/styling/style_binding_list.ts index 1922a52e63..f73e28c33a 100644 --- a/packages/core/src/render3/styling/style_binding_list.ts +++ b/packages/core/src/render3/styling/style_binding_list.ts @@ -6,13 +6,18 @@ * found in the LICENSE file at https://angular.io/license */ +import {unwrapSafeValue} from '../../sanitization/bypass'; import {stylePropNeedsSanitization, ɵɵsanitizeStyle} from '../../sanitization/sanitization'; -import {assertEqual, throwError} from '../../util/assert'; +import {assertEqual, assertString, throwError} from '../../util/assert'; +import {CharCode} from '../../util/char_code'; +import {concatStringsWithSpace} from '../../util/stringify'; +import {assertFirstUpdatePass} from '../assert'; import {TNode} from '../interfaces/node'; import {SanitizerFn} from '../interfaces/sanitization'; import {TStylingKey, TStylingMapKey, TStylingRange, getTStylingRangeNext, getTStylingRangePrev, getTStylingRangePrevDuplicate, setTStylingRangeNext, setTStylingRangeNextDuplicate, setTStylingRangePrev, setTStylingRangePrevDuplicate, toTStylingRange} from '../interfaces/styling'; import {LView, TData, TVIEW} from '../interfaces/view'; import {getLView} from '../state'; +import {NO_CHANGE} from '../tokens'; import {splitClassList, toggleClass} from './class_differ'; import {StyleChangesMap, parseKeyValue, removeStyle} from './style_differ'; import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseStyleNext} from './styling_parser'; @@ -172,7 +177,7 @@ import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseS * NOTE: See `should support example in 'tnode_linked_list.ts' documentation` in * `tnode_linked_list_spec.ts` for working example. */ - +let __unused_const_as_closure_does_not_like_standalone_comment_blocks__: undefined; /** * Insert new `tStyleValue` at `TData` and link existing style bindings such that we maintain linked @@ -197,9 +202,7 @@ import {getLastParsedKey, parseClassName, parseClassNameNext, parseStyle, parseS export function insertTStylingBinding( tData: TData, tNode: TNode, tStylingKey: TStylingKey, index: number, isHostBinding: boolean, isClassBinding: boolean): void { - ngDevMode && assertEqual( - getLView()[TVIEW].firstUpdatePass, true, - 'Should be called during \'firstUpdatePass` only.'); + ngDevMode && assertFirstUpdatePass(getLView()[TVIEW]); let tBindings = isClassBinding ? tNode.classBindings : tNode.styleBindings; let tmplHead = getTStylingRangePrev(tBindings); let tmplTail = getTStylingRangeNext(tBindings); @@ -255,7 +258,7 @@ export function insertTStylingBinding( // Now we need to update / compute the duplicates. // Starting with our location search towards head (least priority) markDuplicates( - tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) as string, true, + tData, tStylingKey, index, (isClassBinding ? tNode.classes : tNode.styles) || '', true, isClassBinding); markDuplicates(tData, tStylingKey, index, '', false, isClassBinding); @@ -347,6 +350,7 @@ function markDuplicates( cursor = isPrevDir ? getTStylingRangePrev(tStyleRangeAtCursor) : getTStylingRangeNext(tStyleRangeAtCursor); } + // We also need to process the static values. if (staticValues !== '' && // If we have static values to search !foundDuplicate // If we have duplicate don't bother since we are already marked as // duplicate @@ -355,6 +359,8 @@ function markDuplicates( // if we are a Map (and we have statics) we must assume duplicate foundDuplicate = true; } else if (staticValues != null) { + // If we found non-map then we iterate over its keys to determine if any of them match ours + // If we find a match than we mark it as duplicate. for (let i = isClassBinding ? parseClassName(staticValues) : parseStyle(staticValues); // i >= 0; // i = isClassBinding ? parseClassNameNext(staticValues, i) : @@ -367,6 +373,7 @@ function markDuplicates( } } if (foundDuplicate) { + // if we found a duplicate, than mark ourselves. tData[index + 1] = isPrevDir ? setTStylingRangePrevDuplicate(tStylingAtIndex) : setTStylingRangeNextDuplicate(tStylingAtIndex); } @@ -392,18 +399,21 @@ export function flushStyleBinding( // value and look up the previous concatenation as a starting point going forward. const lastUnchangedValueIndex = getTStylingRangePrev(tStylingRangeAtIndex); let text = lastUnchangedValueIndex === 0 ? - ((isClassBinding ? tNode.classes : tNode.styles) as string) : - lView[lastUnchangedValueIndex + 1] as string; + (isClassBinding ? tNode.classes : tNode.styles) : + lView[lastUnchangedValueIndex + 1] as string | NO_CHANGE; + if (text === null || text === NO_CHANGE) text = ''; + ngDevMode && assertString(text, 'Last unchanged value should be a string'); let cursor = index; while (cursor !== 0) { const value = lView[cursor]; const key = tData[cursor] as TStylingKey; const stylingRange = tData[cursor + 1] as TStylingRange; lView[cursor + 1] = text = appendStyling( - text, key, value, null, getTStylingRangePrevDuplicate(stylingRange), isClassBinding); + text as string, key, value, null, getTStylingRangePrevDuplicate(stylingRange), + isClassBinding); cursor = getTStylingRangeNext(stylingRange); } - return text; + return text as string; } @@ -452,17 +462,27 @@ export function appendStyling( if (hasPreviousDuplicate) { text = toggleClass(text, stylingKey as string, !!value); } else if (value) { - text = text === '' ? stylingKey as string : text + ' ' + stylingKey; + text = concatStringsWithSpace(text, stylingKey as string); } } else { + if (value === undefined) { + // If undefined than treat it as if we have no value. This means that we will fallback to the + // previous value (if any). + // `
` => `width: 10px`. + return text; + } if (hasPreviousDuplicate) { text = removeStyle(text, key); } - const keyValue = - key + ': ' + (typeof suffixOrSanitizer === 'function' ? - suffixOrSanitizer(value) : - (suffixOrSanitizer == null ? value : value + suffixOrSanitizer)); - text = text === '' ? keyValue : text + '; ' + keyValue; + if (value !== false && value !== null) { + // `
` => ``. (remove it) + // `
` => ``. (remove it) + value = typeof suffixOrSanitizer === 'function' ? suffixOrSanitizer(value) : + unwrapSafeValue(value); + const keyValue = key + ': ' + + (typeof suffixOrSanitizer === 'string' ? value + suffixOrSanitizer : value) + ';'; + text = concatStringsWithSpace(text, keyValue); + } } return text; } @@ -489,7 +509,10 @@ export const CLASS_MAP_STYLING_KEY: TStylingMapKey = { } else if (typeof value === 'object') { // We support maps for (let key in value) { - text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true); + if (key !== '') { + // We have to guard for `""` empty string as key since it will break search and replace. + text = appendStyling(text, key, value[key], null, hasPreviousDuplicate, true); + } } } else if (typeof value === 'string') { // We support strings @@ -500,7 +523,7 @@ export const CLASS_MAP_STYLING_KEY: TStylingMapKey = { changes.forEach((_, key) => text = appendStyling(text, key, true, null, true, true)); } else { // No duplicates, just append it. - text = text === '' ? value : text + ' ' + value; + text = concatStringsWithSpace(text, value); } } else { // All other cases are not supported. @@ -531,9 +554,12 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = { } else if (typeof value === 'object') { // We support maps for (let key in value) { - text = appendStyling( - text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null, - hasPreviousDuplicate, false); + if (key !== '') { + // We have to guard for `""` empty string as key since it will break search and replace. + text = appendStyling( + text, key, value[key], stylePropNeedsSanitization(key) ? ɵɵsanitizeStyle : null, + hasPreviousDuplicate, false); + } } } else if (typeof value === 'string') { // We support strings @@ -548,7 +574,10 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = { true, false)); } else { // No duplicates, just append it. - text = text === '' ? value : text + '; ' + value; + if (value.charCodeAt(value.length - 1) !== CharCode.SEMI_COLON) { + value += ';'; + } + text = concatStringsWithSpace(text, value); } } else { // All other cases are not supported. @@ -557,3 +586,14 @@ export const STYLE_MAP_STYLING_KEY: TStylingMapKey = { return text; } }; + + +/** + * If we have `
` such that `my-dir` has `@Input('class')`, the `my-dir` captures + * the `[class]` binding, so that it no longer participates in the style bindings. For this case + * we use `IGNORE_DUE_TO_INPUT_SHADOW` so that `flushStyleBinding` ignores it. + */ +export const IGNORE_DUE_TO_INPUT_SHADOW: TStylingMapKey = { + key: null, + extra: (text: string, value: any, hasPreviousDuplicate: boolean): string => { return text;} +}; \ No newline at end of file diff --git a/packages/core/src/render3/styling/style_differ.ts b/packages/core/src/render3/styling/style_differ.ts index 80459b7ac4..1238d01ec4 100644 --- a/packages/core/src/render3/styling/style_differ.ts +++ b/packages/core/src/render3/styling/style_differ.ts @@ -6,7 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {getLastParsedKey, getLastParsedValue, getLastParsedValueEnd, parseStyle, parseStyleNext, resetParserState} from './styling_parser'; +import {concatStringsWithSpace} from '../../util/stringify'; +import {consumeWhitespace, getLastParsedKey, getLastParsedValue, parseStyle, parseStyleNext, resetParserState} from './styling_parser'; /** * Stores changes to Style values. @@ -111,18 +112,20 @@ export function removeStyle(cssText: string, styleToRemove: string): string { for (let i = parseStyle(cssText); i >= 0; i = parseStyleNext(cssText, i)) { const key = getLastParsedKey(cssText); if (key === styleToRemove) { + // Consume any remaining whitespace. + i = consumeWhitespace(cssText, i, cssText.length); if (lastValueEnd === 0) { cssText = cssText.substring(i); i = 0; } else if (i === cssText.length) { return cssText.substring(0, lastValueEnd); } else { - cssText = cssText.substring(0, lastValueEnd) + '; ' + cssText.substring(i); - i = lastValueEnd + 2; // 2 is for '; '.length(so that we skip the separator) + cssText = concatStringsWithSpace(cssText.substring(0, lastValueEnd), cssText.substring(i)); + i = lastValueEnd + 1; // 1 is for ';'.length(so that we skip the separator) } resetParserState(cssText); } - lastValueEnd = getLastParsedValueEnd(); + lastValueEnd = i; } return cssText; } diff --git a/packages/core/src/render3/styling/styling_parser.ts b/packages/core/src/render3/styling/styling_parser.ts index be097b9d8f..84f711049e 100644 --- a/packages/core/src/render3/styling/styling_parser.ts +++ b/packages/core/src/render3/styling/styling_parser.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ +import {assertEqual, throwError} from '../../util/assert'; import {CharCode} from '../../util/char_code'; /** @@ -142,17 +143,16 @@ export function parseStyle(text: string): number { */ export function parseStyleNext(text: string, startIndex: number): number { const end = parserState.textEnd; - if (end === startIndex) { + let index = parserState.key = consumeWhitespace(text, startIndex, end); + if (end === index) { // we reached an end so just quit return -1; } - let index = parserState.keyEnd = consumeStyleKey(text, parserState.key = startIndex, end); - index = parserState.value = consumeSeparatorWithWhitespace(text, index, end, CharCode.COLON); + index = parserState.keyEnd = consumeStyleKey(text, index, end); + index = consumeSeparator(text, index, end, CharCode.COLON); + index = parserState.value = consumeWhitespace(text, index, end); index = parserState.valueEnd = consumeStyleValue(text, index, end); - if (ngDevMode && parserState.value === parserState.valueEnd) { - throw malformedStyleError(text, index); - } - return consumeSeparatorWithWhitespace(text, index, end, CharCode.SEMI_COLON); + return consumeSeparator(text, index, end, CharCode.SEMI_COLON); } /** @@ -167,15 +167,6 @@ export function resetParserState(text: string): void { parserState.textEnd = text.length; } -/** - * Retrieves tha `valueEnd` from the parser global state. - * - * See: `ParserState`. - */ -export function getLastParsedValueEnd(): number { - return parserState.valueEnd; -} - /** * Returns index of next non-whitespace character. * @@ -233,16 +224,15 @@ export function consumeStyleKey(text: string, startIndex: number, endIndex: numb * @param endIndex Ending index of character where the scan should end. * @returns Index after separator and surrounding whitespace. */ -export function consumeSeparatorWithWhitespace( +export function consumeSeparator( text: string, startIndex: number, endIndex: number, separator: number): number { startIndex = consumeWhitespace(text, startIndex, endIndex); if (startIndex < endIndex) { if (ngDevMode && text.charCodeAt(startIndex) !== separator) { - throw expectingError(text, String.fromCharCode(separator), startIndex); + malformedStyleError(text, String.fromCharCode(separator), startIndex); } startIndex++; } - startIndex = consumeWhitespace(text, startIndex, endIndex); return startIndex; } @@ -310,18 +300,14 @@ export function consumeQuotedText( ch1 = ch; } } - throw ngDevMode ? expectingError(text, String.fromCharCode(quoteCharCode), endIndex) : + throw ngDevMode ? malformedStyleError(text, String.fromCharCode(quoteCharCode), endIndex) : new Error(); } -function expectingError(text: string, expecting: string, index: number) { - return new Error( - `Expecting '${expecting}' at location ${index} in string '` + text.substring(0, index) + - '[>>' + text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.'); -} - -function malformedStyleError(text: string, index: number) { - return new Error( +function malformedStyleError(text: string, expecting: string, index: number): never { + ngDevMode && assertEqual(typeof text === 'string', true, 'String expected here'); + throw throwError( `Malformed style at location ${index} in string '` + text.substring(0, index) + '[>>' + - text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + '\'.'); + text.substring(index, index + 1) + '<<]' + text.substr(index + 1) + + `'. Expecting '${expecting}'.`); } diff --git a/packages/core/src/render3/tokens.ts b/packages/core/src/render3/tokens.ts index 2ef57c0ab6..9c53282f2e 100644 --- a/packages/core/src/render3/tokens.ts +++ b/packages/core/src/render3/tokens.ts @@ -8,8 +8,9 @@ export interface NO_CHANGE { // This is a brand that ensures that this type can never match anything else - brand: 'NO_CHANGE'; + __brand__: 'NO_CHANGE'; } /** A special value which designates that a value has not changed. */ -export const NO_CHANGE = {} as NO_CHANGE; +export const NO_CHANGE: NO_CHANGE = + (typeof ngDevMode === 'undefined' || ngDevMode) ? {__brand__: 'NO_CHANGE'} : ({} as NO_CHANGE); diff --git a/packages/core/src/sanitization/sanitization.ts b/packages/core/src/sanitization/sanitization.ts index 1108eee5f5..653f5a5b69 100644 --- a/packages/core/src/sanitization/sanitization.ts +++ b/packages/core/src/sanitization/sanitization.ts @@ -187,6 +187,12 @@ export function ɵɵsanitizeUrlOrResourceUrl(unsafeUrl: any, tag: string, prop: */ export const ɵɵdefaultStyleSanitizer = (function(prop: string, value: string|null, mode?: StyleSanitizeMode): string | boolean | null { + if (value === undefined && mode === undefined) { + // This is a workaround for the fact that `StyleSanitizeFn` should not exist once PR#34480 + // lands. For now the `StyleSanitizeFn` and should act like `(value: any) => string` as a + // work around. + return ɵɵsanitizeStyle(prop); + } mode = mode || StyleSanitizeMode.ValidateAndSanitize; let doSanitizeValue = true; if (mode & StyleSanitizeMode.ValidateProperty) { @@ -201,9 +207,11 @@ export const ɵɵdefaultStyleSanitizer = } as StyleSanitizeFn); export function stylePropNeedsSanitization(prop: string): boolean { - return prop === 'background-image' || prop === 'background' || prop === 'border-image' || - prop === 'filter' || prop === 'list-style' || prop === 'list-style-image' || - prop === 'clip-path'; + return prop === 'background-image' || prop === 'backgroundImage' || prop === 'background' || + prop === 'border-image' || prop === 'borderImage' || prop === 'border-image-source' || + prop === 'borderImageSource' || prop === 'filter' || prop === 'list-style' || + prop === 'listStyle' || prop === 'list-style-image' || prop === 'listStyleImage' || + prop === 'clip-path' || prop === 'clipPath'; } export function validateAgainstEventProperties(name: string) { diff --git a/packages/core/src/util/stringify.ts b/packages/core/src/util/stringify.ts index 0f789c8082..11c5f331f8 100644 --- a/packages/core/src/util/stringify.ts +++ b/packages/core/src/util/stringify.ts @@ -37,7 +37,6 @@ export function stringify(token: any): string { return newLineIndex === -1 ? res : res.substring(0, newLineIndex); } - /** * Concatenates two strings with separator, allocating new strings only when necessary. * @@ -50,4 +49,4 @@ export function concatStringsWithSpace(before: string | null, after: string | nu return (before == null || before === '') ? (after === null ? '' : after) : ((after == null || after === '') ? before : before + ' ' + after); -} \ No newline at end of file +} diff --git a/packages/core/test/acceptance/change_detection_spec.ts b/packages/core/test/acceptance/change_detection_spec.ts index 125ba447db..a5d6b41b2e 100644 --- a/packages/core/test/acceptance/change_detection_spec.ts +++ b/packages/core/test/acceptance/change_detection_spec.ts @@ -1536,16 +1536,15 @@ describe('change detection', () => { }); it('should include style prop name in case of style binding', () => { - const message = ivyEnabled ? - `Previous value for 'style.color': 'red'. Current value: 'green'` : - `Previous value: 'color: red'. Current value: 'color: green'`; + const message = ivyEnabled ? `Previous value for 'color': 'red'. Current value: 'green'` : + `Previous value: 'color: red'. Current value: 'color: green'`; expect(() => initWithTemplate('
')) .toThrowError(new RegExp(message)); }); it('should include class name in case of class binding', () => { const message = ivyEnabled ? - `Previous value for 'class.someClass': 'true'. Current value: 'false'` : + `Previous value for 'someClass': 'true'. Current value: 'false'` : `Previous value: 'someClass: true'. Current value: 'someClass: false'`; expect(() => initWithTemplate('
')) .toThrowError(new RegExp(message)); @@ -1574,16 +1573,15 @@ describe('change detection', () => { }); it('should include style prop name in case of host style bindings', () => { - const message = ivyEnabled ? - `Previous value for 'style.color': 'red'. Current value: 'green'` : - `Previous value: 'color: red'. Current value: 'color: green'`; + const message = ivyEnabled ? `Previous value for 'color': 'red'. Current value: 'green'` : + `Previous value: 'color: red'. Current value: 'color: green'`; expect(() => initWithHostBindings({'[style.color]': 'unstableColorExpression'})) .toThrowError(new RegExp(message)); }); it('should include class name in case of host class bindings', () => { const message = ivyEnabled ? - `Previous value for 'class.someClass': 'true'. Current value: 'false'` : + `Previous value for 'someClass': 'true'. Current value: 'false'` : `Previous value: 'someClass: true'. Current value: 'someClass: false'`; expect(() => initWithHostBindings({'[class.someClass]': 'unstableBooleanExpression'})) .toThrowError(new RegExp(message)); diff --git a/packages/core/test/acceptance/discover_utils_spec.ts b/packages/core/test/acceptance/discover_utils_spec.ts index 9aa1e1d7b5..e357331bb8 100644 --- a/packages/core/test/acceptance/discover_utils_spec.ts +++ b/packages/core/test/acceptance/discover_utils_spec.ts @@ -10,6 +10,8 @@ import {Component, Directive, HostBinding, InjectionToken, ViewChild} from '@ang import {isLView} from '@angular/core/src/render3/interfaces/type_checks'; import {CONTEXT} from '@angular/core/src/render3/interfaces/view'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {getElementStyles} from '@angular/core/testing/src/styling'; +import {expect} from '@angular/core/testing/src/testing_internal'; import {onlyInIvy} from '@angular/private/testing'; import {getHostElement, markDirty} from '../../src/render3/index'; @@ -473,11 +475,10 @@ onlyInIvy('Ivy-specific utilities').describe('discovery utils deprecated', () => const childDebug = getDebugNode(child) !; expect(childDebug.native).toBe(child); - expect(childDebug.styles).toBeTruthy(); - - const styles = childDebug.styles !.values; - expect(styles['width']).toEqual('200px'); - expect(styles['height']).toEqual('400px'); + expect(getElementStyles(child)).toEqual({ + width: '200px', + height: '400px', + }); }); }); }); diff --git a/packages/core/test/acceptance/host_binding_spec.ts b/packages/core/test/acceptance/host_binding_spec.ts index f6a018dadb..6de8c5c1e5 100644 --- a/packages/core/test/acceptance/host_binding_spec.ts +++ b/packages/core/test/acceptance/host_binding_spec.ts @@ -254,7 +254,7 @@ describe('host bindings', () => { } TestBed.configureTestingModule( - {declarations: [MyApp, ParentDir, ChildDir, SiblingDir]}); + {declarations: [MyApp, ParentDir, SiblingDir, ChildDir]}); const fixture = TestBed.createComponent(MyApp); const element = fixture.nativeElement; fixture.detectChanges(); @@ -262,10 +262,9 @@ describe('host bindings', () => { const childElement = element.querySelector('div'); // width/height values were set in all directives, but the sub-class directive - // (ChildDir) - // had priority over the parent directive (ParentDir) which is why its value won. It - // also - // won over Dir because the SiblingDir directive was evaluated later on. + // (ChildDir) had priority over the parent directive (ParentDir) which is why its + // value won. It also won over Dir because the SiblingDir directive was declared + // later in `declarations`. expect(childElement.style.width).toEqual('200px'); expect(childElement.style.height).toEqual('200px'); diff --git a/packages/core/test/acceptance/inherit_definition_feature_spec.ts b/packages/core/test/acceptance/inherit_definition_feature_spec.ts index 75492c979f..745df8146c 100644 --- a/packages/core/test/acceptance/inherit_definition_feature_spec.ts +++ b/packages/core/test/acceptance/inherit_definition_feature_spec.ts @@ -116,10 +116,10 @@ describe('inheritance', () => { '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); + expect(getDirectiveDef(BaseDirective) !.hostVars).toEqual(2); + expect(getDirectiveDef(SuperDirective) !.hostVars).toEqual(4); + expect(getDirectiveDef(Sub1Directive) !.hostVars).toEqual(6); + expect(getDirectiveDef(Sub2Directive) !.hostVars).toEqual(6); } }); }); diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index ca44ccf073..3f26cd3597 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -1032,7 +1032,7 @@ describe('acceptance integration tests', () => { fixture.componentInstance.value = false; fixture.detectChanges(); - expect(structuralCompEl.getAttribute('class')).toEqual(''); + expect(structuralCompEl.getAttribute('class')).toBeFalsy(); }); @Directive({selector: '[DirWithClass]'}) @@ -1071,7 +1071,7 @@ describe('acceptance integration tests', () => { it('should delegate initial styles to a [style] input binding if present on a directive on the same element', () => { - @Component({template: '
'}) + @Component({template: '
'}) class App { @ViewChild(DirWithStyleDirective) mockStyleDirective !: DirWithStyleDirective; @@ -1084,8 +1084,8 @@ describe('acceptance integration tests', () => { const styles = fixture.componentInstance.mockStyleDirective.stylesVal; // Use `toContain` since Ivy and ViewEngine have some slight differences in formatting. - expect(styles).toContain('width:100px'); - expect(styles).toContain('height:200px'); + expect(styles).toContain('width: 100px'); + expect(styles).toContain('height: 200px'); }); it('should update `[class]` and bindings in the provided directive if the input is matched', @@ -1122,7 +1122,7 @@ describe('acceptance integration tests', () => { fixture.detectChanges(); expect(fixture.componentInstance.mockStyleDirective.stylesVal) - .toEqual({'width': '200px', 'height': '500px'}); + .toEqual({width: '200px', height: '500px'}); }); onlyInIvy('Style binding merging works differently in Ivy') @@ -1177,8 +1177,8 @@ describe('acceptance integration tests', () => { } }) class DirWithSingleStylingBindings { - width: null|string = null; - height: null|string = null; + width: string|null|undefined = undefined; + height: string|null|undefined = undefined; activateXYZClass: boolean = false; } @@ -1214,8 +1214,8 @@ describe('acceptance integration tests', () => { expect(target.classList.contains('def')).toBeTruthy(); expect(target.classList.contains('xyz')).toBeTruthy(); - dirInstance.width = null; - dirInstance.height = null; + dirInstance.width = undefined; + dirInstance.height = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('100px'); @@ -1230,7 +1230,7 @@ describe('acceptance integration tests', () => { () => { @Directive({selector: '[Dir1WithStyle]', host: {'[style.width]': 'width'}}) class Dir1WithStyle { - width: null|string = null; + width: null|string|undefined = undefined; } @Directive({ @@ -1238,7 +1238,7 @@ describe('acceptance integration tests', () => { host: {'style': 'width: 111px', '[style.width]': 'width'} }) class Dir2WithStyle { - width: null|string = null; + width: null|string|undefined = undefined; } @Component( @@ -1246,10 +1246,10 @@ describe('acceptance integration tests', () => { class App { @ViewChild(Dir1WithStyle) dir1Instance !: Dir1WithStyle; @ViewChild(Dir2WithStyle) dir2Instance !: Dir2WithStyle; - width: string|null = null; + width: string|null|undefined = undefined; } - TestBed.configureTestingModule({declarations: [App, Dir1WithStyle, Dir2WithStyle]}); + TestBed.configureTestingModule({declarations: [App, Dir2WithStyle, Dir1WithStyle]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const {dir1Instance, dir2Instance} = fixture.componentInstance; @@ -1263,15 +1263,15 @@ describe('acceptance integration tests', () => { fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('999px'); - fixture.componentInstance.width = null; + fixture.componentInstance.width = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('222px'); - dir1Instance.width = null; + dir1Instance.width = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('333px'); - dir2Instance.width = null; + dir2Instance.width = undefined; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('111px'); @@ -1316,7 +1316,7 @@ describe('acceptance integration tests', () => { } TestBed.configureTestingModule( - {declarations: [App, Dir1WithStyling, Dir2WithStyling]}); + {declarations: [App, Dir2WithStyling, Dir1WithStyling]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const {dir1Instance, dir2Instance} = fixture.componentInstance; @@ -1325,7 +1325,7 @@ describe('acceptance integration tests', () => { expect(target.style.getPropertyValue('width')).toEqual('111px'); const compInstance = fixture.componentInstance; - compInstance.stylesExp = {width: '999px', height: null}; + compInstance.stylesExp = {width: '999px', height: undefined}; compInstance.classesExp = {one: true, two: false}; dir1Instance.stylesExp = {width: '222px'}; dir1Instance.classesExp = {two: true, three: false}; diff --git a/packages/core/test/acceptance/renderer_factory_spec.ts b/packages/core/test/acceptance/renderer_factory_spec.ts index 324d08b5a7..f10269cf63 100644 --- a/packages/core/test/acceptance/renderer_factory_spec.ts +++ b/packages/core/test/acceptance/renderer_factory_spec.ts @@ -73,7 +73,6 @@ describe('renderer factory lifecycle', () => { fixture.detectChanges(); expect(logs).toEqual( ['create', 'create', 'begin', 'some_component create', 'some_component update', 'end']); - logs = []; fixture.detectChanges(); expect(logs).toEqual(['begin', 'some_component update', 'end']); diff --git a/packages/core/test/acceptance/styling_spec.ts b/packages/core/test/acceptance/styling_spec.ts index 9a74b70d63..8fdd3da292 100644 --- a/packages/core/test/acceptance/styling_spec.ts +++ b/packages/core/test/acceptance/styling_spec.ts @@ -7,16 +7,472 @@ */ import {CommonModule} from '@angular/common'; import {Component, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostBinding, Input, NgModule, Renderer2, ViewChild, ViewContainerRef} from '@angular/core'; -import {getDebugNode} from '@angular/core/src/render3/util/discovery_utils'; +import {bypassSanitizationTrustStyle} from '@angular/core/src/sanitization/bypass'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; +import {getElementClasses, getElementStyles, getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling'; import {By, DomSanitizer, SafeStyle} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; -import {ivyEnabled, onlyInIvy} from '@angular/private/testing'; +import {ivyEnabled, modifiedInIvy, onlyInIvy} from '@angular/private/testing'; describe('styling', () => { beforeEach(ngDevModeResetPerfCounters); + describe('apply in prioritization order', () => { + it('should perform static bindings', () => { + @Component({template: `
`}) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + + const [staticDiv] = fixture.nativeElement.querySelectorAll('div'); + expect(getSortedClassName(staticDiv)).toEqual('STATIC'); + expect(getSortedStyle(staticDiv)).toEqual('color: blue;'); + }); + + it('should perform prop bindings', () => { + @Component({ + template: `
` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic'); + expect(getSortedStyle(div)).toEqual('color: blue; width: 100px;'); + }); + + onlyInIvy('style merging is ivy only feature').it('should perform map bindings', () => { + @Component({ + template: `
` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic'); + expect(getSortedStyle(div)).toEqual('color: blue; width: 100px;'); + }); + + onlyInIvy('style merging is ivy only feature') + .it('should perform interpolation bindings', () => { + @Component({ + // TODO(misko): change `style-x` to `style` once #34202 lands + template: `
` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic static'); + expect(getSortedStyle(div)).toEqual('color: blue;'); + }); + + onlyInIvy('style merging is ivy only feature').it('should support hostBindings', () => { + @Component({ + template: + `
` + }) + class Cmp { + } + @Directive({ + selector: '[my-host-bindings-1]', + host: {'class': 'HOST_STATIC_1', 'style': 'font-family: "c1"'} + }) + class Dir1 { + } + + @Directive({ + selector: '[my-host-bindings-2]', + host: {'class': 'HOST_STATIC_2', 'style': 'font-family: "c2"'} + }) + class Dir2 { + } + + TestBed.configureTestingModule({ + declarations: [ + // Order of directives in the template does not matter. + // Order of declarations matters as it determines the relative priority for overrides. + Dir1, + Dir2, + // Even thought component is at the end, it will still have lowest priority because + // components are special that way. + Cmp, + ] + }); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('HOST_STATIC_1 HOST_STATIC_2 STATIC'); + expect(getSortedStyle(div)).toEqual('color: blue; font-family: c2;'); + }); + + it('should support hostBindings inheritance', () => { + @Component({template: `
`}) + class Cmp { + } + @Directive({host: {'class': 'SUPER_STATIC', 'style': 'font-family: "super"; width: "1px";'}}) + class SuperDir { + } + @Directive({ + selector: '[my-host-bindings]', + host: {'class': 'HOST_STATIC', 'style': 'font-family: "host font"'} + }) + class Dir extends SuperDir { + } + + TestBed.configureTestingModule({declarations: [Cmp, Dir]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)) + .toEqual(ivyEnabled ? 'HOST_STATIC STATIC SUPER_STATIC' : 'HOST_STATIC STATIC'); + // Browsers keep the '"' around the font name, but Domino removes it some we do search and + // replace. Yes we could do `replace(/"/g, '')` but that fails on android. + expect(getSortedStyle(div).replace('"', '').replace('"', '')) + .toEqual( + ivyEnabled ? 'color: blue; font-family: host font; width: 1px;' : + 'color: blue; font-family: host font;'); + }); + + onlyInIvy('style merging is ivy only feature') + .it('should apply template classes in correct order', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const classDiv = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(classDiv)).toEqual('STATIC bar foo'); + }); + + onlyInIvy('style merging is ivy only feature') + .it('should apply template styles in correct order', () => { + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const styleDiv = fixture.nativeElement.querySelector('div'); + expect(getSortedStyle(styleDiv)) + .toEqual('background-color: yellow; color: blue; width: 110px;'); + }); + + it('should work with ngClass/ngStyle', () => { + @Component( + {template: `
`}) + class Cmp { + } + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(getSortedClassName(div)).toEqual('dynamic'); + expect(getSortedStyle(div)).toEqual('font-family: dynamic;'); + }); + }); + + describe('css variables', () => { + onlyInIvy('css variables').it('should support css variables', () => { + // This test only works in browsers which support CSS variables. + if (!(typeof getComputedStyle !== 'undefined' && typeof CSS !== 'undefined' && + typeof CSS.supports !== 'undefined' && CSS.supports('color', 'var(--fake-var)'))) + return; + @Component({ + template: ` +
+ CONTENT +
` + }) + class Cmp { + } + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + // document.body.appendChild(fixture.nativeElement); + fixture.detectChanges(); + + const span = fixture.nativeElement.querySelector('span') as HTMLElement; + expect(getComputedStyle(span).getPropertyValue('background-color')).toEqual('rgb(255, 0, 0)'); + }); + }); + + modifiedInIvy('shadow bindings include static portion') + .it('should bind [class] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + // IVY behavior is slightly different see next test with same name. + @Component({ + template: ` +
+
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-class-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('class') + set klass(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-class', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const [div1, div2] = fixture.nativeElement.querySelectorAll('div'); + // Static value `class="s1"` is always written to the DOM. + expect(div1.className).toEqual('s1'); + // VE passes the dynamic portion of `class` to the directive. + expect(div1.getAttribute('shadow-class')).toEqual('d1'); + // Interpolation `class="s2 {{'d2'}}"` does not have a static portion and so no value is + // written to DOM. + expect(div2.className).toEqual(''); + expect(div2.getAttribute('shadow-class')).toEqual('s2 d2'); + }); + + + onlyInIvy('shadow bindings include static portion') + .it('should bind [class] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + // IVY behavior is slightly different see next test with same name. + @Component({ + template: ` +
+
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-class-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('class') + set klass(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-class', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const [div1, div2] = fixture.nativeElement.querySelectorAll('div'); + // Static value `class="s1"` is always written to the DOM. + expect(div1.className).toEqual('s1'); + // VE has weird behavior where it calls the @Input('class') with either `class="static` or + // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality + // we don't know if `[class]` is coming if we see `class` only. So we need to combine the + // static and dynamic parte. This results in slightly different calling sequence, but should + // result in the same final DOM. + expect(div1.getAttribute('shadow-class')).toEqual('s1 d1'); + + expect(div2.className).toEqual(''); + expect(div2.getAttribute('shadow-class')).toEqual('s2 d2'); + }); + + + modifiedInIvy('shadow bindings include static portion') + .it('should bind [style] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-style-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('style') + set style(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-style', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.cssText).toEqual('color: red;'); + // VE has weird behavior where it calls the @Input('class') with either `class="static` or + // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality + // we + // don't know if `[class]` is coming if we see `class` only. So we need to combine the two + // This results in slightly different calling sequence, but should result in the same final + // DOM. + expect(div.getAttribute('shadow-style')).toEqual('width: 100px;'); + }); + + onlyInIvy('shadow bindings include static portion') + .it('should bind [style] as input to directive', () => { + // VE Behavior https://stackblitz.com/edit/angular-cycpsf + @Component({ + template: ` +
+ ` + }) + class Cmp { + } + + @Directive({selector: '[dir-shadows-style-input]'}) + class DirectiveShadowsClassInput { + constructor(private elementRef: ElementRef) {} + @Input('style') + set style(value: string) { + this.elementRef.nativeElement.setAttribute('shadow-style', value); + } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.style.cssText).toEqual('color: red;'); + // VE has weird behavior where it calls the @Input('class') with either `class="static` or + // `[class]="dynamic"` but never both. This is determined at compile time. Due to locality + // we + // don't know if `[class]` is coming if we see `class` only. So we need to combine the two + // This results in slightly different calling sequence, but should result in the same final + // DOM. + expect(div.getAttribute('shadow-style')).toEqual('color: red; width: 100px;'); + }); + + it('should prevent circular ExpressionChangedAfterItHasBeenCheckedError on shadow inputs', () => { + @Component({template: `
`}) + class Cmp { + } + + @Directive({selector: '[dir-shadows-class-input]'}) + class DirectiveShadowsClassInput { + @Input('class') + klass: string|undefined; + + @HostBinding('class') + get hostClasses() { return `${this.klass} SUFFIX`; } + } + + TestBed.configureTestingModule({declarations: [Cmp, DirectiveShadowsClassInput]}); + const fixture = TestBed.createComponent(Cmp); + expect(() => fixture.detectChanges()).not.toThrow(); + + const div = fixture.nativeElement.querySelector('div'); + expect(div.className).toEqual('s1 SUFFIX'); + }); + + it('should recover from exceptions', () => { + @Component({ + template: ` +
+ +
+ ` + }) + class Cmp { + id = 'throw_id'; + klass: string|string[] = 'throw_klass'; + foo = `throw_foo`; + + maybeThrow(value: any) { + if (typeof value === 'string' && value.indexOf('throw') === 0) { + throw new Error(value); + } + return value; + } + } + + let myDirHostBinding = false; + @Directive({selector: '[my-dir]'}) + class MyDirective { + @HostBinding('class.myDir') + get myDir(): boolean { + if (myDirHostBinding === false) { + throw new Error('class.myDir'); + } + return myDirHostBinding; + } + } + + TestBed.configureTestingModule({declarations: [Cmp, MyDirective]}); + const fixture = TestBed.createComponent(Cmp); + const cmp = fixture.componentInstance; + const div = fixture.nativeElement.querySelector('div'); + const span = fixture.nativeElement.querySelector('span'); + + expect(() => fixture.detectChanges()).toThrowError(/throw_id/); + expect(div.id).toBeFalsy(); + expectClass(span).toEqual({}); + + cmp.id = 'myId'; + expect(() => fixture.detectChanges()).toThrowError(/throw_klass/); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual({}); + + cmp.klass = ['BAR']; + expect(() => fixture.detectChanges()).toThrowError(/throw_foo/); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual(ivyEnabled ? {BAR: true} : {}); + + cmp.foo = 'foo'; + expect(() => fixture.detectChanges()).toThrowError(/class.myDir/); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual(ivyEnabled ? {BAR: true, foo: true} : {}); + + myDirHostBinding = true; + fixture.detectChanges(); + expect(div.id).toEqual('myId'); + expectClass(span).toEqual({BAR: true, foo: true, myDir: true}); + }); + it('should render inline style and class attribute values on the element before a directive is instantiated', () => { @Component({ @@ -197,16 +653,15 @@ describe('styling', () => { expect(div.style.backgroundImage).toBe('url("#test")'); onlyInIvy('perf counters').expectPerfCounters({ - styleProp: 2, - stylePropCacheMiss: 1, + rendererSetStyle: 1, tNode: 3, }); }); - it('should not throw if host style binding is on a template node', () => { - // This ex is a bit contrived. In real apps, you might have a shared class that is extended both - // by components with host elements and by directives on template nodes. In that case, the host - // styles for the template directives should just be ignored. + it('should not write to the native element if a directive shadows the class input', () => { + // This ex is a bit contrived. In real apps, you might have a shared class that is extended + // both by components with host elements and by directives on template nodes. In that case, the + // host styles for the template directives should just be ignored. @Directive({selector: 'ng-template[styleDir]', host: {'[style.display]': 'display'}}) class StyleDir { display = 'block'; @@ -217,10 +672,7 @@ describe('styling', () => { } TestBed.configureTestingModule({declarations: [MyApp, StyleDir]}); - expect(() => { - const fixture = TestBed.createComponent(MyApp); - fixture.detectChanges(); - }).not.toThrow(); + TestBed.createComponent(MyApp).detectChanges(); }); it('should be able to bind a SafeValue to clip-path', () => { @@ -496,23 +948,18 @@ describe('styling', () => { } } - // Ivy does an extra `[class]` write with a falsy value since the value - // is applied during creation mode. This is a deviation from VE and should - // be (Jira Issue = FW-1467). - let totalWrites = ivyEnabled ? 1 : 0; - TestBed.configureTestingModule({declarations: [Cmp, MyClassDir]}); const fixture = TestBed.createComponent(Cmp); - expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingCount).toEqual(0); fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingCount).toEqual(1); expect(capturedClassBindingValue as any).toEqual('bar'); fixture.componentInstance.c = 'dynamic-bar'; fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(totalWrites++); + expect(capturedClassBindingCount).toEqual(2); expect(capturedClassBindingValue !).toEqual('dynamic-bar'); }); @@ -653,15 +1100,28 @@ describe('styling', () => { const fixture = TestBed.createComponent(Cmp); fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(1); - expect(capturedClassBindingValue !).toEqual('static-val'); + expect(capturedClassBindingCount) + .toEqual( + 2 + // '2' is not ideal as '1' would be preferred. + // The reason for two writes is that one is for the static + // `class="static-val"` and one for `[class]="c"`. This means that + // `class="static-val"` is written during the create block which is not ideal. + // To do this correctly we would have to delay the `class="static-val"` until + // the update block, but that would be expensive since it would require that we + // would check if we possibly have this situation on every `advance()` + // instruction. We don't think this is worth it, and we are just going to live + // with this. + ); + expect(capturedClassBindingValue).toEqual(null); expect(capturedMyClassBindingCount).toEqual(1); expect(capturedMyClassBindingValue !).toEqual('foo'); + capturedClassBindingCount = 0; fixture.componentInstance.c = 'dynamic-val'; fixture.detectChanges(); - expect(capturedClassBindingCount).toEqual(2); + expect(capturedClassBindingCount).toEqual(1); expect(capturedClassBindingValue !).toEqual('static-val dynamic-val'); expect(capturedMyClassBindingCount).toEqual(1); expect(capturedMyClassBindingValue !).toEqual('foo'); @@ -791,17 +1251,17 @@ describe('styling', () => { @Component({ template: `
+ [style.height]="h" + [style.opacity]="o" + style="width:200px; height:200px;" + [class.abc]="abc" + [class.xyz]="xyz">
` }) class Cmp { - w: string|null = '100px'; - h: string|null = '100px'; - o: string|null = '0.5'; + w: string|null|undefined = '100px'; + h: string|null|undefined = '100px'; + o: string|null|undefined = '0.5'; abc = true; xyz = false; } @@ -817,9 +1277,9 @@ describe('styling', () => { expect(element.classList.contains('abc')).toBeTruthy(); expect(element.classList.contains('xyz')).toBeFalsy(); - fixture.componentInstance.w = null; - fixture.componentInstance.h = null; - fixture.componentInstance.o = null; + fixture.componentInstance.w = undefined; + fixture.componentInstance.h = undefined; + fixture.componentInstance.o = undefined; fixture.componentInstance.abc = false; fixture.componentInstance.xyz = true; fixture.detectChanges(); @@ -829,6 +1289,14 @@ describe('styling', () => { expect(element.style.opacity).toBeFalsy(); expect(element.classList.contains('abc')).toBeFalsy(); expect(element.classList.contains('xyz')).toBeTruthy(); + + fixture.componentInstance.w = null; + fixture.componentInstance.h = null; + fixture.componentInstance.o = null; + fixture.detectChanges(); + expect(element.style.width).toBeFalsy(); + expect(element.style.height).toBeFalsy(); + expect(element.style.opacity).toBeFalsy(); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -846,14 +1314,14 @@ describe('styling', () => { @Component({ template: `
+ [dir-that-sets-width]="w1" + [another-dir-that-sets-width]="w2"> ` }) class Cmp { - w0: string|null = null; - w1: string|null = null; - w2: string|null = null; + w0: string|null|undefined = null; + w1: string|null|undefined = null; + w2: string|null|undefined = null; } TestBed.configureTestingModule( @@ -867,17 +1335,17 @@ describe('styling', () => { const element = fixture.nativeElement.querySelector('div'); expect(element.style.width).toEqual('100px'); - fixture.componentInstance.w0 = null; - fixture.detectChanges(); - - expect(element.style.width).toEqual('200px'); - - fixture.componentInstance.w1 = null; + fixture.componentInstance.w0 = undefined; fixture.detectChanges(); expect(element.style.width).toEqual('300px'); - fixture.componentInstance.w2 = null; + fixture.componentInstance.w2 = undefined; + fixture.detectChanges(); + + expect(element.style.width).toEqual('200px'); + + fixture.componentInstance.w1 = undefined; fixture.detectChanges(); expect(element.style.width).toBeFalsy(); @@ -920,7 +1388,8 @@ describe('styling', () => { opacity: string|null = '0.5'; @ViewChild(CompWithStyling, {static: true}) compWithStyling: CompWithStyling|null = null; - @ViewChild(DirWithStyling, {static: true}) dirWithStyling: DirWithStyling|null = null; + @ViewChild(DirWithStyling, {static: true}) + dirWithStyling: DirWithStyling|null = null; } TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]}); @@ -929,14 +1398,6 @@ describe('styling', () => { const component = fixture.componentInstance; const element = fixture.nativeElement.querySelector('comp-with-styling'); - const node = getDebugNode(element) !; - - const styles = node.styles !; - const config = styles.context.config; - expect(config.hasCollisions).toBeFalsy(); - expect(config.hasMapBindings).toBeFalsy(); - expect(config.hasPropBindings).toBeTruthy(); - expect(config.allowDirectStyling).toBeTruthy(); expect(element.style.opacity).toEqual('0.5'); expect(element.style.width).toEqual('900px'); @@ -958,8 +1419,8 @@ describe('styling', () => { expect(element.style.height).toEqual('100px'); expect(element.style.fontSize).toEqual('50px'); - // there is no need to flush styling since the styles are applied directly - expect(ngDevMode !.flushStyling).toEqual(0); + // once for the template flush and again for the host bindings + expect(ngDevMode !.flushStyling).toEqual(2); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -993,8 +1454,8 @@ describe('styling', () => { ` }) class Cmp { - opacity: string|null = '0.5'; - width: string|null = 'auto'; + opacity: string|null|undefined = '0.5'; + width: string|null|undefined = 'auto'; tplClass = true; } @@ -1004,40 +1465,36 @@ describe('styling', () => { const element = fixture.nativeElement.querySelector('comp-with-styling'); - const node = getDebugNode(element) !; - const styles = node.styles !; - const classes = node.classes !; - - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'color': 'red', - 'width': 'auto', - 'opacity': '0.5', + 'font-size': '100px', 'height': '900px', - 'font-size': '100px' + 'opacity': '0.5', + 'width': 'auto', }); - expect(classes.values).toEqual({ + expectClass(element).toEqual({ 'dir': true, 'comp': true, 'tpl': true, }); - fixture.componentInstance.width = null; - fixture.componentInstance.opacity = null; + fixture.componentInstance.width = undefined; + fixture.componentInstance.opacity = undefined; fixture.componentInstance.tplClass = false; fixture.detectChanges(); - expect(styles.values).toEqual({ - 'color': 'red', - 'width': '900px', - 'opacity': null, - 'height': '900px', - 'font-size': '100px' - }); - expect(classes.values).toEqual({ + expectStyle(element).toEqual( + {'color': 'red', 'width': '900px', 'height': '900px', 'font-size': '100px'}); + expectClass(element).toEqual({ 'dir': true, 'comp': true, - 'tpl': false, }); + + fixture.componentInstance.width = null; + fixture.componentInstance.opacity = null; + fixture.detectChanges(); + + expectStyle(element).toEqual({'color': 'red', 'height': '900px', 'font-size': '100px'}); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -1059,7 +1516,7 @@ describe('styling', () => { ` }) class Cmp { - w3: string|null = '300px'; + w3: string|null|undefined = '300px'; } TestBed.configureTestingModule( @@ -1069,80 +1526,23 @@ describe('styling', () => { const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; - - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '300px', }); fixture.componentInstance.w3 = null; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({}); + + fixture.componentInstance.w3 = undefined; + fixture.detectChanges(); + + expectStyle(element).toEqual({ 'width': '200px', }); }); - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should support situations where there are more than 32 bindings', () => { - const TOTAL_BINDINGS = 34; - - let bindingsHTML = ''; - let bindingsArr: any[] = []; - for (let i = 0; i < TOTAL_BINDINGS; i++) { - bindingsHTML += `[style.prop${i}]="bindings[${i}]" `; - bindingsArr.push(null); - } - - @Component({template: `
`}) - class Cmp { - bindings = bindingsArr; - - updateBindings(value: string) { - for (let i = 0; i < TOTAL_BINDINGS; i++) { - this.bindings[i] = value + i; - } - } - } - - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - - let testValue = 'initial'; - fixture.componentInstance.updateBindings('initial'); - fixture.detectChanges(); - - const element = fixture.nativeElement.querySelector('div'); - - const node = getDebugNode(element) !; - const styles = node.styles !; - - let values = styles.values; - let props = Object.keys(values); - expect(props.length).toEqual(TOTAL_BINDINGS); - - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const value = values[prop] as string; - const num = value.substr(testValue.length); - expect(value).toEqual(`initial${num}`); - } - - testValue = 'final'; - fixture.componentInstance.updateBindings('final'); - fixture.detectChanges(); - - values = styles.values; - props = Object.keys(values); - expect(props.length).toEqual(TOTAL_BINDINGS); - for (let i = 0; i < props.length; i++) { - const prop = props[i]; - const value = values[prop] as string; - const num = value.substr(testValue.length); - expect(value).toEqual(`final${num}`); - } - }); onlyInIvy('only ivy has style debugging support') .it('should apply map-based style and class entries', () => { @@ -1176,23 +1576,8 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - let styles = node.styles !; - let classes = node.classes !; - - let stylesSummary = styles.summary; - let widthSummary = stylesSummary['width']; - expect(widthSummary.prop).toEqual('width'); - expect(widthSummary.value).toEqual('100px'); - - let heightSummary = stylesSummary['height']; - expect(heightSummary.prop).toEqual('height'); - expect(heightSummary.value).toEqual('200px'); - - let classesSummary = classes.summary; - let abcSummary = classesSummary['abc']; - expect(abcSummary.prop).toEqual('abc'); - expect(abcSummary.value).toBeTruthy(); + expectStyle(element).toEqual({width: '100px', height: '200px'}); + expectClass(element).toEqual({abc: true}); comp.reset(); comp.updateStyles('width', '500px'); @@ -1200,23 +1585,8 @@ describe('styling', () => { comp.updateClasses('def'); fixture.detectChanges(); - styles = node.styles !; - classes = node.classes !; - - stylesSummary = styles.summary; - widthSummary = stylesSummary['width']; - expect(widthSummary.value).toEqual('500px'); - - heightSummary = stylesSummary['height']; - expect(heightSummary.value).toEqual(null); - - classesSummary = classes.summary; - abcSummary = classesSummary['abc']; - expect(abcSummary).toBeUndefined(); - - let defSummary = classesSummary['def']; - expect(defSummary.prop).toEqual('def'); - expect(defSummary.value).toBeTruthy(); + expectStyle(element).toEqual({width: '500px'}); + expectClass(element).toEqual({def: true}); }); onlyInIvy('ivy resolves styling across directives, components and templates in unison') @@ -1230,16 +1600,16 @@ describe('styling', () => { @Component({ template: `
+ [style]="map" + style="width:200px; font-size:99px" + dir-that-sets-styling + #dir + [class.xyz]="xyz">
` }) class Cmp { map: any = {width: '111px', opacity: '0.5'}; - width: string|null = '555px'; + width: string|null|undefined = '555px'; @ViewChild('dir', {read: DirThatSetsStyling, static: true}) dir !: DirThatSetsStyling; @@ -1251,21 +1621,17 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - - const styles = node.styles !; - - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '555px', 'color': 'red', 'font-size': '99px', 'opacity': '0.5', }); - comp.width = null; + comp.width = undefined; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '111px', 'color': 'red', 'font-size': '99px', @@ -1275,21 +1641,18 @@ describe('styling', () => { comp.map = null; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '777px', 'color': 'red', 'font-size': '99px', - 'opacity': null, }); comp.dir.map = null; fixture.detectChanges(); - expect(styles.values).toEqual({ + expectStyle(element).toEqual({ 'width': '200px', - 'color': null, 'font-size': '99px', - 'opacity': null, }); }); @@ -1312,8 +1675,8 @@ describe('styling', () => { ` }) class Cmp { - width: string|null = '111px'; - height: string|null = '111px'; + width: string|null|undefined = '111px'; + height: string|null|undefined = '111px'; map: any = {width: '555px', height: '555px'}; @@ -1329,8 +1692,7 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - // both are applied because this is the first pass - assertStyleCounters(2, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '111px'); assertStyle(element, 'height', '111px'); @@ -1350,7 +1712,7 @@ describe('styling', () => { assertStyle(element, 'width', '222px'); assertStyle(element, 'height', '222px'); - comp.width = null; + comp.width = undefined; ngDevModeResetPerfCounters(); fixture.detectChanges(); @@ -1370,17 +1732,16 @@ describe('styling', () => { ngDevModeResetPerfCounters(); fixture.detectChanges(); - // both are applied because the map was altered - assertStyleCounters(2, 0); + // No change, hence no write + assertStyleCounters(0, 0); assertStyle(element, 'width', '123px'); assertStyle(element, 'height', '123px'); - comp.width = null; + comp.width = undefined; ngDevModeResetPerfCounters(); fixture.detectChanges(); - // the width is applied both in TEMPLATE and in HOST_BINDINGS mode - assertStyleCounters(2, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '999px'); assertStyle(element, 'height', '123px'); @@ -1397,19 +1758,18 @@ describe('styling', () => { ngDevModeResetPerfCounters(); fixture.detectChanges(); - // only the width and color have changed - assertStyleCounters(2, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '123px'); assertStyle(element, 'color', 'red'); - comp.height = null; + comp.height = undefined; ngDevModeResetPerfCounters(); fixture.detectChanges(); // height gets applied twice and all other // values get applied - assertStyleCounters(4, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '1000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'red'); @@ -1418,7 +1778,7 @@ describe('styling', () => { ngDevModeResetPerfCounters(); fixture.detectChanges(); - assertStyleCounters(5, 0); + assertStyleCounters(1, 0); assertStyle(element, 'width', '2000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'blue'); @@ -1429,20 +1789,21 @@ describe('styling', () => { fixture.detectChanges(); // all four are applied because the map was altered - assertStyleCounters(4, 1); + // TODO: temporary dissable as it fails in IE. Re-enabled in #34804 + // assertStyleCounters(1, 0); assertStyle(element, 'width', '2000px'); assertStyle(element, 'height', '1000px'); assertStyle(element, 'color', 'blue'); assertStyle(element, 'opacity', ''); }); - onlyInIvy('only ivy has style/class bindings debugging support') + onlyInIvy('only ivy has [style] support') .it('should sanitize style values before writing them', () => { @Component({ template: `
+ [style.background-image]="bgImageExp" + [style]="styleMapExp">
` }) class Cmp { @@ -1456,92 +1817,63 @@ describe('styling', () => { const comp = fixture.componentInstance; fixture.detectChanges(); - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; + const div = fixture.nativeElement.querySelector('div'); - const lastSanitizedProps: any[] = []; - styles.overrideSanitizer((prop, value) => { - lastSanitizedProps.push(prop); - return value; - }); - - comp.bgImageExp = '123'; + comp.bgImageExp = 'url("javascript:img")'; fixture.detectChanges(); + // for some reasons `background-image: unsafe` is suppressed + expect(getSortedStyle(div)).toEqual(''); - expect(styles.values).toEqual({ - 'background-image': '123', - 'width': null, - }); - - expect(lastSanitizedProps).toEqual(['background-image']); - lastSanitizedProps.length = 0; - - comp.styleMapExp = {'clip-path': '456'}; + // for some reasons `border-image: unsafe` is NOT suppressed + comp.styleMapExp = {'filter': 'url("javascript:border")'}; fixture.detectChanges(); + expect(getSortedStyle(div)).not.toContain('javascript'); - expect(styles.values).toEqual({ - 'background-image': '123', - 'clip-path': '456', - 'width': null, - }); - - expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); - lastSanitizedProps.length = 0; - + // Prove that bindings work. comp.widthExp = '789px'; + comp.bgImageExp = bypassSanitizationTrustStyle(comp.bgImageExp) as string; + comp.styleMapExp = { + 'filter': bypassSanitizationTrustStyle(comp.styleMapExp['filter']) as string + }; fixture.detectChanges(); - expect(styles.values).toEqual({ - 'background-image': '123', - 'clip-path': '456', - 'width': '789px', - }); - - expect(lastSanitizedProps).toEqual(['background-image', 'clip-path']); - lastSanitizedProps.length = 0; + expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")'); + // Some browsers strip `url` on filter so we use `toContain` + expect(div.style.getPropertyValue('filter')).toContain('javascript:border'); + expect(div.style.getPropertyValue('width')).toEqual('789px'); }); - onlyInIvy('only ivy has style/class bindings debugging support') - .it('should apply a unit to a style before writing it', () => { - @Component({ - template: ` + it('should apply a unit to a style before writing it', () => { + @Component({ + template: `
+ [style.height.em]="heightExp">
` - }) - class Cmp { - widthExp: string|number|null = ''; - heightExp: string|number|null = ''; - } + }) + class Cmp { + widthExp: string|number|null = ''; + heightExp: string|number|null = ''; + } - TestBed.configureTestingModule({declarations: [Cmp]}); - const fixture = TestBed.createComponent(Cmp); - const comp = fixture.componentInstance; - fixture.detectChanges(); + TestBed.configureTestingModule({declarations: [Cmp]}); + const fixture = TestBed.createComponent(Cmp); + const comp = fixture.componentInstance; + fixture.detectChanges(); - const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; + const div = fixture.nativeElement.querySelector('div'); - comp.widthExp = '200'; - comp.heightExp = 10; - fixture.detectChanges(); + comp.widthExp = '200'; + comp.heightExp = 10; + fixture.detectChanges(); - expect(styles.values).toEqual({ - 'width': '200px', - 'height': '10em', - }); + expect(getSortedStyle(div)).toEqual('height: 10em; width: 200px;'); - comp.widthExp = 0; - comp.heightExp = null; - fixture.detectChanges(); + comp.widthExp = 0; + comp.heightExp = null; + fixture.detectChanges(); - expect(styles.values).toEqual({ - 'width': '0px', - 'height': null, - }); - }); + expect(getSortedStyle(div)).toEqual('width: 0px;'); + }); it('should be able to bind a SafeValue to clip-path', () => { @Component({template: '
'}) @@ -1657,15 +1989,10 @@ describe('styling', () => { fixture.detectChanges(); const element = fixture.nativeElement.querySelector('div'); - const node = getDebugNode(element) !; - const styles = node.styles !; - - const values = styles.values; - const props = Object.keys(values).sort(); - expect(props).toEqual(['color', 'width']); - - expect(values['width']).toEqual('200px'); - expect(values['color']).toEqual('red'); + expectStyle(element).toEqual({ + color: 'red', + width: '200px', + }); }); onlyInIvy('only ivy has style/class bindings debugging support') @@ -2350,12 +2677,12 @@ describe('styling', () => { ` }) class Cmp { - style: any = {width: '100px'}; - klass: any = {foo: true, bar: false}; + style: any = 'width: 100px'; + klass: any = 'foo'; ngAfterViewInit() { - this.style = {height: '200px'}; - this.klass = {foo: false}; + this.style = 'height: 200px'; + this.klass = 'bar'; } } @@ -2486,25 +2813,393 @@ describe('styling', () => { expect(getComputedStyle(div).width).toBe('10px'); }); - it('should allow classes with trailing and leading spaces in [ngClass]', () => { - @Component({ - template: ` -
-
- ` - }) - class Cmp { - applyClasses = true; + onlyInIvy('[style] binding is supported in Ivy only') + .it('should allow multiple styling bindings to work alongside property/attribute bindings', + () => { + @Component({ + template: ` +
+
` + }) + class MyComp { + } + + @Directive({selector: '[dir-that-sets-styles]'}) + class DirThatSetsStyling { + @HostBinding('style.width') public w = '100px'; + @HostBinding('style.height') public h = '200px'; + } + + const fixture = + TestBed.configureTestingModule({declarations: [MyComp, DirThatSetsStyling]}) + .createComponent(MyComp); + fixture.detectChanges(); + const div = fixture.nativeElement.querySelector('div') !; + expect(div.style.getPropertyValue('width')).toEqual('100px'); + expect(div.style.getPropertyValue('height')).toEqual('200px'); + expect(div.style.getPropertyValue('font-size')).toEqual('300px'); + expect(div.getAttribute('title')).toEqual('my-title'); + expect(div.getAttribute('data-foo')).toEqual('my-foo'); + }); + + onlyInIvy('VE clobers in case of @HostBinding("class")') + .it('should allow host styling on the root element with external styling', () => { + @Component({template: '...'}) + class MyComp { + @HostBinding('class') public classes = ''; + } + + const fixture = + TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp); + fixture.detectChanges(); + const root = fixture.nativeElement as HTMLElement; + expect(root.className).toEqual(''); + + fixture.componentInstance.classes = '1 2 3'; + fixture.detectChanges(); + expect(root.className.split(/\s+/).sort().join(' ')).toEqual('1 2 3'); + + root.classList.add('0'); + expect(root.className.split(/\s+/).sort().join(' ')).toEqual('0 1 2 3'); + + fixture.componentInstance.classes = '1 2 3 4'; + fixture.detectChanges(); + expect(root.className.split(/\s+/).sort().join(' ')).toEqual('0 1 2 3 4'); + }); + + it('should apply camelCased class names', () => { + @Component({template: `
`}) + class MyComp { } - TestBed.configureTestingModule({declarations: [Cmp]}); + TestBed.configureTestingModule({ + declarations: [MyComp], + }); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const classList = (fixture.nativeElement.querySelector('div') as HTMLDivElement).classList; + expect(classList.contains('fooBar')).toBeTruthy(); + expect(classList.contains('barFoo')).toBeTruthy(); + }); + + // onlyInIvy('[style] bindings are ivy only') + xit('should convert camelCased style property names to snake-case', () => { + // TODO(misko): Temporarily disabled in this PR renabled in + // https://github.com/angular/angular/pull/34616 + // Current implementation uses strings to write to DOM. Because of that it does not convert + // property names from camelCase to dash-case. This is rectified in #34616 because we switch + // from string API to `element.style.setProperty` API. + @Component({template: `
`}) + class MyComp { + myStyles = {}; + } + + TestBed.configureTestingModule({ + declarations: [MyComp], + }); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const div = fixture.nativeElement.querySelector('div') as HTMLDivElement; + fixture.componentInstance.myStyles = {fontSize: '200px'}; + fixture.detectChanges(); + + expect(div.style.getPropertyValue('font-size')).toEqual('200px'); + }); + + it('should recover from an error thrown in styling bindings', () => { + let raiseWidthError = false; + + @Component({template: `
`}) + class MyComp { + get myWidth() { + if (raiseWidthError) { + throw new Error('...'); + } + return '100px'; + } + } + + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + + raiseWidthError = true; + expect(() => fixture.detectChanges()).toThrow(); + + raiseWidthError = false; + fixture.detectChanges(); + const div = fixture.nativeElement.querySelector('div') as HTMLDivElement; + expect(div.style.getPropertyValue('width')).toEqual('100px'); + expect(div.style.getPropertyValue('height')).toEqual('200px'); + }); + + onlyInIvy('Prioritization works in Ivy only') + .it('should prioritize host bindings for templates first, then directives and finally components', + () => { + @Component({selector: 'my-comp-with-styling', template: ''}) + class MyCompWithStyling { + @HostBinding('style') + myStyles: any = {width: '300px'}; + + @HostBinding('style.height') + myHeight: any = '305px'; + } + + @Directive({selector: '[my-dir-with-styling]'}) + class MyDirWithStyling { + @HostBinding('style') + myStyles: any = {width: '200px'}; + + @HostBinding('style.height') + myHeight: any = '205px'; + } + + @Component({ + template: ` + + + ` + }) + class MyComp { + myStyles: {width?: string} = {width: '100px'}; + myHeight: string|null|undefined = '100px'; + + @ViewChild(MyDirWithStyling) dir !: MyDirWithStyling; + @ViewChild(MyCompWithStyling) comp !: MyCompWithStyling; + } + + TestBed.configureTestingModule( + {declarations: [MyComp, MyCompWithStyling, MyDirWithStyling]}); + const fixture = TestBed.createComponent(MyComp); + const comp = fixture.componentInstance; + const elm = fixture.nativeElement.querySelector('my-comp-with-styling') !; + + fixture.detectChanges(); + expect(elm.style.width).toEqual('100px'); + expect(elm.style.height).toEqual('100px'); + + comp.myStyles = {}; + comp.myHeight = undefined; + fixture.detectChanges(); + expect(elm.style.width).toEqual('200px'); + expect(elm.style.height).toEqual('205px'); + + comp.dir.myStyles = {}; + comp.dir.myHeight = undefined; + fixture.detectChanges(); + expect(elm.style.width).toEqual('300px'); + expect(elm.style.height).toEqual('305px'); + + comp.comp.myStyles = {}; + comp.comp.myHeight = undefined; + fixture.detectChanges(); + expect(elm.style.width).toEqual('1px'); + expect(elm.style.height).toEqual('1px'); + }); + + it('should combine host class.foo bindings from multiple directives', () => { + + @Directive({ + selector: '[dir-that-sets-one-two]', + exportAs: 'one', + }) + class DirThatSetsOneTwo { + @HostBinding('class.one') one = false; + @HostBinding('class.two') two = false; + } + + @Directive({ + selector: '[dir-that-sets-three-four]', + exportAs: 'two', + }) + class DirThatSetsThreeFour { + @HostBinding('class.three') three = false; + @HostBinding('class.four') four = false; + } + + @Component({ + template: ` +
+
+ ` + }) + class MyComp { + @ViewChild('div1', {static: true, read: DirThatSetsOneTwo}) + public dirOneA: DirThatSetsOneTwo|null = null; + + @ViewChild('div1', {static: true, read: DirThatSetsThreeFour}) + public dirTwoA: DirThatSetsThreeFour|null = null; + + @ViewChild('div2', {static: true, read: DirThatSetsOneTwo}) + public dirOneB: DirThatSetsOneTwo|null = null; + + @ViewChild('div2', {static: true, read: DirThatSetsThreeFour}) + public dirTwoB: DirThatSetsThreeFour|null = null; + + zero = false; + } + + TestBed.configureTestingModule( + {declarations: [MyComp, DirThatSetsThreeFour, DirThatSetsOneTwo]}); + + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const [div1, div2] = fixture.nativeElement.querySelectorAll('div') as HTMLDivElement[]; + + expect(div1.className).toBe(''); + expect(div2.className).toBe(''); + + const comp = fixture.componentInstance; + comp.dirOneA !.one = comp.dirOneB !.one = true; + comp.dirOneA !.two = comp.dirOneB !.two = true; + fixture.detectChanges(); + + expect(div1.classList.contains('one')).toBeTruthy(); + expect(div1.classList.contains('two')).toBeTruthy(); + expect(div1.classList.contains('three')).toBeFalsy(); + expect(div1.classList.contains('four')).toBeFalsy(); + expect(div2.classList.contains('one')).toBeTruthy(); + expect(div2.classList.contains('two')).toBeTruthy(); + expect(div2.classList.contains('three')).toBeFalsy(); + expect(div2.classList.contains('four')).toBeFalsy(); + expect(div2.classList.contains('zero')).toBeFalsy(); + + comp.dirTwoA !.three = comp.dirTwoB !.three = true; + comp.dirTwoA !.four = comp.dirTwoB !.four = true; + fixture.detectChanges(); + + expect(div1.classList.contains('one')).toBeTruthy(); + expect(div1.classList.contains('two')).toBeTruthy(); + expect(div1.classList.contains('three')).toBeTruthy(); + expect(div1.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('one')).toBeTruthy(); + expect(div2.classList.contains('two')).toBeTruthy(); + expect(div2.classList.contains('three')).toBeTruthy(); + expect(div2.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('zero')).toBeFalsy(); + + comp.zero = true; + fixture.detectChanges(); + + expect(div1.classList.contains('one')).toBeTruthy(); + expect(div1.classList.contains('two')).toBeTruthy(); + expect(div1.classList.contains('three')).toBeTruthy(); + expect(div1.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('one')).toBeTruthy(); + expect(div2.classList.contains('two')).toBeTruthy(); + expect(div2.classList.contains('three')).toBeTruthy(); + expect(div2.classList.contains('four')).toBeTruthy(); + expect(div2.classList.contains('zero')).toBeTruthy(); + }); + + it('should combine static host classes with component "class" host attribute', () => { + @Component({selector: 'comp-with-classes', template: '', host: {'class': 'host'}}) + class CompWithClasses { + constructor(ref: ElementRef) { ref.nativeElement.classList.add('custom'); } + } + + @Component({ + template: `` + }) + class MyComp { + items = [1, 2, 3]; + } + + const fixture = TestBed + .configureTestingModule({ + declarations: [MyComp, CompWithClasses], + }) + .createComponent(MyComp); + fixture.detectChanges(); + + const [one, two, three] = + fixture.nativeElement.querySelectorAll('comp-with-classes') as HTMLDivElement[]; + + expect(one.classList.contains('custom')).toBeTruthy(); + expect(one.classList.contains('inline')).toBeTruthy(); + expect(one.classList.contains('host')).toBeTruthy(); + + expect(two.classList.contains('custom')).toBeTruthy(); + expect(two.classList.contains('inline')).toBeTruthy(); + expect(two.classList.contains('host')).toBeTruthy(); + + expect(three.classList.contains('custom')).toBeTruthy(); + expect(three.classList.contains('inline')).toBeTruthy(); + expect(three.classList.contains('host')).toBeTruthy(); + }); + + it('should allow a single style host binding on an element', () => { + @Component({template: `
`}) + class Cmp { + } + + @Directive({selector: '[single-host-style-dir]'}) + class SingleHostStyleDir { + @HostBinding('style.width') + width = '100px'; + } + + TestBed.configureTestingModule({declarations: [Cmp, SingleHostStyleDir]}); const fixture = TestBed.createComponent(Cmp); fixture.detectChanges(); - const leading = fixture.nativeElement.querySelector('[leading-space]'); - const trailing = fixture.nativeElement.querySelector('[trailing-space]'); - expect(leading.className).toBe('foo', 'Expected class to be applied despite leading space.'); - expect(trailing.className).toBe('foo', 'Expected class to be applied despite trailing space.'); + const element = fixture.nativeElement.querySelector('div'); + expect(element.style.width).toEqual('100px'); + }); + + it('should override class bindings when a directive extends another directive', () => { + @Component({template: ``}) + class Cmp { + } + + @Component({ + selector: 'parent-comp', + host: {'class': 'parent-comp', '[class.parent-comp-active]': 'true'}, + template: '...', + }) + class ParentComp { + } + + @Component({ + selector: 'child-comp', + host: { + 'class': 'child-comp', + '[class.child-comp-active]': 'true', + '[class.parent-comp]': 'false', + '[class.parent-comp-active]': 'false' + }, + template: '...', + }) + class ChildComp extends ParentComp { + } + + TestBed.configureTestingModule({declarations: [Cmp, ChildComp, ParentComp]}); + const fixture = TestBed.createComponent(Cmp); + fixture.detectChanges(); + + const element = fixture.nativeElement.querySelector('child-comp'); + expect(element.classList.contains('template')).toBeTruthy(); + + expect(element.classList.contains('child-comp')).toBeTruthy(); + expect(element.classList.contains('child-comp-active')).toBeTruthy(); + + expect(element.classList.contains('parent-comp')).toBeFalsy(); + expect(element.classList.contains('parent-comp-active')).toBeFalsy(); }); // TODO(FW-1360): re-enable this test once the new styling changes are in place. @@ -2530,7 +3225,6 @@ describe('styling', () => { expect(logs).toEqual([]); }); - }); function assertStyleCounters(countForSet: number, countForRemove: number) { @@ -2541,3 +3235,11 @@ function assertStyleCounters(countForSet: number, countForRemove: number) { function assertStyle(element: HTMLElement, prop: string, value: any) { expect((element.style as any)[prop]).toEqual(value); } + +function expectStyle(element: HTMLElement) { + return expect(getElementStyles(element)); +} + +function expectClass(element: HTMLElement) { + return expect(getElementClasses(element)); +} \ No newline at end of file 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 dfd99b843c..b56ac936cc 100644 --- a/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json +++ b/packages/core/test/bundling/cyclic_import/bundle.golden_symbols.json @@ -113,9 +113,6 @@ { "name": "RENDERER_FACTORY" }, - { - "name": "RendererStyleFlags3" - }, { "name": "SANITIZER" }, @@ -158,18 +155,12 @@ { "name": "addHostBindingsToExpandoInstructions" }, - { - "name": "addItemToStylingMap" - }, { "name": "addToViewTree" }, { "name": "allocLFrame" }, - { - "name": "allocStylingMapArray" - }, { "name": "appendChild" }, @@ -195,7 +186,13 @@ "name": "classIndexOf" }, { - "name": "concatString" + "name": "clearActiveHostElement" + }, + { + "name": "computeStaticStyling" + }, + { + "name": "concatStringsWithSpace" }, { "name": "createDirectivesInstances" @@ -284,9 +281,6 @@ { "name": "findDirectiveDefMatches" }, - { - "name": "forceStylesAsString" - }, { "name": "generateExpandoInstructionBlock" }, @@ -335,9 +329,6 @@ { "name": "getFirstNativeNode" }, - { - "name": "getInitialStylingValue" - }, { "name": "getInjectorIndex" }, @@ -353,12 +344,6 @@ { "name": "getLViewParent" }, - { - "name": "getMapProp" - }, - { - "name": "getMapValue" - }, { "name": "getNameOnlyMarkerIndex" }, @@ -407,15 +392,9 @@ { "name": "getSelectedIndex" }, - { - "name": "getStylingMapArray" - }, { "name": "growHostVarsSpace" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasClassInput" }, @@ -428,18 +407,12 @@ { "name": "hasTagAndTypeMatch" }, - { - "name": "hyphenate" - }, { "name": "includeViewProviders" }, { "name": "increaseElementDepthCount" }, - { - "name": "incrementActiveDirectiveId" - }, { "name": "incrementInitPhaseFlags" }, @@ -509,12 +482,6 @@ { "name": "isProceduralRenderer" }, - { - "name": "isStylingContext" - }, - { - "name": "isStylingValueDefined" - }, { "name": "leaveDI" }, @@ -557,12 +524,6 @@ { "name": "noSideEffects" }, - { - "name": "objectToClassName" - }, - { - "name": "setHostBindingsByExecutingExpandoInstructions" - }, { "name": "refreshChildComponents" }, @@ -581,9 +542,6 @@ { "name": "refreshView" }, - { - "name": "registerInitialStylingOnTNode" - }, { "name": "registerPostOrderHooks" }, @@ -599,15 +557,9 @@ { "name": "renderComponent" }, - { - "name": "renderInitialStyling" - }, { "name": "renderStringify" }, - { - "name": "renderStylingMap" - }, { "name": "renderView" }, @@ -623,9 +575,6 @@ { "name": "saveResolvedLocalsInData" }, - { - "name": "selectClassBasedInputName" - }, { "name": "selectIndexInternal" }, @@ -638,17 +587,14 @@ { "name": "setBindingRoot" }, - { - "name": "setClass" - }, - { - "name": "setClassName" - }, { "name": "setCurrentQueryIndex" }, { - "name": "setDirectiveStylingInput" + "name": "setDirectiveInputsWhichShadowsStyling" + }, + { + "name": "setHostBindingsByExecutingExpandoInstructions" }, { "name": "setIncludeViewProviders" @@ -665,30 +611,18 @@ { "name": "setIsNotParent" }, - { - "name": "setMapValue" - }, { "name": "setPreviousOrParentTNode" }, { "name": "setSelectedIndex" }, - { - "name": "setStyle" - }, - { - "name": "setStyleAttr" - }, { "name": "setUpAttributes" }, { "name": "stringifyForError" }, - { - "name": "stylingMapToString" - }, { "name": "syncViewWithBlueprint" }, @@ -698,14 +632,14 @@ { "name": "unwrapRNode" }, - { - "name": "updateRawValueOnContext" - }, { "name": "viewAttachedToChangeDetector" }, { - "name": "writeStylingValueDirectly" + "name": "writeDirectClass" + }, + { + "name": "writeDirectStyle" }, { "name": "ɵɵdefineComponent" 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 54896c13b1..6c68029791 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -104,9 +104,6 @@ { "name": "RENDERER_FACTORY" }, - { - "name": "RendererStyleFlags3" - }, { "name": "SANITIZER" }, @@ -143,18 +140,12 @@ { "name": "addHostBindingsToExpandoInstructions" }, - { - "name": "addItemToStylingMap" - }, { "name": "addToViewTree" }, { "name": "allocLFrame" }, - { - "name": "allocStylingMapArray" - }, { "name": "appendChild" }, @@ -174,7 +165,13 @@ "name": "callHooks" }, { - "name": "concatString" + "name": "clearActiveHostElement" + }, + { + "name": "computeStaticStyling" + }, + { + "name": "concatStringsWithSpace" }, { "name": "createLFrame" @@ -242,9 +239,6 @@ { "name": "extractPipeDef" }, - { - "name": "forceStylesAsString" - }, { "name": "generateExpandoInstructionBlock" }, @@ -278,9 +272,6 @@ { "name": "getFirstNativeNode" }, - { - "name": "getInitialStylingValue" - }, { "name": "getInjectorIndex" }, @@ -296,12 +287,6 @@ { "name": "getLViewParent" }, - { - "name": "getMapProp" - }, - { - "name": "getMapValue" - }, { "name": "getNativeAnchorNode" }, @@ -344,27 +329,15 @@ { "name": "getSelectedIndex" }, - { - "name": "getStylingMapArray" - }, { "name": "growHostVarsSpace" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasParentInjector" }, - { - "name": "hyphenate" - }, { "name": "includeViewProviders" }, - { - "name": "incrementActiveDirectiveId" - }, { "name": "incrementInitPhaseFlags" }, @@ -404,12 +377,6 @@ { "name": "isProceduralRenderer" }, - { - "name": "isStylingContext" - }, - { - "name": "isStylingValueDefined" - }, { "name": "leaveDI" }, @@ -443,12 +410,6 @@ { "name": "noSideEffects" }, - { - "name": "objectToClassName" - }, - { - "name": "setHostBindingsByExecutingExpandoInstructions" - }, { "name": "refreshChildComponents" }, @@ -467,9 +428,6 @@ { "name": "refreshView" }, - { - "name": "registerInitialStylingOnTNode" - }, { "name": "registerPreOrderHooks" }, @@ -482,15 +440,9 @@ { "name": "renderComponent" }, - { - "name": "renderInitialStyling" - }, { "name": "renderStringify" }, - { - "name": "renderStylingMap" - }, { "name": "renderView" }, @@ -509,59 +461,44 @@ { "name": "setBindingRoot" }, - { - "name": "setClass" - }, - { - "name": "setClassName" - }, { "name": "setCurrentQueryIndex" }, + { + "name": "setHostBindingsByExecutingExpandoInstructions" + }, { "name": "setIncludeViewProviders" }, { "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": "writeDirectClass" + }, + { + "name": "writeDirectStyle" }, { "name": "ɵɵdefineComponent" diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 8f3e88c04d..3917b6be2c 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -2,9 +2,6 @@ { "name": "ACTIVE_INDEX" }, - { - "name": "BIT_MASK_START_VALUE" - }, { "name": "BLOOM_MASK" }, @@ -38,18 +35,6 @@ { "name": "DECLARATION_VIEW" }, - { - "name": "DEFAULT_BINDING_INDEX" - }, - { - "name": "DEFAULT_BINDING_VALUE" - }, - { - "name": "DEFAULT_GUARD_MASK_VALUE" - }, - { - "name": "DEFAULT_TOTAL_SOURCES" - }, { "name": "DOCUMENT" }, @@ -90,7 +75,7 @@ "name": "HOST" }, { - "name": "INDEX_START_VALUE" + "name": "IGNORE_DUE_TO_INPUT_SHADOW" }, { "name": "INJECTOR" @@ -107,12 +92,6 @@ { "name": "IterableDiffers" }, - { - "name": "MAP_BASED_ENTRY_PROP_NAME" - }, - { - "name": "MAP_DIRTY_VALUE" - }, { "name": "MONKEY_PATCH_KEY_NAME" }, @@ -206,15 +185,9 @@ { "name": "RecordViewTuple" }, - { - "name": "RendererStyleFlags3" - }, { "name": "SANITIZER" }, - { - "name": "STYLING_INDEX_FOR_MAP_BINDING" - }, { "name": "SWITCH_ELEMENT_REF_FACTORY" }, @@ -230,9 +203,6 @@ { "name": "SkipSelf" }, - { - "name": "TEMPLATE_DIRECTIVE_INDEX" - }, { "name": "TNODE" }, @@ -317,9 +287,6 @@ { "name": "__window" }, - { - "name": "_activeStylingMapApplyFn" - }, { "name": "_currentInjector" }, @@ -332,27 +299,15 @@ { "name": "_renderCompCount" }, - { - "name": "_state" - }, { "name": "_symbolIterator" }, - { - "name": "addBindingIntoContext" - }, { "name": "addComponentLogic" }, { "name": "addHostBindingsToExpandoInstructions" }, - { - "name": "addItemToStylingMap" - }, - { - "name": "addNewSourceColumn" - }, { "name": "addRemoveViewFromContainer" }, @@ -365,21 +320,12 @@ { "name": "allocLFrame" }, - { - "name": "allocStylingMapArray" - }, - { - "name": "allocTStylingContext" - }, - { - "name": "allocateNewContextEntry" - }, - { - "name": "allowDirectStyling" - }, { "name": "appendChild" }, + { + "name": "appendStyling" + }, { "name": "applyContainer" }, @@ -389,15 +335,6 @@ { "name": "applyProjectionRecursive" }, - { - "name": "applyStylingValue" - }, - { - "name": "applyStylingValueDirectly" - }, - { - "name": "applyStylingViaContext" - }, { "name": "applyToElementOrContainer" }, @@ -437,17 +374,50 @@ { "name": "checkNoChangesInternal" }, + { + "name": "checkStylingProperty" + }, { "name": "classIndexOf" }, { "name": "cleanUpView" }, + { + "name": "clearActiveHostElement" + }, { "name": "collectNativeNodes" }, { - "name": "concatString" + "name": "computeClassChanges" + }, + { + "name": "computeStaticStyling" + }, + { + "name": "computeStyleChanges" + }, + { + "name": "concatStringsWithSpace" + }, + { + "name": "consumeClassToken" + }, + { + "name": "consumeQuotedText" + }, + { + "name": "consumeSeparator" + }, + { + "name": "consumeStyleKey" + }, + { + "name": "consumeStyleValue" + }, + { + "name": "consumeWhitespace" }, { "name": "createContainerRef" @@ -575,9 +545,6 @@ { "name": "extractPipeDef" }, - { - "name": "findAndApplyMapValue" - }, { "name": "findAttrIndexInNode" }, @@ -591,10 +558,10 @@ "name": "findViaComponent" }, { - "name": "flushStyling" + "name": "flushStyleBinding" }, { - "name": "forceStylesAsString" + "name": "flushStylingOnElementExit" }, { "name": "forwardRef" @@ -608,15 +575,9 @@ { "name": "generatePropertyAliases" }, - { - "name": "getActiveDirectiveId" - }, { "name": "getBeforeNodeForView" }, - { - "name": "getBindingValue" - }, { "name": "getBindingsEnabled" }, @@ -624,7 +585,7 @@ "name": "getCheckNoChangesMode" }, { - "name": "getClassesContext" + "name": "getClassBindingChanged" }, { "name": "getCleanup" @@ -647,9 +608,6 @@ { "name": "getContainerRenderParent" }, - { - "name": "getContext" - }, { "name": "getContextLView" }, @@ -659,9 +617,6 @@ { "name": "getDebugContext" }, - { - "name": "getDefaultValue" - }, { "name": "getDirectiveDef" }, @@ -680,12 +635,6 @@ { "name": "getFirstNativeNode" }, - { - "name": "getGuardMask" - }, - { - "name": "getInitialStylingValue" - }, { "name": "getInjectableDef" }, @@ -705,10 +654,10 @@ "name": "getLViewParent" }, { - "name": "getMapProp" + "name": "getLastParsedKey" }, { - "name": "getMapValue" + "name": "getLastParsedValue" }, { "name": "getNameOnlyMarkerIndex" @@ -773,35 +722,14 @@ { "name": "getPreviousOrParentTNode" }, - { - "name": "getProp" - }, - { - "name": "getPropConfig" - }, - { - "name": "getPropValuesStartPosition" - }, { "name": "getRenderParent" }, - { - "name": "getRenderer" - }, { "name": "getSelectedIndex" }, { - "name": "getStylesContext" - }, - { - "name": "getStylingMapArray" - }, - { - "name": "getStylingMapsSyncFn" - }, - { - "name": "getStylingState" + "name": "getStyleBindingChanged" }, { "name": "getSymbolIterator" @@ -810,10 +738,19 @@ "name": "getTNode" }, { - "name": "getTViewCleanup" + "name": "getTStylingRangeNext" }, { - "name": "getTotalSources" + "name": "getTStylingRangePrev" + }, + { + "name": "getTStylingRangePrevDuplicate" + }, + { + "name": "getTStylingRangeTail" + }, + { + "name": "getTViewCleanup" }, { "name": "getTypeName" @@ -821,42 +758,27 @@ { "name": "getTypeNameForDebugging" }, - { - "name": "getValue" - }, - { - "name": "getValuesCount" - }, { "name": "growHostVarsSpace" }, { "name": "handleError" }, - { - "name": "hasActiveElementFlag" - }, { "name": "hasClassInput" }, - { - "name": "hasConfig" - }, { "name": "hasParentInjector" }, { "name": "hasStyleInput" }, + { + "name": "hasStylingInputShadow" + }, { "name": "hasTagAndTypeMatch" }, - { - "name": "hasValueChanged" - }, - { - "name": "hyphenate" - }, { "name": "includeViewProviders" }, @@ -864,7 +786,7 @@ "name": "increaseElementDepthCount" }, { - "name": "incrementActiveDirectiveId" + "name": "incrementBindingIndex" }, { "name": "incrementInitPhaseFlags" @@ -893,6 +815,9 @@ { "name": "insertBloom" }, + { + "name": "insertTStylingBinding" + }, { "name": "insertView" }, @@ -917,6 +842,9 @@ { "name": "invokeHostBindingsInCreationMode" }, + { + "name": "isActiveHostElement" + }, { "name": "isAnimationProp" }, @@ -948,10 +876,7 @@ "name": "isForwardRef" }, { - "name": "isHostStyling" - }, - { - "name": "isHostStylingActive" + "name": "isInHostBindings" }, { "name": "isJsObject" @@ -983,15 +908,6 @@ { "name": "isRootView" }, - { - "name": "isSanitizationRequired" - }, - { - "name": "isStylingContext" - }, - { - "name": "isStylingValueDefined" - }, { "name": "iterateListLike" }, @@ -1037,6 +953,12 @@ { "name": "markDirtyIfOnPush" }, + { + "name": "markDuplicates" + }, + { + "name": "markStylingBindingDirty" + }, { "name": "markViewDirty" }, @@ -1083,19 +1005,28 @@ "name": "noSideEffects" }, { - "name": "normalizeBitMaskValue" + "name": "parseClassName" }, { - "name": "objectToClassName" + "name": "parseClassNameNext" }, { - "name": "patchConfig" + "name": "parseKeyValue" }, { - "name": "patchHostStylingFlag" + "name": "parseStyle" }, { - "name": "setHostBindingsByExecutingExpandoInstructions" + "name": "parseStyleNext" + }, + { + "name": "parserState" + }, + { + "name": "processClassToken" + }, + { + "name": "processStyleKeyValue" }, { "name": "readPatchedData" @@ -1103,6 +1034,12 @@ { "name": "readPatchedLView" }, + { + "name": "reconcileClassNames" + }, + { + "name": "reconcileStyleNames" + }, { "name": "refreshChildComponents" }, @@ -1121,12 +1058,6 @@ { "name": "refreshView" }, - { - "name": "registerBinding" - }, - { - "name": "registerInitialStylingOnTNode" - }, { "name": "registerPostOrderHooks" }, @@ -1139,6 +1070,9 @@ { "name": "removeListeners" }, + { + "name": "removeStyle" + }, { "name": "removeView" }, @@ -1157,30 +1091,18 @@ { "name": "renderDetachView" }, - { - "name": "renderHostBindingsAsStale" - }, - { - "name": "renderInitialStyling" - }, { "name": "renderStringify" }, - { - "name": "renderStylingMap" - }, { "name": "renderView" }, { - "name": "resetCurrentStyleSanitizer" + "name": "resetParserState" }, { "name": "resetPreOrderHookFlags" }, - { - "name": "resetStylingState" - }, { "name": "resolveDirectives" }, @@ -1199,15 +1121,9 @@ { "name": "searchTokensOnInjector" }, - { - "name": "selectClassBasedInputName" - }, { "name": "selectIndexInternal" }, - { - "name": "setActiveElementFlag" - }, { "name": "setActiveHostElement" }, @@ -1220,29 +1136,17 @@ { "name": "setCheckNoChangesMode" }, - { - "name": "setClass" - }, - { - "name": "setClassName" - }, { "name": "setCurrentQueryIndex" }, { - "name": "setCurrentStyleSanitizer" - }, - { - "name": "setDefaultValue" - }, - { - "name": "setDirectiveStylingInput" + "name": "setDirectiveInputsWhichShadowsStyling" }, { "name": "setElementExitFn" }, { - "name": "setGuardMask" + "name": "setHostBindingsByExecutingExpandoInstructions" }, { "name": "setIncludeViewProviders" @@ -1262,12 +1166,6 @@ { "name": "setLContainerActiveIndex" }, - { - "name": "setMapAsDirty" - }, - { - "name": "setMapValue" - }, { "name": "setPreviousOrParentTNode" }, @@ -1275,19 +1173,25 @@ "name": "setSelectedIndex" }, { - "name": "setStyle" + "name": "setTStylingRangeNext" }, { - "name": "setStyleAttr" + "name": "setTStylingRangeNextDuplicate" + }, + { + "name": "setTStylingRangePrev" + }, + { + "name": "setTStylingRangePrevDuplicate" }, { "name": "setUpAttributes" }, { - "name": "setValue" + "name": "shouldSearchParent" }, { - "name": "shouldSearchParent" + "name": "splitClassList" }, { "name": "storeCleanupFn" @@ -1299,16 +1203,10 @@ "name": "stringifyForError" }, { - "name": "stylingApply" + "name": "styleKeyValue" }, { - "name": "stylingMapToString" - }, - { - "name": "stylingProp" - }, - { - "name": "syncContextInitialStyling" + "name": "stylingPropertyFirstUpdatePass" }, { "name": "syncViewWithBlueprint" @@ -1325,6 +1223,12 @@ { "name": "tickRootContext" }, + { + "name": "toTStylingRange" + }, + { + "name": "toggleClass" + }, { "name": "trackByIdentity" }, @@ -1337,21 +1241,6 @@ { "name": "unwrapSafeValue" }, - { - "name": "updateBindingData" - }, - { - "name": "updateClassViaContext" - }, - { - "name": "updateInitialStylingOnContext" - }, - { - "name": "updateRawValueOnContext" - }, - { - "name": "updateStyleViaContext" - }, { "name": "viewAttachedToChangeDetector" }, @@ -1365,7 +1254,16 @@ "name": "wrapListener" }, { - "name": "writeStylingValueDirectly" + "name": "writeAndReconcileClass" + }, + { + "name": "writeAndReconcileStyle" + }, + { + "name": "writeDirectClass" + }, + { + "name": "writeDirectStyle" }, { "name": "ɵɵadvance" diff --git a/packages/core/test/render3/component_ref_spec.ts b/packages/core/test/render3/component_ref_spec.ts index 4ed8043b72..6d5b4375ab 100644 --- a/packages/core/test/render3/component_ref_spec.ts +++ b/packages/core/test/render3/component_ref_spec.ts @@ -8,10 +8,10 @@ import {Injector, NgModuleRef, ViewEncapsulation} from '../../src/core'; import {ComponentFactory} from '../../src/linker/component_factory'; -import {RendererFactory2} from '../../src/render/api'; +import {RendererFactory2, RendererType2} from '../../src/render/api'; import {injectComponentFactoryResolver} from '../../src/render3/component_ref'; -import {ɵɵdefineComponent} from '../../src/render3/index'; -import {domRendererFactory3} from '../../src/render3/interfaces/renderer'; +import {AttributeMarker, ɵɵdefineComponent} from '../../src/render3/index'; +import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer'; import {Sanitizer} from '../../src/sanitization/sanitizer'; describe('ComponentFactory', () => { @@ -97,6 +97,7 @@ describe('ComponentFactory', () => { decls: 0, vars: 0, template: () => undefined, + hostAttrs: [AttributeMarker.Classes, 'HOST_COMPONENT'] }); } @@ -291,5 +292,24 @@ describe('ComponentFactory', () => { expect(mSanitizerFactorySpy).toHaveBeenCalled(); }); }); + + it('should ensure that rendererFactory is called after initial styling is set', () => { + const myRendererFactory: RendererFactory3 = { + createRenderer: function(hostElement: RElement|null, rendererType: RendererType2|null): + Renderer3 { + if (hostElement) { + hostElement.classList.add('HOST_RENDERER'); + } + return document; + } + }; + const injector = Injector.create([ + {provide: RendererFactory2, useValue: myRendererFactory}, + ]); + + const hostNode = document.createElement('div'); + const componentRef = cf.create(injector, undefined, hostNode); + expect(hostNode.className).toEqual('HOST_COMPONENT HOST_RENDERER'); + }); }); }); diff --git a/packages/core/test/render3/instructions/lview_debug_spec.ts b/packages/core/test/render3/instructions/lview_debug_spec.ts new file mode 100644 index 0000000000..6b5a3f5800 --- /dev/null +++ b/packages/core/test/render3/instructions/lview_debug_spec.ts @@ -0,0 +1,147 @@ +/** + * @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 {TNodeDebug} from '@angular/core/src/render3/instructions/lview_debug'; +import {createTNode, createTView} from '@angular/core/src/render3/instructions/shared'; +import {TNodeType} from '@angular/core/src/render3/interfaces/node'; +import {LView, TView, TViewType} from '@angular/core/src/render3/interfaces/view'; +import {enterView, leaveView} from '@angular/core/src/render3/state'; +import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list'; + + +describe('lView_debug', () => { + const mockFirstUpdatePassLView: LView = [null, {firstUpdatePass: true}] as any; + beforeEach(() => enterView(mockFirstUpdatePassLView, null)); + afterEach(() => leaveView()); + + describe('TNode', () => { + let tNode !: TNodeDebug; + let tView !: TView; + beforeEach(() => { + tView = createTView(TViewType.Component, 0, null, 0, 0, null, null, null, null, null); + tNode = createTNode(tView, null !, TNodeType.Element, 0, '', null) as TNodeDebug; + }); + afterEach(() => tNode = tView = null !); + + describe('styling', () => { + it('should decode no styling', () => { + expect(tNode.styleBindings_).toEqual([null]); + expect(tNode.classBindings_).toEqual([null]); + }); + + it('should decode static styling', () => { + tNode.styles = 'color: blue'; + tNode.classes = 'STATIC'; + expect(tNode.styleBindings_).toEqual(['color: blue']); + expect(tNode.classBindings_).toEqual(['STATIC']); + }); + + it('should decode no-template property binding', () => { + tNode.classes = 'STATIC'; + insertTStylingBinding(tView.data, tNode, 'CLASS', 2, true, true); + insertTStylingBinding(tView.data, tNode, 'color', 4, true, false); + + expect(tNode.styleBindings_).toEqual([ + null, { + index: 4, + key: 'color', + isTemplate: false, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + expect(tNode.classBindings_).toEqual([ + 'STATIC', { + index: 2, + key: 'CLASS', + isTemplate: false, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + }); + + it('should decode template and directive property binding', () => { + tNode.classes = 'STATIC'; + insertTStylingBinding(tView.data, tNode, 'CLASS', 2, false, true); + insertTStylingBinding(tView.data, tNode, 'color', 4, false, false); + + expect(tNode.styleBindings_).toEqual([ + null, { + index: 4, + key: 'color', + isTemplate: true, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + expect(tNode.classBindings_).toEqual([ + 'STATIC', { + index: 2, + key: 'CLASS', + isTemplate: true, + prevDuplicate: false, + nextDuplicate: false, + prevIndex: 0, + nextIndex: 0, + } + ]); + + insertTStylingBinding(tView.data, tNode, STYLE_MAP_STYLING_KEY, 6, true, true); + insertTStylingBinding(tView.data, tNode, CLASS_MAP_STYLING_KEY, 8, true, false); + + expect(tNode.styleBindings_).toEqual([ + null, { + index: 8, + key: CLASS_MAP_STYLING_KEY, + isTemplate: false, + prevDuplicate: false, + nextDuplicate: true, + prevIndex: 0, + nextIndex: 4, + }, + { + index: 4, + key: 'color', + isTemplate: true, + prevDuplicate: true, + nextDuplicate: false, + prevIndex: 8, + nextIndex: 0, + } + ]); + expect(tNode.classBindings_).toEqual([ + 'STATIC', { + index: 6, + key: STYLE_MAP_STYLING_KEY, + isTemplate: false, + prevDuplicate: true, + nextDuplicate: true, + prevIndex: 0, + nextIndex: 2, + }, + { + index: 2, + key: 'CLASS', + isTemplate: true, + prevDuplicate: true, + nextDuplicate: false, + prevIndex: 6, + nextIndex: 0, + } + ]); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/core/test/render3/instructions_spec.ts b/packages/core/test/render3/instructions_spec.ts index be6f6b5fa2..09139881ad 100644 --- a/packages/core/test/render3/instructions_spec.ts +++ b/packages/core/test/render3/instructions_spec.ts @@ -7,11 +7,12 @@ */ import {NgForOfContext} from '@angular/common'; +import {getSortedClassName} from '@angular/core/testing/src/styling'; import {ɵɵdefineComponent} from '../../src/render3/definition'; import {RenderFlags, ɵɵattribute, ɵɵclassMap, ɵɵelement, ɵɵelementEnd, ɵɵelementStart, ɵɵproperty, ɵɵselect, ɵɵstyleMap, ɵɵstyleProp, ɵɵstyleSanitizer, ɵɵtemplate, ɵɵtext, ɵɵtextInterpolate1} from '../../src/render3/index'; import {AttributeMarker} from '../../src/render3/interfaces/node'; -import {bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass'; +import {SafeValue, bypassSanitizationTrustHtml, bypassSanitizationTrustResourceUrl, bypassSanitizationTrustScript, bypassSanitizationTrustStyle, bypassSanitizationTrustUrl, getSanitizationBypassType, unwrapSafeValue} from '../../src/sanitization/bypass'; import {ɵɵdefaultStyleSanitizer, ɵɵsanitizeHtml, ɵɵsanitizeResourceUrl, ɵɵsanitizeScript, ɵɵsanitizeStyle, ɵɵsanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer} from '../../src/sanitization/sanitizer'; import {SecurityContext} from '../../src/sanitization/security'; @@ -137,18 +138,20 @@ describe('instructions', () => { describe('styleProp', () => { it('should automatically sanitize unless a bypass operation is applied', () => { - const t = new TemplateFixture(() => { return createDiv(); }, () => {}, 1); - t.update(() => { - ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); - ɵɵstyleProp('background-image', 'url("http://server")'); - }); + let backgroundImage: string|SafeValue = 'url("http://server")'; + const t = new TemplateFixture( + () => { return createDiv(); }, + () => { + ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); + ɵɵstyleProp('background-image', backgroundImage); + }, + 2, 2); // nothing is set because sanitizer suppresses it. - expect(t.html).toEqual('
'); + expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) + .toEqual(''); - t.update(() => { - ɵɵstyleSanitizer(ɵɵdefaultStyleSanitizer); - ɵɵstyleProp('background-image', bypassSanitizationTrustStyle('url("http://server2")')); - }); + backgroundImage = bypassSanitizationTrustStyle('url("http://server2")'); + t.update(); expect((t.hostElement.firstChild as HTMLElement).style.getPropertyValue('background-image')) .toEqual('url("http://server2")'); }); @@ -160,9 +163,10 @@ describe('instructions', () => { function createDivWithStyle() { ɵɵelement(0, 'div', 0); } it('should add style', () => { - const fixture = new TemplateFixture( - createDivWithStyle, () => {}, 1, 0, null, null, null, undefined, attrs); - fixture.update(() => { ɵɵstyleMap({'background-color': 'red'}); }); + const fixture = new TemplateFixture(createDivWithStyle, () => { + ɵɵstyleMap({'background-color': 'red'}); + }, 1, 2, null, null, null, undefined, attrs); + fixture.update(); expect(fixture.html).toEqual('
'); }); @@ -184,7 +188,7 @@ describe('instructions', () => { 'width': 'width' }); }, - 1, 0, null, null, sanitizerInterceptor); + 1, 2, null, null, sanitizerInterceptor); const props = detectedValues.sort(); expect(props).toEqual([ @@ -197,9 +201,10 @@ describe('instructions', () => { function createDivWithStyling() { ɵɵelement(0, 'div'); } it('should add class', () => { - const fixture = - new TemplateFixture(createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1); - expect(fixture.html).toEqual('
'); + const fixture = new TemplateFixture( + createDivWithStyling, () => { ɵɵclassMap('multiple classes'); }, 1, 2); + const div = fixture.containerElement.querySelector('div.multiple') !; + expect(getSortedClassName(div)).toEqual('classes multiple'); }); }); diff --git a/packages/core/test/render3/styling_next/class_differ_spec.ts b/packages/core/test/render3/styling_next/class_differ_spec.ts index 7f91bfabcd..3c377803e5 100644 --- a/packages/core/test/render3/styling_next/class_differ_spec.ts +++ b/packages/core/test/render3/styling_next/class_differ_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {classIndexOf, computeClassChanges, removeClass, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ'; +import {classIndexOf, computeClassChanges, splitClassList, toggleClass} from '../../../src/render3/styling/class_differ'; describe('class differ', () => { describe('computeClassChanges', () => { @@ -81,25 +81,25 @@ describe('class differ', () => { }); }); - describe('removeClass', () => { + describe('toggleClass', () => { it('should remove class name from a class-list string', () => { - expect(removeClass('', '')).toEqual(''); - expect(removeClass('A', 'A')).toEqual(''); - expect(removeClass('AB', 'AB')).toEqual(''); - expect(removeClass('A B', 'A')).toEqual('B'); - expect(removeClass('A B', 'A')).toEqual('B'); + expect(toggleClass('', '', false)).toEqual(''); + expect(toggleClass('A', 'A', false)).toEqual(''); + expect(toggleClass('AB', 'AB', false)).toEqual(''); + expect(toggleClass('A B', 'A', false)).toEqual('B'); + expect(toggleClass('A B', 'A', false)).toEqual('B'); + expect(toggleClass('A B', 'B', false)).toEqual('A'); + expect(toggleClass(' B ', 'B', false)).toEqual(''); }); it('should not remove a sub-string', () => { - expect(removeClass('ABC', 'A')).toEqual('ABC'); - expect(removeClass('ABC', 'B')).toEqual('ABC'); - expect(removeClass('ABC', 'C')).toEqual('ABC'); - expect(removeClass('ABC', 'AB')).toEqual('ABC'); - expect(removeClass('ABC', 'BC')).toEqual('ABC'); + expect(toggleClass('ABC', 'A', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'B', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'C', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'AB', false)).toEqual('ABC'); + expect(toggleClass('ABC', 'BC', false)).toEqual('ABC'); }); - }); - describe('removeClass', () => { it('should toggle a class', () => { expect(toggleClass('', 'B', false)).toEqual(''); expect(toggleClass('', 'B', true)).toEqual('B'); diff --git a/packages/core/test/render3/styling_next/reconcile_spec.ts b/packages/core/test/render3/styling_next/reconcile_spec.ts index 2ae9160b21..8ee940a6c4 100644 --- a/packages/core/test/render3/styling_next/reconcile_spec.ts +++ b/packages/core/test/render3/styling_next/reconcile_spec.ts @@ -8,6 +8,7 @@ import {Renderer3, domRendererFactory3} from '@angular/core/src/render3/interfaces/renderer'; import {writeAndReconcileClass, writeAndReconcileStyle} from '@angular/core/src/render3/styling/reconcile'; +import {getSortedClassName, getSortedStyle} from '@angular/core/testing/src/styling'; describe('styling reconcile', () => { [document, domRendererFactory3.createRenderer(null, null)].forEach((renderer: Renderer3) => { @@ -84,41 +85,3 @@ describe('styling reconcile', () => { }); }); }); - -function getSortedClassName(element: HTMLElement): string { - const names: string[] = []; - const classList = element.classList || []; - for (let i = 0; i < classList.length; i++) { - const name = classList[i]; - if (names.indexOf(name) === -1) { - names.push(name); - } - } - names.sort(); - return names.join(' '); -} - -function getSortedStyle(element: HTMLElement): string { - const names: string[] = []; - const style = element.style; - // reading `style.color` is a work around for a bug in Domino. The issue is that Domino has stale - // value for `style.length`. It seems that reading a property from the element causes the stale - // value to be updated. (As of Domino v 2.1.3) - style.color; - for (let i = 0; i < style.length; i++) { - const name = style.item(i); - if (names.indexOf(name) === -1) { - names.push(name); - } - } - names.sort(); - let sorted = ''; - names.forEach(key => { - const value = style.getPropertyValue(key); - if (value != null && value !== '') { - if (sorted !== '') sorted += ' '; - sorted += key + ': ' + value + ';'; - } - }); - return sorted; -} \ No newline at end of file diff --git a/packages/core/test/render3/styling_next/style_binding_list_spec.ts b/packages/core/test/render3/styling_next/style_binding_list_spec.ts index 260427c060..45fe5238c3 100644 --- a/packages/core/test/render3/styling_next/style_binding_list_spec.ts +++ b/packages/core/test/render3/styling_next/style_binding_list_spec.ts @@ -12,7 +12,6 @@ import {TStylingKey, TStylingRange, getTStylingRangeNext, getTStylingRangeNextDu import {LView, TData} from '@angular/core/src/render3/interfaces/view'; import {enterView, leaveView} from '@angular/core/src/render3/state'; import {CLASS_MAP_STYLING_KEY, STYLE_MAP_STYLING_KEY, appendStyling, flushStyleBinding, insertTStylingBinding} from '@angular/core/src/render3/styling/style_binding_list'; -import {getStylingBindingHead} from '@angular/core/src/render3/styling/styling_debug'; import {newArray} from '@angular/core/src/util/array_utils'; describe('TNode styling linked list', () => { @@ -438,34 +437,47 @@ describe('TNode styling linked list', () => { it('should write basic value', () => { const fixture = new StylingFixture([['color']], false); fixture.setBinding(0, 'red'); - expect(fixture.flush(0)).toEqual('color: red'); + expect(fixture.flush(0)).toEqual('color: red;'); }); it('should chain values and allow update mid list', () => { const fixture = new StylingFixture([['color', {key: 'width', extra: 'px'}]], false); fixture.setBinding(0, 'red'); fixture.setBinding(1, '100'); - expect(fixture.flush(0)).toEqual('color: red; width: 100px'); + expect(fixture.flush(0)).toEqual('color: red; width: 100px;'); fixture.setBinding(0, 'blue'); fixture.setBinding(1, '200'); - expect(fixture.flush(1)).toEqual('color: red; width: 200px'); - expect(fixture.flush(0)).toEqual('color: blue; width: 200px'); + expect(fixture.flush(1)).toEqual('color: red; width: 200px;'); + expect(fixture.flush(0)).toEqual('color: blue; width: 200px;'); }); it('should remove duplicates', () => { const fixture = new StylingFixture([['color', 'color']], false); fixture.setBinding(0, 'red'); fixture.setBinding(1, 'blue'); - expect(fixture.flush(0)).toEqual('color: blue'); + expect(fixture.flush(0)).toEqual('color: blue;'); + }); + + it('should treat undefined values as previous value', () => { + const fixture = new StylingFixture([['color', 'color']], false); + fixture.setBinding(0, 'red'); + fixture.setBinding(1, undefined); + expect(fixture.flush(0)).toEqual('color: red;'); + }); + + it('should treat null value as removal', () => { + const fixture = new StylingFixture([['color']], false); + fixture.setBinding(0, null); + expect(fixture.flush(0)).toEqual(''); }); }); describe('appendStyling', () => { it('should append simple style', () => { - expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red'); - expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red'); + expect(appendStyling('', 'color', 'red', null, false, false)).toEqual('color: red;'); + expect(appendStyling('', 'color', 'red', null, true, false)).toEqual('color: red;'); expect(appendStyling('', 'color', 'red', null, false, true)).toEqual('color'); expect(appendStyling('', 'color', 'red', null, true, true)).toEqual('color'); expect(appendStyling('', 'color', true, null, true, true)).toEqual('color'); @@ -476,25 +488,25 @@ describe('TNode styling linked list', () => { it('should append simple style with suffix', () => { expect(appendStyling('', {key: 'width', extra: 'px'}, 100, null, false, false)) - .toEqual('width: 100px'); + .toEqual('width: 100px;'); }); it('should append simple style with sanitizer', () => { expect( appendStyling('', {key: 'width', extra: (v: any) => `-${v}-`}, 100, null, false, false)) - .toEqual('width: -100-'); + .toEqual('width: -100-;'); }); it('should append class/style', () => { - expect(appendStyling('color: white', 'color', 'red', null, false, false)) - .toEqual('color: white; color: red'); + expect(appendStyling('color: white;', 'color', 'red', null, false, false)) + .toEqual('color: white; color: red;'); expect(appendStyling('MY-CLASS', 'color', true, null, false, true)).toEqual('MY-CLASS color'); expect(appendStyling('MY-CLASS', 'color', false, null, true, true)).toEqual('MY-CLASS'); }); it('should remove existing', () => { - expect(appendStyling('color: white', 'color', 'blue', null, true, false)) - .toEqual('color: blue'); + expect(appendStyling('color: white;', 'color', 'blue', null, true, false)) + .toEqual('color: blue;'); expect(appendStyling('A YES B', 'YES', false, null, true, true)).toEqual('A B'); }); @@ -510,10 +522,10 @@ describe('TNode styling linked list', () => { it('should support maps for styles', () => { expect(appendStyling('', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) - .toEqual('A: a; B: b'); + .toEqual('A: a; B: b;'); expect(appendStyling( - 'A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) - .toEqual('C:_; A: a; B: b'); + 'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, {A: 'a', B: 'b'}, null, true, false)) + .toEqual('C:_; A: a; B: b;'); }); it('should support strings for classes', () => { @@ -525,11 +537,11 @@ describe('TNode styling linked list', () => { }); it('should support strings for styles', () => { - expect(appendStyling('A:a;B:b', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false)) - .toEqual('A:a;B:b; A : a ; B : b'); - expect( - appendStyling('A:_; B:_; C:_', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false)) - .toEqual('C:_; A: a; B: b'); + expect(appendStyling('A:a;B:b;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, false, false)) + .toEqual('A:a;B:b; A : a ; B : b;'); + expect(appendStyling( + 'A:_; B:_; C:_;', STYLE_MAP_STYLING_KEY, 'A : a ; B : b', null, true, false)) + .toEqual('C:_; A: a; B: b;'); }); it('should throw no arrays for styles', () => { @@ -560,7 +572,7 @@ describe('TNode styling linked list', () => { 'list-style: unsafe; ' + 'list-style-image: unsafe; ' + 'clip-path: unsafe; ' + - 'width: url(javascript:evil())'); + 'width: url(javascript:evil());'); // verify string expect(appendStyling( '', STYLE_MAP_STYLING_KEY, @@ -571,7 +583,7 @@ describe('TNode styling linked list', () => { 'list-style: url(javascript:evil());' + 'list-style-image: url(javascript:evil());' + 'clip-path: url(javascript:evil());' + - 'width: url(javascript:evil())' // should not sanitize + 'width: url(javascript:evil());' // should not sanitize , null, true, false)) .toEqual( @@ -582,7 +594,7 @@ describe('TNode styling linked list', () => { 'list-style: unsafe; ' + 'list-style-image: unsafe; ' + 'clip-path: unsafe; ' + - 'width: url(javascript:evil())'); + 'width: url(javascript:evil());'); }); }); }); @@ -632,6 +644,24 @@ function expectPriorityOrder(tData: TData, tNode: TNode, isClassBinding: boolean return expect(indexes); } + +/** + * Find the head of the styling binding linked list. + */ +export function getStylingBindingHead(tData: TData, tNode: TNode, isClassBinding: boolean): number { + let index = getTStylingRangePrev(isClassBinding ? tNode.classBindings : tNode.styleBindings); + while (true) { + const tStylingRange = tData[index + 1] as TStylingRange; + const prev = getTStylingRangePrev(tStylingRange); + if (prev === 0) { + // found head exit. + return index; + } else { + index = prev; + } + } +} + class StylingFixture { tData: TData = [null, null]; lView: LView = [null, null !] as any; diff --git a/packages/core/test/render3/styling_next/style_differ_spec.ts b/packages/core/test/render3/styling_next/style_differ_spec.ts index 6d7d51e995..b8ce8f0f33 100644 --- a/packages/core/test/render3/styling_next/style_differ_spec.ts +++ b/packages/core/test/render3/styling_next/style_differ_spec.ts @@ -7,8 +7,7 @@ */ import {StyleChangesMap, parseKeyValue, removeStyle} from '@angular/core/src/render3/styling/style_differ'; -import {consumeSeparatorWithWhitespace, consumeStyleValue} from '@angular/core/src/render3/styling/styling_parser'; -import {CharCode} from '@angular/core/src/util/char_code'; +import {getLastParsedValue, parseStyle} from '@angular/core/src/render3/styling/styling_parser'; import {sortedForEach} from './class_differ_spec'; describe('style differ', () => { @@ -31,6 +30,13 @@ describe('style differ', () => { expectParseValue(': text1 text2 ;🛑').toBe('text1 text2'); }); + it('should parse empty vale', () => { + expectParseValue(':').toBe(''); + expectParseValue(': ').toBe(''); + expectParseValue(': ;🛑').toBe(''); + expectParseValue(':;🛑').toBe(''); + }); + it('should parse quoted values', () => { expectParseValue(':""').toBe('""'); expectParseValue(':"\\\\"').toBe('"\\\\"'); @@ -54,11 +60,16 @@ describe('style differ', () => { }); describe('parseKeyValue', () => { - it('should parse empty value', () => { + it('should parse empty string', () => { expectParseKeyValue('').toEqual([]); expectParseKeyValue(' \n\t\r ').toEqual([]); }); + it('should parse empty value', () => { + expectParseKeyValue('key:').toEqual(['key', '', null]); + expectParseKeyValue('key: \n\t\r; ').toEqual(['key', '', null]); + }); + it('should prase single style', () => { expectParseKeyValue('width: 100px').toEqual(['width', '100px', null]); expectParseKeyValue(' width : 100px ;').toEqual(['width', '100px', null]); @@ -79,27 +90,27 @@ describe('style differ', () => { describe('removeStyle', () => { it('should remove no style', () => { expect(removeStyle('', 'foo')).toEqual(''); - expect(removeStyle('abc: bar', 'a')).toEqual('abc: bar'); - expect(removeStyle('abc: bar', 'b')).toEqual('abc: bar'); - expect(removeStyle('abc: bar', 'c')).toEqual('abc: bar'); - expect(removeStyle('abc: bar', 'bar')).toEqual('abc: bar'); + expect(removeStyle('abc: bar;', 'a')).toEqual('abc: bar;'); + expect(removeStyle('abc: bar;', 'b')).toEqual('abc: bar;'); + expect(removeStyle('abc: bar;', 'c')).toEqual('abc: bar;'); + expect(removeStyle('abc: bar;', 'bar')).toEqual('abc: bar;'); }); it('should remove all style', () => { - expect(removeStyle('foo: bar', 'foo')).toEqual(''); + expect(removeStyle('foo: bar;', 'foo')).toEqual(''); expect(removeStyle('foo: bar; foo: bar;', 'foo')).toEqual(''); }); it('should remove some of the style', () => { - expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b'); - expect(removeStyle('a: a; foo: bar; b: b', 'foo')).toEqual('a: a; b: b'); - expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c', 'foo')) - .toEqual('a: a; b: b; c: c'); + expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;'); + expect(removeStyle('a: a; foo: bar; b: b;', 'foo')).toEqual('a: a; b: b;'); + expect(removeStyle('a: a; foo: bar; b: b; foo: bar; c: c;', 'foo')) + .toEqual('a: a; b: b; c: c;'); }); it('should remove trailing ;', () => { - expect(removeStyle('a: a; foo: bar', 'foo')).toEqual('a: a'); - expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a'); + expect(removeStyle('a: a; foo: bar;', 'foo')).toEqual('a: a;'); + expect(removeStyle('a: a ; foo: bar ; ', 'foo')).toEqual('a: a ;'); }); }); }); @@ -114,11 +125,9 @@ function expectParseValue( text: string) { let stopIndex = text.indexOf('🛑'); if (stopIndex < 0) stopIndex = text.length; - const valueStart = consumeSeparatorWithWhitespace(text, 0, text.length, CharCode.COLON); - const valueEnd = consumeStyleValue(text, valueStart, text.length); - const valueSep = consumeSeparatorWithWhitespace(text, valueEnd, text.length, CharCode.SEMI_COLON); - expect(valueSep).toBe(stopIndex); - return expect(text.substring(valueStart, valueEnd)); + let i = parseStyle(text); + expect(i).toBe(stopIndex); + return expect(getLastParsedValue(text)); } function expectParseKeyValue(text: string) { diff --git a/packages/core/testing/src/styling.ts b/packages/core/testing/src/styling.ts new file mode 100644 index 0000000000..dcb4ba904f --- /dev/null +++ b/packages/core/testing/src/styling.ts @@ -0,0 +1,84 @@ +/** + * @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 + */ + +/** + * Returns element classes in form of a stable (sorted) string. + * + * @param element HTML Element. + * @returns Returns element classes in form of a stable (sorted) string. + */ +export function getSortedClassName(element: Element): string { + const names: string[] = Object.keys(getElementClasses(element)); + names.sort(); + return names.join(' '); +} + +/** + * Returns element classes in form of a map. + * + * @param element HTML Element. + * @returns Map of class values. + */ +export function getElementClasses(element: Element): {[key: string]: true} { + const classes: {[key: string]: true} = {}; + if (element.nodeType === Node.ELEMENT_NODE) { + const classList = element.classList; + for (let i = 0; i < classList.length; i++) { + const key = classList[i]; + classes[key] = true; + } + } + return classes; +} + +/** + * Returns element styles in form of a stable (sorted) string. + * + * @param element HTML Element. + * @returns Returns element styles in form of a stable (sorted) string. + */ +export function getSortedStyle(element: Element): string { + const styles = getElementStyles(element); + const names: string[] = Object.keys(styles); + names.sort(); + let sorted = ''; + names.forEach(key => { + const value = styles[key]; + if (value != null && value !== '') { + if (sorted !== '') sorted += ' '; + sorted += key + ': ' + value + ';'; + } + }); + return sorted; +} + +/** + * Returns element styles in form of a map. + * + * @param element HTML Element. + * @returns Map of style values. + */ +export function getElementStyles(element: Element): {[key: string]: string} { + const styles: {[key: string]: string} = {}; + if (element.nodeType === Node.ELEMENT_NODE) { + const style = (element as HTMLElement).style; + // reading `style.color` is a work around for a bug in Domino. The issue is that Domino has + // stale value for `style.length`. It seems that reading a property from the element causes the + // stale value to be updated. (As of Domino v 2.1.3) + style.color; + for (let i = 0; i < style.length; i++) { + const key = style.item(i); + const value = style.getPropertyValue(key); + if (value !== '') { + // Workaround for IE not clearing properties, instead it just sets them to blank value. + styles[key] = value; + } + } + } + return styles; +} \ No newline at end of file diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 14235e9cc7..4f25cd39d1 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -195,7 +195,10 @@ export declare class NgClass implements DoCheck { [klass: string]: any; }); constructor(_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer2); + applyChanges(): void; ngDoCheck(): void; + setClass(value: string): void; + setNgClass(value: any): void; } export declare class NgComponentOutlet implements OnChanges, OnDestroy { @@ -270,7 +273,9 @@ export declare class NgStyle implements DoCheck { [klass: string]: any; } | null); constructor(_ngEl: ElementRef, _differs: KeyValueDiffers, _renderer: Renderer2); + applyChanges(): void; ngDoCheck(): void; + setNgStyle(value: any): void; } export declare class NgSwitch { diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 6516edcf3a..b9ec3e0da5 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -703,8 +703,8 @@ export declare function ɵɵattributeInterpolate8(attrName: string, prefix: stri export declare function ɵɵattributeInterpolateV(attrName: string, values: any[], sanitizer?: SanitizerFn, namespace?: string): typeof ɵɵattributeInterpolateV; export declare function ɵɵclassMap(classes: { - [className: string]: any; -} | NO_CHANGE | string | null): void; + [className: string]: boolean | null | undefined; +} | Map | Set | string[] | string | null | undefined): void; export declare function ɵɵclassMapInterpolate1(prefix: string, v0: any, suffix: string): void; @@ -724,7 +724,7 @@ export declare function ɵɵclassMapInterpolate8(prefix: string, v0: any, i0: st export declare function ɵɵclassMapInterpolateV(values: any[]): void; -export declare function ɵɵclassProp(className: string, value: boolean | null): typeof ɵɵclassProp; +export declare function ɵɵclassProp(className: string, value: boolean | null | undefined): typeof ɵɵclassProp; export declare type ɵɵComponentDefWithMeta(predicate: Type | string[], export declare function ɵɵstyleMap(styles: { [styleName: string]: any; -} | NO_CHANGE | null): void; +} | Map | string | null | undefined): void; -export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null, suffix?: string | null): typeof ɵɵstyleProp; +export declare function ɵɵstyleProp(prop: string, value: string | number | SafeValue | null | undefined, suffix?: string | null): typeof ɵɵstyleProp; export declare function ɵɵstylePropInterpolate1(prop: string, prefix: string, v0: any, suffix: string, valueSuffix?: string | null): typeof ɵɵstylePropInterpolate1;