Skip to content

Commit

Permalink
feat(ivy): convert [ngStyle] and [ngClass] to use ivy styling bindings (
Browse files Browse the repository at this point in the history
angular#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 angular#28711
  • Loading branch information
matsko authored and benlesh committed Feb 20, 2019
1 parent 2cd9c6e commit a7e1c0c
Show file tree
Hide file tree
Showing 15 changed files with 921 additions and 228 deletions.
2 changes: 1 addition & 1 deletion integration/_payload-limits.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"master": {
"uncompressed": {
"runtime": 1440,
"main": 194626,
"main": 207765,
"polyfills": 38390
}
}
Expand Down
7 changes: 6 additions & 1 deletion packages/common/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
9 changes: 5 additions & 4 deletions packages/common/src/directives/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -27,10 +27,11 @@ export {
NgPlural,
NgPluralCase,
NgStyle,
NgStyleBase,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,
NgTemplateOutlet
NgTemplateOutlet,
};


Expand Down
185 changes: 68 additions & 117 deletions packages/common/src/directives/ng_class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string>| null;
// TODO(issue/24571): remove '!'.
private _keyValueDiffer !: KeyValueDiffer<string, any>| null;
private _initialClasses: string[] = [];
// TODO(issue/24571): remove '!'.
private _rawClass !: string[] | Set<string>| {[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<string>|{[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();
}
}
}

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<string, any>): 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);
}
});
this._delegate.setNgClass(value);
}

private _applyIterableChanges(changes: IterableChanges<string>): 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<string>|{[klass: string]: any}) {
if (rawClassVal) {
if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) {
(<any>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<string>|{[klass: string]: any}) {
if (rawClassVal) {
if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) {
(<any>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(); }
}
Loading

0 comments on commit a7e1c0c

Please sign in to comment.