From cfb2d176f80ad52cfdbf8e31188b93adb73f4ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Mon, 18 Feb 2019 16:12:42 -0800 Subject: [PATCH] feat(ivy): convert [ngStyle] and [ngClass] to use ivy styling bindings (#28711) Prior to this fix, both the `NgStyle` and `NgClass` directives made use of `Renderer2` and this dependency raised issues for future versions of Angular that cannot inject it. This patch ensures that there are two versions of both directives: one for the VE and another for Ivy. Jira Issue: FW-882 PR Close #28711 --- integration/_payload-limits.json | 2 +- packages/common/src/common.ts | 7 +- packages/common/src/directives/index.ts | 9 +- packages/common/src/directives/ng_class.ts | 185 ++++------- .../common/src/directives/ng_class_impl.ts | 208 ++++++++++++ packages/common/src/directives/ng_style.ts | 120 ++++--- .../common/src/directives/ng_style_impl.ts | 114 +++++++ .../common/src/directives/styling_differ.ts | 302 ++++++++++++++++++ packages/core/src/render3/index.ts | 2 +- .../styling/class_and_style_bindings.ts | 84 +++-- packages/core/src/render3/styling/util.ts | 10 + .../test/bundling/animation_world/index.ts | 2 +- .../bundling/todo/bundle.golden_symbols.json | 6 + .../styling/class_and_style_bindings_spec.ts | 68 +++- tools/public_api_guard/common/common.d.ts | 30 +- 15 files changed, 921 insertions(+), 228 deletions(-) create mode 100644 packages/common/src/directives/ng_class_impl.ts create mode 100644 packages/common/src/directives/ng_style_impl.ts create mode 100644 packages/common/src/directives/styling_differ.ts diff --git a/integration/_payload-limits.json b/integration/_payload-limits.json index 62088d7ca7..0be7eba374 100644 --- a/integration/_payload-limits.json +++ b/integration/_payload-limits.json @@ -21,7 +21,7 @@ "master": { "uncompressed": { "runtime": 1440, - "main": 194626, + "main": 207765, "polyfills": 38390 } } diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 1bfa538ce0..750a5ef48b 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -19,10 +19,15 @@ export {registerLocaleData} from './i18n/locale_data'; export {Plural, NumberFormatStyle, FormStyle, Time, TranslationWidth, FormatWidth, NumberSymbol, WeekDay, getNumberOfCurrencyDigits, getCurrencySymbol, getLocaleDayPeriods, getLocaleDayNames, getLocaleMonthNames, getLocaleId, getLocaleEraNames, getLocaleWeekEndRange, getLocaleFirstDayOfWeek, getLocaleDateFormat, getLocaleDateTimeFormat, getLocaleExtraDayPeriodRules, getLocaleExtraDayPeriods, getLocalePluralCase, getLocaleTimeFormat, getLocaleNumberSymbol, getLocaleNumberFormat, getLocaleCurrencyName, getLocaleCurrencySymbol} from './i18n/locale_data_api'; export {parseCookieValue as ɵparseCookieValue} from './cookie'; export {CommonModule, DeprecatedI18NPipesModule} from './common_module'; -export {NgClass, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; +export {NgClass, NgClassBase, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgStyleBase, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index'; export {DOCUMENT} from './dom_tokens'; export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe, KeyValuePipe, KeyValue} from './pipes/index'; export {DeprecatedDatePipe, DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from './pipes/deprecated/index'; export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id'; export {VERSION} from './version'; export {ViewportScroller, NullViewportScroller as ɵNullViewportScroller} from './viewport_scroller'; + +export {NgClassImplProvider__POST_R3__ as ɵNgClassImplProvider__POST_R3__, NgClassR2Impl as ɵNgClassR2Impl, NgClassImpl as ɵNgClassImpl} from './directives/ng_class_impl'; +export {NgStyleImplProvider__POST_R3__ as ɵNgStyleImplProvider__POST_R3__, NgStyleR2Impl as ɵNgStyleR2Impl, NgStyleImpl as ɵNgStyleImpl} from './directives/ng_style_impl'; +export {ngStyleDirectiveDef__POST_R3__ as ɵngStyleDirectiveDef__POST_R3__} from './directives/ng_style'; +export {ngClassDirectiveDef__POST_R3__ as ɵngClassDirectiveDef__POST_R3__} from './directives/ng_class'; diff --git a/packages/common/src/directives/index.ts b/packages/common/src/directives/index.ts index c2dcabe37d..214e2f9aa5 100644 --- a/packages/common/src/directives/index.ts +++ b/packages/common/src/directives/index.ts @@ -7,18 +7,18 @@ */ import {Provider} from '@angular/core'; - -import {NgClass} from './ng_class'; +import {NgClass, NgClassBase} from './ng_class'; import {NgComponentOutlet} from './ng_component_outlet'; import {NgForOf, NgForOfContext} from './ng_for_of'; import {NgIf, NgIfContext} from './ng_if'; import {NgPlural, NgPluralCase} from './ng_plural'; -import {NgStyle} from './ng_style'; +import {NgStyle, NgStyleBase} from './ng_style'; import {NgSwitch, NgSwitchCase, NgSwitchDefault} from './ng_switch'; import {NgTemplateOutlet} from './ng_template_outlet'; export { NgClass, + NgClassBase, NgComponentOutlet, NgForOf, NgForOfContext, @@ -27,10 +27,11 @@ export { NgPlural, NgPluralCase, NgStyle, + NgStyleBase, NgSwitch, NgSwitchCase, NgSwitchDefault, - NgTemplateOutlet + NgTemplateOutlet, }; diff --git a/packages/common/src/directives/ng_class.ts b/packages/common/src/directives/ng_class.ts index 8e0bb039b5..d6f23c83d7 100644 --- a/packages/common/src/directives/ng_class.ts +++ b/packages/common/src/directives/ng_class.ts @@ -5,8 +5,68 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {Directive, DoCheck, Input, ɵRenderFlags, ɵdefineDirective, ɵelementStyling, ɵelementStylingApply, ɵelementStylingMap} from '@angular/core'; -import {Directive, DoCheck, ElementRef, Input, IterableChanges, IterableDiffer, IterableDiffers, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer2, ɵisListLikeIterable as isListLikeIterable, ɵstringify as stringify} from '@angular/core'; +import {NgClassImpl, NgClassImplProvider} from './ng_class_impl'; + + + +/* + * NgClass (as well as NgStyle) behaves differently when loaded in the VE and when not. + * + * If the VE is present (which is for older versions of Angular) then NgClass will inject + * the legacy diffing algorithm as a service and delegate all styling changes to that. + * + * If the VE is not present then NgStyle will normalize (through the injected service) and + * then write all styling changes to the `[style]` binding directly (through a host binding). + * Then Angular will notice the host binding change and treat the changes as styling + * changes and apply them via the core styling instructions that exist within Angular. + */ + +// used when the VE is present +export const ngClassDirectiveDef__PRE_R3__ = undefined; + +// used when the VE is not present (note the directive will +// never be instantiated normally because it is apart of a +// base class) +export const ngClassDirectiveDef__POST_R3__ = ɵdefineDirective({ + type: function() {} as any, + selectors: null as any, + factory: () => {}, + hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { + if (rf & ɵRenderFlags.Create) { + ɵelementStyling(null, null, null, ctx); + } + if (rf & ɵRenderFlags.Update) { + ɵelementStylingMap(elIndex, ctx.getValue(), null, ctx); + ɵelementStylingApply(elIndex, ctx); + } + } +}); + +export const ngClassDirectiveDef = ngClassDirectiveDef__PRE_R3__; + +/** + * Serves as the base non-VE container for NgClass. + * + * While this is a base class that NgClass extends from, the + * class itself acts as a container for non-VE code to setup + * a link to the `[class]` host binding (via the static + * `ngDirectiveDef` property on the class). + * + * Note that the `ngDirectiveDef` property's code is switched + * depending if VE is present or not (this allows for the + * binding code to be set only for newer versions of Angular). + * + * @publicApi + */ +export class NgClassBase { + static ngDirectiveDef: any = ngClassDirectiveDef; + + constructor(protected _delegate: NgClassImpl) {} + + getValue() { return this._delegate.getValue(); } +} /** * @ngModule CommonModule @@ -36,126 +96,17 @@ import {Directive, DoCheck, ElementRef, Input, IterableChanges, IterableDiffer, * * @publicApi */ -@Directive({selector: '[ngClass]'}) -export class NgClass implements DoCheck { - // TODO(issue/24571): remove '!'. - private _iterableDiffer !: IterableDiffer| null; - // TODO(issue/24571): remove '!'. - private _keyValueDiffer !: KeyValueDiffer| null; - private _initialClasses: string[] = []; - // TODO(issue/24571): remove '!'. - private _rawClass !: string[] | Set| {[klass: string]: any}; - - constructor( - private _iterableDiffers: IterableDiffers, private _keyValueDiffers: KeyValueDiffers, - private _ngEl: ElementRef, private _renderer: Renderer2) {} +@Directive({selector: '[ngClass]', providers: [NgClassImplProvider]}) +export class NgClass extends NgClassBase implements DoCheck { + constructor(delegate: NgClassImpl) { super(delegate); } @Input('class') - set klass(value: string) { - this._removeClasses(this._initialClasses); - this._initialClasses = typeof value === 'string' ? value.split(/\s+/) : []; - this._applyClasses(this._initialClasses); - this._applyClasses(this._rawClass); - } + set klass(value: string) { this._delegate.setClass(value); } - @Input() + @Input('ngClass') set ngClass(value: string|string[]|Set|{[klass: string]: any}) { - this._removeClasses(this._rawClass); - this._applyClasses(this._initialClasses); - - this._iterableDiffer = null; - this._keyValueDiffer = null; - - this._rawClass = typeof value === 'string' ? value.split(/\s+/) : value; - - if (this._rawClass) { - if (isListLikeIterable(this._rawClass)) { - this._iterableDiffer = this._iterableDiffers.find(this._rawClass).create(); - } else { - this._keyValueDiffer = this._keyValueDiffers.find(this._rawClass).create(); - } - } + this._delegate.setNgClass(value); } - ngDoCheck(): void { - if (this._iterableDiffer) { - const iterableChanges = this._iterableDiffer.diff(this._rawClass as string[]); - if (iterableChanges) { - this._applyIterableChanges(iterableChanges); - } - } else if (this._keyValueDiffer) { - const keyValueChanges = this._keyValueDiffer.diff(this._rawClass as{[k: string]: any}); - if (keyValueChanges) { - this._applyKeyValueChanges(keyValueChanges); - } - } - } - - private _applyKeyValueChanges(changes: KeyValueChanges): void { - changes.forEachAddedItem((record) => this._toggleClass(record.key, record.currentValue)); - changes.forEachChangedItem((record) => this._toggleClass(record.key, record.currentValue)); - changes.forEachRemovedItem((record) => { - if (record.previousValue) { - this._toggleClass(record.key, false); - } - }); - } - - private _applyIterableChanges(changes: IterableChanges): void { - changes.forEachAddedItem((record) => { - if (typeof record.item === 'string') { - this._toggleClass(record.item, true); - } else { - throw new Error( - `NgClass can only toggle CSS classes expressed as strings, got ${stringify(record.item)}`); - } - }); - - changes.forEachRemovedItem((record) => this._toggleClass(record.item, false)); - } - - /** - * Applies a collection of CSS classes to the DOM element. - * - * For argument of type Set and Array CSS class names contained in those collections are always - * added. - * For argument of type Map CSS class name in the map's key is toggled based on the value (added - * for truthy and removed for falsy). - */ - private _applyClasses(rawClassVal: string[]|Set|{[klass: string]: any}) { - if (rawClassVal) { - if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) { - (rawClassVal).forEach((klass: string) => this._toggleClass(klass, true)); - } else { - Object.keys(rawClassVal).forEach(klass => this._toggleClass(klass, !!rawClassVal[klass])); - } - } - } - - /** - * Removes a collection of CSS classes from the DOM element. This is mostly useful for cleanup - * purposes. - */ - private _removeClasses(rawClassVal: string[]|Set|{[klass: string]: any}) { - if (rawClassVal) { - if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) { - (rawClassVal).forEach((klass: string) => this._toggleClass(klass, false)); - } else { - Object.keys(rawClassVal).forEach(klass => this._toggleClass(klass, false)); - } - } - } - - private _toggleClass(klass: string, enabled: boolean): void { - klass = klass.trim(); - if (klass) { - klass.split(/\s+/g).forEach(klass => { - if (enabled) { - this._renderer.addClass(this._ngEl.nativeElement, klass); - } else { - this._renderer.removeClass(this._ngEl.nativeElement, klass); - } - }); - } - } + ngDoCheck() { this._delegate.applyChanges(); } } diff --git a/packages/common/src/directives/ng_class_impl.ts b/packages/common/src/directives/ng_class_impl.ts new file mode 100644 index 0000000000..ef48b17292 --- /dev/null +++ b/packages/common/src/directives/ng_class_impl.ts @@ -0,0 +1,208 @@ +/** + * @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 {ElementRef, Injectable, IterableChanges, IterableDiffer, IterableDiffers, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer2, ɵisListLikeIterable as isListLikeIterable, ɵstringify as stringify} from '@angular/core'; + +import {StylingDiffer, StylingDifferOptions} from './styling_differ'; + +/** + * Used as a token for an injected service within the NgClass directive. + * + * NgClass behaves differenly whether or not VE is being used or not. If + * present then the legacy ngClass diffing algorithm will be used as an + * injected service. Otherwise the new diffing algorithm (which delegates + * to the `[class]` binding) will be used. This toggle behavior is done so + * via the ivy_switch mechanism. + */ +export abstract class NgClassImpl { + abstract setClass(value: string): void; + abstract setNgClass(value: string|string[]|Set|{[klass: string]: any}): void; + abstract applyChanges(): void; + abstract getValue(): {[key: string]: any}|null; +} + +@Injectable() +export class NgClassR2Impl implements NgClassImpl { + // TODO(issue/24571): remove '!'. + private _iterableDiffer !: IterableDiffer| null; + // TODO(issue/24571): remove '!'. + private _keyValueDiffer !: KeyValueDiffer| null; + private _initialClasses: string[] = []; + // TODO(issue/24571): remove '!'. + private _rawClass !: string[] | Set| {[klass: string]: any}; + + constructor( + private _iterableDiffers: IterableDiffers, private _keyValueDiffers: KeyValueDiffers, + private _ngEl: ElementRef, private _renderer: Renderer2) {} + + getValue() { return null; } + + setClass(value: string) { + this._removeClasses(this._initialClasses); + this._initialClasses = typeof value === 'string' ? value.split(/\s+/) : []; + this._applyClasses(this._initialClasses); + this._applyClasses(this._rawClass); + } + + setNgClass(value: string) { + this._removeClasses(this._rawClass); + this._applyClasses(this._initialClasses); + + this._iterableDiffer = null; + this._keyValueDiffer = null; + + this._rawClass = typeof value === 'string' ? value.split(/\s+/) : value; + + if (this._rawClass) { + if (isListLikeIterable(this._rawClass)) { + this._iterableDiffer = this._iterableDiffers.find(this._rawClass).create(); + } else { + this._keyValueDiffer = this._keyValueDiffers.find(this._rawClass).create(); + } + } + } + + applyChanges() { + if (this._iterableDiffer) { + const iterableChanges = this._iterableDiffer.diff(this._rawClass as string[]); + if (iterableChanges) { + this._applyIterableChanges(iterableChanges); + } + } else if (this._keyValueDiffer) { + const keyValueChanges = this._keyValueDiffer.diff(this._rawClass as{[k: string]: any}); + if (keyValueChanges) { + this._applyKeyValueChanges(keyValueChanges); + } + } + } + + private _applyKeyValueChanges(changes: KeyValueChanges): void { + changes.forEachAddedItem((record) => this._toggleClass(record.key, record.currentValue)); + changes.forEachChangedItem((record) => this._toggleClass(record.key, record.currentValue)); + changes.forEachRemovedItem((record) => { + if (record.previousValue) { + this._toggleClass(record.key, false); + } + }); + } + + private _applyIterableChanges(changes: IterableChanges): void { + changes.forEachAddedItem((record) => { + if (typeof record.item === 'string') { + this._toggleClass(record.item, true); + } else { + throw new Error( + `NgClass can only toggle CSS classes expressed as strings, got ${stringify(record.item)}`); + } + }); + + changes.forEachRemovedItem((record) => this._toggleClass(record.item, false)); + } + + /** + * Applies a collection of CSS classes to the DOM element. + * + * For argument of type Set and Array CSS class names contained in those collections are always + * added. + * For argument of type Map CSS class name in the map's key is toggled based on the value (added + * for truthy and removed for falsy). + */ + private _applyClasses(rawClassVal: string[]|Set|{[klass: string]: any}) { + if (rawClassVal) { + if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) { + (rawClassVal).forEach((klass: string) => this._toggleClass(klass, true)); + } else { + Object.keys(rawClassVal).forEach(klass => this._toggleClass(klass, !!rawClassVal[klass])); + } + } + } + + /** + * Removes a collection of CSS classes from the DOM element. This is mostly useful for cleanup + * purposes. + */ + private _removeClasses(rawClassVal: string[]|Set|{[klass: string]: any}) { + if (rawClassVal) { + if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) { + (rawClassVal).forEach((klass: string) => this._toggleClass(klass, false)); + } else { + Object.keys(rawClassVal).forEach(klass => this._toggleClass(klass, false)); + } + } + } + + private _toggleClass(klass: string, enabled: boolean): void { + klass = klass.trim(); + if (klass) { + klass.split(/\s+/g).forEach(klass => { + if (enabled) { + this._renderer.addClass(this._ngEl.nativeElement, klass); + } else { + this._renderer.removeClass(this._ngEl.nativeElement, klass); + } + }); + } + } +} + +@Injectable() +export class NgClassR3Impl implements NgClassImpl { + private _value: {[key: string]: boolean}|null = null; + private _ngClassDiffer = new StylingDiffer<{[key: string]: boolean}|null>( + 'NgClass', StylingDifferOptions.TrimProperties| + StylingDifferOptions.AllowSubKeys| + StylingDifferOptions.AllowStringValue|StylingDifferOptions.ForceAsMap); + private _classStringDiffer: StylingDiffer<{[key: string]: boolean}>|null = null; + + getValue() { return this._value; } + + setClass(value: string) { + // early exit incase the binding gets emitted as an empty value which + // means there is no reason to instantiate and diff the values... + if (!value && !this._classStringDiffer) return; + + this._classStringDiffer = this._classStringDiffer || + new StylingDiffer('class', + StylingDifferOptions.AllowStringValue | StylingDifferOptions.ForceAsMap); + this._classStringDiffer.setValue(value); + } + + setNgClass(value: string|string[]|Set|{[klass: string]: any}) { + this._ngClassDiffer.setValue(value); + } + + applyChanges() { + const classChanged = + this._classStringDiffer ? this._classStringDiffer.hasValueChanged() : false; + const ngClassChanged = this._ngClassDiffer.hasValueChanged(); + if (classChanged || ngClassChanged) { + let value = this._ngClassDiffer.value; + if (this._classStringDiffer) { + let classValue = this._classStringDiffer.value; + if (classValue) { + value = value ? {...classValue, ...value} : classValue; + } + } + this._value = value; + } + } +} + +// the implementation for both NgStyleR2Impl and NgStyleR3Impl are +// not ivy_switch'd away, instead they are only hooked up into the +// DI via NgStyle's directive's provider property. +export const NgClassImplProvider__PRE_R3__ = { + provide: NgClassImpl, + useClass: NgClassR2Impl +}; + +export const NgClassImplProvider__POST_R3__ = { + provide: NgClassImpl, + useClass: NgClassR3Impl +}; + +export const NgClassImplProvider = NgClassImplProvider__PRE_R3__; diff --git a/packages/common/src/directives/ng_style.ts b/packages/common/src/directives/ng_style.ts index 0bd23baaa2..4ae2dfcafc 100644 --- a/packages/common/src/directives/ng_style.ts +++ b/packages/common/src/directives/ng_style.ts @@ -5,8 +5,68 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {Directive, DoCheck, Input, ɵRenderFlags, ɵdefineDirective, ɵelementStyling, ɵelementStylingApply, ɵelementStylingMap} from '@angular/core'; -import {Directive, DoCheck, ElementRef, Input, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer2} from '@angular/core'; +import {NgStyleImpl, NgStyleImplProvider} from './ng_style_impl'; + + + +/* + * NgStyle (as well as NgClass) behaves differently when loaded in the VE and when not. + * + * If the VE is present (which is for older versions of Angular) then NgStyle will inject + * the legacy diffing algorithm as a service and delegate all styling changes to that. + * + * If the VE is not present then NgStyle will normalize (through the injected service) and + * then write all styling changes to the `[style]` binding directly (through a host binding). + * Then Angular will notice the host binding change and treat the changes as styling + * changes and apply them via the core styling instructions that exist within Angular. + */ + +// used when the VE is present +export const ngStyleDirectiveDef__PRE_R3__ = undefined; + +// used when the VE is not present (note the directive will +// never be instantiated normally because it is apart of a +// base class) +export const ngStyleDirectiveDef__POST_R3__ = ɵdefineDirective({ + type: function() {} as any, + selectors: null as any, + factory: () => {}, + hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) { + if (rf & ɵRenderFlags.Create) { + ɵelementStyling(null, null, null, ctx); + } + if (rf & ɵRenderFlags.Update) { + ɵelementStylingMap(elIndex, null, ctx.getValue(), ctx); + ɵelementStylingApply(elIndex, ctx); + } + } +}); + +export const ngStyleDirectiveDef = ngStyleDirectiveDef__PRE_R3__; + +/** + * Serves as the base non-VE container for NgStyle. + * + * While this is a base class that NgStyle extends from, the + * class itself acts as a container for non-VE code to setup + * a link to the `[style]` host binding (via the static + * `ngDirectiveDef` property on the class). + * + * Note that the `ngDirectiveDef` property's code is switched + * depending if VE is present or not (this allows for the + * binding code to be set only for newer versions of Angular). + * + * @publicApi + */ +export class NgStyleBase { + static ngDirectiveDef: any = ngStyleDirectiveDef; + + constructor(protected _delegate: NgStyleImpl) {} + + getValue() { return this._delegate.getValue(); } +} /** * @ngModule CommonModule @@ -44,58 +104,12 @@ import {Directive, DoCheck, ElementRef, Input, KeyValueChanges, KeyValueDiffer, * * @publicApi */ -@Directive({selector: '[ngStyle]'}) -export class NgStyle implements DoCheck { - // TODO(issue/24571): remove '!'. - private _ngStyle !: {[key: string]: string}; - // TODO(issue/24571): remove '!'. - private _differ !: KeyValueDiffer; +@Directive({selector: '[ngStyle]', providers: [NgStyleImplProvider]}) +export class NgStyle extends NgStyleBase implements DoCheck { + constructor(delegate: NgStyleImpl) { super(delegate); } - constructor( - private _differs: KeyValueDiffers, private _ngEl: ElementRef, private _renderer: Renderer2) {} + @Input('ngStyle') + set ngStyle(value: {[klass: string]: any}|null) { this._delegate.setNgStyle(value); } - @Input() - set ngStyle( - /** - * A map of style properties, specified as colon-separated - * key-value pairs. - * * The key is a style name, with an optional `.` suffix - * (such as 'top.px', 'font-style.em'). - * * The value is an expression to be evaluated. - */ - values: {[key: string]: string}) { - this._ngStyle = values; - if (!this._differ && values) { - this._differ = this._differs.find(values).create(); - } - } - - /** - * Applies the new styles if needed. - */ - ngDoCheck() { - if (this._differ) { - const changes = this._differ.diff(this._ngStyle); - if (changes) { - this._applyChanges(changes); - } - } - } - - private _applyChanges(changes: KeyValueChanges): void { - changes.forEachRemovedItem((record) => this._setStyle(record.key, null)); - changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue)); - changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue)); - } - - private _setStyle(nameAndUnit: string, value: string|number|null|undefined): void { - const [name, unit] = nameAndUnit.split('.'); - value = value != null && unit ? `${value}${unit}` : value; - - if (value != null) { - this._renderer.setStyle(this._ngEl.nativeElement, name, value as string); - } else { - this._renderer.removeStyle(this._ngEl.nativeElement, name); - } - } + ngDoCheck() { this._delegate.applyChanges(); } } diff --git a/packages/common/src/directives/ng_style_impl.ts b/packages/common/src/directives/ng_style_impl.ts new file mode 100644 index 0000000000..5c0e87566d --- /dev/null +++ b/packages/common/src/directives/ng_style_impl.ts @@ -0,0 +1,114 @@ +/** + * @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 {ElementRef, Injectable, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer2} from '@angular/core'; + +import {StylingDiffer, StylingDifferOptions} from './styling_differ'; + +/** + * Used as a token for an injected service within the NgStyle directive. + * + * NgStyle behaves differenly whether or not VE is being used or not. If + * present then the legacy ngClass diffing algorithm will be used as an + * injected service. Otherwise the new diffing algorithm (which delegates + * to the `[style]` binding) will be used. This toggle behavior is done so + * via the ivy_switch mechanism. + */ +export abstract class NgStyleImpl { + abstract getValue(): {[key: string]: any}|null; + abstract setNgStyle(value: {[key: string]: any}|null): void; + abstract applyChanges(): void; +} + +@Injectable() +export class NgStyleR2Impl implements NgStyleImpl { + // TODO(issue/24571): remove '!'. + private _ngStyle !: {[key: string]: string}; + // TODO(issue/24571): remove '!'. + private _differ !: KeyValueDiffer; + + constructor( + private _ngEl: ElementRef, private _differs: KeyValueDiffers, private _renderer: Renderer2) {} + + getValue() { return null; } + + /** + * A map of style properties, specified as colon-separated + * key-value pairs. + * * The key is a style name, with an optional `.` suffix + * (such as 'top.px', 'font-style.em'). + * * The value is an expression to be evaluated. + */ + setNgStyle(values: {[key: string]: string}) { + this._ngStyle = values; + if (!this._differ && values) { + this._differ = this._differs.find(values).create(); + } + } + + /** + * Applies the new styles if needed. + */ + applyChanges() { + if (this._differ) { + const changes = this._differ.diff(this._ngStyle); + if (changes) { + this._applyChanges(changes); + } + } + } + + private _applyChanges(changes: KeyValueChanges): void { + changes.forEachRemovedItem((record) => this._setStyle(record.key, null)); + changes.forEachAddedItem((record) => this._setStyle(record.key, record.currentValue)); + changes.forEachChangedItem((record) => this._setStyle(record.key, record.currentValue)); + } + + private _setStyle(nameAndUnit: string, value: string|number|null|undefined): void { + const [name, unit] = nameAndUnit.split('.'); + value = value != null && unit ? `${value}${unit}` : value; + + if (value != null) { + this._renderer.setStyle(this._ngEl.nativeElement, name, value as string); + } else { + this._renderer.removeStyle(this._ngEl.nativeElement, name); + } + } +} + +@Injectable() +export class NgStyleR3Impl implements NgStyleImpl { + private _differ = + new StylingDiffer<{[key: string]: any}|null>('NgStyle', StylingDifferOptions.AllowUnits); + + private _value: {[key: string]: any}|null = null; + + getValue() { return this._value; } + + setNgStyle(value: {[key: string]: any}|null) { this._differ.setValue(value); } + + applyChanges() { + if (this._differ.hasValueChanged()) { + this._value = this._differ.value; + } + } +} + +// the implementation for both NgClassR2Impl and NgClassR3Impl are +// not ivy_switch'd away, instead they are only hooked up into the +// DI via NgStyle's directive's provider property. +export const NgStyleImplProvider__PRE_R3__ = { + provide: NgStyleImpl, + useClass: NgStyleR2Impl +}; + +export const NgStyleImplProvider__POST_R3__ = { + provide: NgStyleImpl, + useClass: NgStyleR3Impl +}; + +export const NgStyleImplProvider = NgStyleImplProvider__PRE_R3__; diff --git a/packages/common/src/directives/styling_differ.ts b/packages/common/src/directives/styling_differ.ts new file mode 100644 index 0000000000..5cc63988a0 --- /dev/null +++ b/packages/common/src/directives/styling_differ.ts @@ -0,0 +1,302 @@ +/** + * @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 + */ + +/** + * Used to diff and convert ngStyle/ngClass instructions into [style] and [class] bindings. + * + * ngStyle and ngClass both accept various forms of input and behave differently than that + * of how [style] and [class] behave in Angular. + * + * The differences are: + * - ngStyle and ngClass both **watch** their binding values for changes each time CD runs + * while [style] and [class] bindings do not (they check for identity changes) + * - ngStyle allows for unit-based keys (e.g. `{'max-width.px':value}`) and [style] does not + * - ngClass supports arrays of class values and [class] only accepts map and string values + * - ngClass allows for multiple className keys (space-separated) within an array or map + * (as the * key) while [class] only accepts a simple key/value map object + * + * Having Angular understand and adapt to all the different forms of behavior is complicated + * and unnecessary. Instead, ngClass and ngStyle should have their input values be converted + * into something that the core-level [style] and [class] bindings understand. + * + * This [StylingDiffer] class handles this conversion by creating a new input value each time + * the inner representation of the binding value have changed. + * + * ## Why do we care about ngStyle/ngClass? + * The styling algorithm code (documented inside of `render3/interfaces/styling.ts`) needs to + * respect and understand the styling values emitted through ngStyle and ngClass (when they + * are present and used in a template). + * + * Instead of having these directives manage styling on their own, they should be included + * into the Angular styling algorithm that exists for [style] and [class] bindings. + * + * Here's why: + * + * - If ngStyle/ngClass is used in combination with [style]/[class] bindings then the + * styles and classes would fall out of sync and be applied and updated at + * inconsistent times + * - Both ngClass/ngStyle do not respect [class.name] and [style.prop] bindings + * (they will write over them given the right combination of events) + * + * ``` + * + *
...
+ * + * + *
...
+ * ``` + * - ngClass/ngStyle were written as a directives and made use of maps, closures and other + * expensive data structures which were evaluated each time CD runs + */ +export class StylingDiffer { + public readonly value: T|null = null; + + private _lastSetValue: {[key: string]: any}|string|string[]|null = null; + private _lastSetValueType: StylingDifferValueTypes = StylingDifferValueTypes.Null; + private _lastSetValueIdentityChange = false; + + constructor(private _name: string, private _options: StylingDifferOptions) {} + + /** + * Sets (updates) the styling value within the differ. + * + * Only when `hasValueChanged` is called then this new value will be evaluted + * and checked against the previous value. + * + * @param value the new styling value provided from the ngClass/ngStyle binding + */ + setValue(value: {[key: string]: any}|string[]|string|null) { + if (Array.isArray(value)) { + this._lastSetValueType = StylingDifferValueTypes.Array; + } else if (value instanceof Set) { + this._lastSetValueType = StylingDifferValueTypes.Set; + } else if (value && typeof value === 'string') { + if (!(this._options & StylingDifferOptions.AllowStringValue)) { + throw new Error(this._name + ' string values are not allowed'); + } + this._lastSetValueType = StylingDifferValueTypes.String; + } else { + this._lastSetValueType = value ? StylingDifferValueTypes.Map : StylingDifferValueTypes.Null; + } + + this._lastSetValueIdentityChange = true; + this._lastSetValue = value || null; + } + + /** + * Determines whether or not the value has changed. + * + * This function can be called right after `setValue()` is called, but it can also be + * called incase the existing value (if it's a collection) changes internally. If the + * value is indeed a collection it will do the necessary diffing work and produce a + * new object value as assign that to `value`. + * + * @returns whether or not the value has changed in some way. + */ + hasValueChanged(): boolean { + let valueHasChanged = this._lastSetValueIdentityChange; + if (!valueHasChanged && !(this._lastSetValueType & StylingDifferValueTypes.Collection)) + return false; + + let finalValue: {[key: string]: any}|string|null = null; + const trimValues = (this._options & StylingDifferOptions.TrimProperties) ? true : false; + const parseOutUnits = (this._options & StylingDifferOptions.AllowUnits) ? true : false; + const allowSubKeys = (this._options & StylingDifferOptions.AllowSubKeys) ? true : false; + + switch (this._lastSetValueType) { + // case 1: [input]="string" + case StylingDifferValueTypes.String: + const tokens = (this._lastSetValue as string).split(/\s+/g); + if (this._options & StylingDifferOptions.ForceAsMap) { + finalValue = {}; + tokens.forEach((token, i) => (finalValue as{[key: string]: any})[token] = true); + } else { + finalValue = tokens.reduce((str, token, i) => str + (i ? ' ' : '') + token); + } + break; + + // case 2: [input]="{key:value}" + case StylingDifferValueTypes.Map: + const map: {[key: string]: any} = this._lastSetValue as{[key: string]: any}; + const keys = Object.keys(map); + if (!valueHasChanged) { + if (this.value) { + // we know that the classExp value exists and that it is + // a map (otherwise an identity change would have occurred) + valueHasChanged = mapHasChanged(keys, this.value as{[key: string]: any}, map); + } else { + valueHasChanged = true; + } + } + + if (valueHasChanged) { + finalValue = + bulidMapFromValues(this._name, trimValues, parseOutUnits, allowSubKeys, map, keys); + } + break; + + // case 3a: [input]="[str1, str2, ...]" + // case 3b: [input]="Set" + case StylingDifferValueTypes.Array: + case StylingDifferValueTypes.Set: + const values = Array.from(this._lastSetValue as string[] | Set); + if (!valueHasChanged) { + const keys = Object.keys(this.value !); + valueHasChanged = !arrayEqualsArray(keys, values); + } + if (valueHasChanged) { + finalValue = + bulidMapFromValues(this._name, trimValues, parseOutUnits, allowSubKeys, values); + } + break; + + // case 4: [input]="null|undefined" + default: + finalValue = null; + break; + } + + if (valueHasChanged) { + (this as any).value = finalValue !; + } + + return valueHasChanged; + } +} + +/** + * Various options that are consumed by the [StylingDiffer] class. + */ +export const enum StylingDifferOptions { + None = 0b00000, + TrimProperties = 0b00001, + AllowSubKeys = 0b00010, + AllowStringValue = 0b00100, + AllowUnits = 0b01000, + ForceAsMap = 0b10000, +} + +/** + * The different types of inputs that the [StylingDiffer] can deal with + */ +const enum StylingDifferValueTypes { + Null = 0b0000, + String = 0b0001, + Map = 0b0010, + Array = 0b0100, + Set = 0b1000, + Collection = 0b1110, +} + + +/** + * builds and returns a map based on the values input value + * + * If the `keys` param is provided then the `values` param is treated as a + * string map. Otherwise `values` is treated as a string array. + */ +function bulidMapFromValues( + errorPrefix: string, trim: boolean, parseOutUnits: boolean, allowSubKeys: boolean, + values: {[key: string]: any} | string[], keys?: string[]) { + const map: {[key: string]: any} = {}; + if (keys) { + // case 1: map + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + key = trim ? key.trim() : key; + const value = (values as{[key: string]: any})[key]; + setMapValues(map, key, value, parseOutUnits, allowSubKeys); + } + } else { + // case 2: array + for (let i = 0; i < values.length; i++) { + let value = (values as string[])[i]; + assertValidValue(errorPrefix, value); + value = trim ? value.trim() : value; + setMapValues(map, value, true, false, allowSubKeys); + } + } + + return map; +} + +function assertValidValue(errorPrefix: string, value: any) { + if (typeof value !== 'string') { + throw new Error( + `${errorPrefix} can only toggle CSS classes expressed as strings, got ${value}`); + } +} + +function setMapValues( + map: {[key: string]: any}, key: string, value: any, parseOutUnits: boolean, + allowSubKeys: boolean) { + if (allowSubKeys && key.indexOf(' ') > 0) { + const innerKeys = key.split(/\s+/g); + for (let j = 0; j < innerKeys.length; j++) { + setIndividualMapValue(map, innerKeys[j], value, parseOutUnits); + } + } else { + setIndividualMapValue(map, key, value, parseOutUnits); + } +} + +function setIndividualMapValue( + map: {[key: string]: any}, key: string, value: any, parseOutUnits: boolean) { + if (parseOutUnits) { + const values = normalizeStyleKeyAndValue(key, value); + value = values.value; + key = values.key; + } + map[key] = value; +} + +function normalizeStyleKeyAndValue(key: string, value: string | null) { + const index = key.indexOf('.'); + if (index > 0) { + const unit = key.substr(index + 1); // ignore the . ([width.px]="'40'" => "40px") + key = key.substring(0, index); + if (value != null) { // we should not convert null values to string + value += unit; + } + } + return {key, value}; +} + +function mapHasChanged(keys: string[], a: {[key: string]: any}, b: {[key: string]: any}) { + const oldKeys = Object.keys(a); + const newKeys = keys; + + // the keys are different which means the map changed + if (!arrayEqualsArray(oldKeys, newKeys)) { + return true; + } + + for (let i = 0; i < newKeys.length; i++) { + const key = newKeys[i]; + if (a[key] !== b[key]) { + return true; + } + } + + return false; +} + +function arrayEqualsArray(a: any[] | null, b: any[] | null) { + if (a && b) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (b.indexOf(a[i]) === -1) return false; + } + return true; + } + return false; +} diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 3b415f02a4..5e76b64721 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -105,7 +105,7 @@ export { export {NgModuleFactory, NgModuleRef, NgModuleType} from './ng_module_ref'; export { - AttributeMarker + AttributeMarker } from './interfaces/node'; export { diff --git a/packages/core/src/render3/styling/class_and_style_bindings.ts b/packages/core/src/render3/styling/class_and_style_bindings.ts index 3123eceec4..7c7e559411 100644 --- a/packages/core/src/render3/styling/class_and_style_bindings.ts +++ b/packages/core/src/render3/styling/class_and_style_bindings.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ import {StyleSanitizeFn} from '../../sanitization/style_sanitizer'; -import {assertNotEqual} from '../../util/assert'; import {EMPTY_ARRAY, EMPTY_OBJ} from '../empty'; import {AttributeMarker, TAttributes} from '../interfaces/node'; import {BindingStore, BindingType, Player, PlayerBuilder, PlayerFactory, PlayerIndex} from '../interfaces/player'; @@ -418,16 +417,9 @@ export function updateContextWithBindings( const directiveMultiStylesStartIndex = multiStylesStartIndex + totalCurrentStyleBindings * StylingIndex.Size; const cachedStyleMapIndex = cachedStyleMapValues.length; - - // this means that ONLY directive style styling (like ngStyle) was used - // therefore the root directive will still need to be filled in - if (directiveIndex > 0 && - cachedStyleMapValues.length <= MapBasedOffsetValuesIndex.ValuesStartPosition) { - cachedStyleMapValues.push(0, directiveMultiStylesStartIndex, null, 0); - } - - cachedStyleMapValues.push( - 0, directiveMultiStylesStartIndex, null, filteredStyleBindingNames.length); + registerMultiMapEntry( + context, directiveIndex, false, directiveMultiStylesStartIndex, + filteredStyleBindingNames.length); for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < cachedStyleMapIndex; i += MapBasedOffsetValuesIndex.Size) { @@ -441,16 +433,9 @@ export function updateContextWithBindings( const directiveMultiClassesStartIndex = multiClassesStartIndex + totalCurrentClassBindings * StylingIndex.Size; const cachedClassMapIndex = cachedClassMapValues.length; - - // this means that ONLY directive class styling (like ngClass) was used - // therefore the root directive will still need to be filled in - if (directiveIndex > 0 && - cachedClassMapValues.length <= MapBasedOffsetValuesIndex.ValuesStartPosition) { - cachedClassMapValues.push(0, directiveMultiClassesStartIndex, null, 0); - } - - cachedClassMapValues.push( - 0, directiveMultiClassesStartIndex, null, filteredClassBindingNames.length); + registerMultiMapEntry( + context, directiveIndex, true, directiveMultiClassesStartIndex, + filteredClassBindingNames.length); for (let i = MapBasedOffsetValuesIndex.ValuesStartPosition; i < cachedClassMapIndex; i += MapBasedOffsetValuesIndex.Size) { @@ -619,7 +604,7 @@ export function updateStylingMap( } const multiStylesStartIndex = getMultiStylesStartIndex(context); - let multiClassesStartIndex = getMultiClassStartIndex(context); + let multiClassesStartIndex = getMultiClassesStartIndex(context); let multiClassesEndIndex = context.length; if (!ignoreAllStyleUpdates) { @@ -862,6 +847,7 @@ function patchStylingMapIntoContext( valuesEntryShapeChange = true; // some values are missing const ctxValue = getValue(context, ctxIndex); const ctxFlag = getPointers(context, ctxIndex); + const ctxDirective = getDirectiveIndexFromEntry(context, ctxIndex); if (ctxValue != null) { valuesEntryShapeChange = true; } @@ -1293,7 +1279,7 @@ function getMultiStartIndex(context: StylingContext): number { return getMultiOrSingleIndex(context[StylingIndex.MasterFlagPosition]) as number; } -function getMultiClassStartIndex(context: StylingContext): number { +function getMultiClassesStartIndex(context: StylingContext): number { const classCache = context[StylingIndex.CachedMultiClasses]; return classCache [MapBasedOffsetValuesIndex.ValuesStartPosition + @@ -1625,15 +1611,31 @@ export function getDirectiveIndexFromEntry(context: StylingContext, index: numbe return value & DirectiveOwnerAndPlayerBuilderIndex.BitMask; } -function getDirectiveIndexFromRegistry(context: StylingContext, directive: any) { - const index = - getDirectiveRegistryValuesIndexOf(context[StylingIndex.DirectiveRegistryPosition], directive); - ngDevMode && - assertNotEqual( - index, -1, - `The provided directive ${directive} has not been allocated to the element\'s style/class bindings`); - return index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0; - // return index / DirectiveRegistryValuesIndex.Size; +function getDirectiveIndexFromRegistry(context: StylingContext, directiveRef: any) { + let directiveIndex: number; + + const dirs = context[StylingIndex.DirectiveRegistryPosition]; + let index = getDirectiveRegistryValuesIndexOf(dirs, directiveRef); + if (index === -1) { + // if the directive was not allocated then this means that styling is + // being applied in a dynamic way AFTER the element was already instantiated + index = dirs.length; + directiveIndex = index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0; + + dirs.push(null, null, null, null); + dirs[index + DirectiveRegistryValuesIndex.DirectiveValueOffset] = directiveRef; + dirs[index + DirectiveRegistryValuesIndex.DirtyFlagOffset] = false; + dirs[index + DirectiveRegistryValuesIndex.SinglePropValuesIndexOffset] = -1; + + const classesStartIndex = + getMultiClassesStartIndex(context) || StylingIndex.SingleStylesStartPosition; + registerMultiMapEntry(context, directiveIndex, true, context.length); + registerMultiMapEntry(context, directiveIndex, false, classesStartIndex); + } else { + directiveIndex = index > 0 ? index / DirectiveRegistryValuesIndex.Size : 0; + } + + return directiveIndex; } function getDirectiveRegistryValuesIndexOf( @@ -1936,3 +1938,21 @@ function hyphenate(value: string): string { return value.replace( /[a-z][A-Z]/g, match => `${match.charAt(0)}-${match.charAt(1).toLowerCase()}`); } + +function registerMultiMapEntry( + context: StylingContext, directiveIndex: number, entryIsClassBased: boolean, + startPosition: number, count = 0) { + const cachedValues = + context[entryIsClassBased ? StylingIndex.CachedMultiClasses : StylingIndex.CachedMultiStyles]; + if (directiveIndex > 0) { + const limit = MapBasedOffsetValuesIndex.ValuesStartPosition + + (directiveIndex * MapBasedOffsetValuesIndex.Size); + while (cachedValues.length < limit) { + // this means that ONLY directive class styling (like ngClass) was used + // therefore the root directive will still need to be filled in as well + // as any other directive spaces incase they only used static values + cachedValues.push(0, startPosition, null, 0); + } + } + cachedValues.push(0, startPosition, null, count); +} diff --git a/packages/core/src/render3/styling/util.ts b/packages/core/src/render3/styling/util.ts index b2b72800dd..599613cf3b 100644 --- a/packages/core/src/render3/styling/util.ts +++ b/packages/core/src/render3/styling/util.ts @@ -56,6 +56,16 @@ export function allocStylingContext( element: RElement | null, templateStyleContext: StylingContext): StylingContext { // each instance gets a copy const context = templateStyleContext.slice() as any as StylingContext; + + // the HEADER values contain arrays which also need + // to be copied over into the new context + for (let i = 0; i < StylingIndex.SingleStylesStartPosition; i++) { + const value = templateStyleContext[i]; + if (Array.isArray(value)) { + context[i] = value.slice(); + } + } + context[StylingIndex.ElementPosition] = element; // this will prevent any other directives from extending the context diff --git a/packages/core/test/bundling/animation_world/index.ts b/packages/core/test/bundling/animation_world/index.ts index 0f4e892a92..61ca860d1f 100644 --- a/packages/core/test/bundling/animation_world/index.ts +++ b/packages/core/test/bundling/animation_world/index.ts @@ -91,7 +91,7 @@ class BoxWithOverriddenStylesComponent { + [style]="{'border-radius':'50px', 'border': '50px solid teal'}" [ngStyle]="{transform:'rotate(50deg)'}"> `, }) diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index dd22242097..bec6fedfe0 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -722,6 +722,9 @@ { "name": "getMatchingBindingIndex" }, + { + "name": "getMultiClassesStartIndex" + }, { "name": "getMultiOrSingleIndex" }, @@ -1100,6 +1103,9 @@ { "name": "refreshDynamicEmbeddedViews" }, + { + "name": "registerMultiMapEntry" + }, { "name": "registerPostOrderHooks" }, diff --git a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts index d109d8ec73..d91a41aa2f 100644 --- a/packages/core/test/render3/styling/class_and_style_bindings_spec.ts +++ b/packages/core/test/render3/styling/class_and_style_bindings_spec.ts @@ -1693,18 +1693,6 @@ describe('style and class based bindings', () => { ]); }); - it('should throw an error if a directive is provided that isn\'t registered', () => { - const template = createEmptyStylingContext(); - const knownDir = {}; - const unknownDir = {}; - updateContextWithBindings(template, knownDir, null, ['color']); - - const ctx = allocStylingContext(element, template); - updateStyleProp(ctx, 0, 'blue', knownDir); - - expect(() => { updateStyleProp(ctx, 0, 'blue', unknownDir); }).toThrow(); - }); - it('should use a different sanitizer when a different directive\'s binding is updated', () => { const getStyles = trackStylesFactory(); @@ -1766,6 +1754,62 @@ describe('style and class based bindings', () => { expect(((ctx[colorIndex] as number) & StylingFlags.Sanitize) > 0).toBeFalsy(); expect(getStyles(ctx, dirWithoutSanitizer)).toEqual({color: 'green'}); }); + + it('should automatically register a styling context with a foreign directive if styling is applied with said directive', + () => { + const template = createEmptyStylingContext(); + const knownDir = {}; + const foreignDir = {}; + updateContextWithBindings(template, knownDir); + + const ctx = allocStylingContext(element, template); + expect(ctx[StylingIndex.DirectiveRegistryPosition]).toEqual([ + null, // + -1, // + false, // + null, // + knownDir, // + 2, // + false, // + null, // + ]); + + expect(ctx[StylingIndex.CachedMultiClasses].length) + .toEqual(template[StylingIndex.CachedMultiClasses].length); + expect(ctx[StylingIndex.CachedMultiClasses]).toEqual([0, 0, 9, null, 0, 0, 9, null, 0]); + + expect(ctx[StylingIndex.CachedMultiStyles].length) + .toEqual(template[StylingIndex.CachedMultiStyles].length); + expect(ctx[StylingIndex.CachedMultiStyles]).toEqual([0, 0, 9, null, 0, 0, 9, null, 0]); + + updateStylingMap(ctx, 'foo', null, foreignDir); + expect(ctx[StylingIndex.DirectiveRegistryPosition]).toEqual([ + null, // + -1, // + false, // + null, // + knownDir, // + 2, // + false, // + null, // + foreignDir, // + -1, // + true, // + null, // + ]); + + expect(ctx[StylingIndex.CachedMultiClasses].length) + .not.toEqual(template[StylingIndex.CachedMultiClasses].length); + expect(ctx[StylingIndex.CachedMultiClasses]).toEqual([ + 1, 0, 9, null, 0, 0, 9, null, 0, 0, 9, 'foo', 1 + ]); + + expect(ctx[StylingIndex.CachedMultiStyles].length) + .not.toEqual(template[StylingIndex.CachedMultiStyles].length); + expect(ctx[StylingIndex.CachedMultiStyles]).toEqual([ + 0, 0, 9, null, 0, 0, 9, null, 0, 0, 9, null, 0 + ]); + }); }); it('should skip issuing style updates if there is nothing to update upon first render', () => { diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index c77d0dac54..6ca180685f 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -207,15 +207,24 @@ export declare class LowerCasePipe implements PipeTransform { transform(value: string): string; } -export declare class NgClass implements DoCheck { +export declare class NgClass extends NgClassBase implements DoCheck { klass: string; ngClass: string | string[] | Set | { [klass: string]: any; }; - constructor(_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer2); + constructor(delegate: NgClassImpl); ngDoCheck(): void; } +export declare class NgClassBase { + protected _delegate: NgClassImpl; + constructor(_delegate: NgClassImpl); + getValue(): { + [key: string]: any; + } | null; + static ngDirectiveDef: any; +} + export declare class NgComponentOutlet implements OnChanges, OnDestroy { ngComponentOutlet: Type; ngComponentOutletContent: any[][]; @@ -283,14 +292,23 @@ export declare class NgPluralCase { constructor(value: string, template: TemplateRef, viewContainer: ViewContainerRef, ngPlural: NgPlural); } -export declare class NgStyle implements DoCheck { +export declare class NgStyle extends NgStyleBase implements DoCheck { ngStyle: { - [key: string]: string; - }; - constructor(_differs: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer2); + [klass: string]: any; + } | null; + constructor(delegate: NgStyleImpl); ngDoCheck(): void; } +export declare class NgStyleBase { + protected _delegate: NgStyleImpl; + constructor(_delegate: NgStyleImpl); + getValue(): { + [key: string]: any; + } | null; + static ngDirectiveDef: any; +} + export declare class NgSwitch { ngSwitch: any; }