Skip to content

Commit cfb2d17

Browse files
matskoIgorMinar
authored andcommitted
feat(ivy): convert [ngStyle] and [ngClass] to use ivy styling bindings (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
1 parent d0e81eb commit cfb2d17

File tree

15 files changed

+921
-228
lines changed

15 files changed

+921
-228
lines changed

integration/_payload-limits.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"master": {
2222
"uncompressed": {
2323
"runtime": 1440,
24-
"main": 194626,
24+
"main": 207765,
2525
"polyfills": 38390
2626
}
2727
}

packages/common/src/common.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,15 @@ export {registerLocaleData} from './i18n/locale_data';
1919
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';
2020
export {parseCookieValue as ɵparseCookieValue} from './cookie';
2121
export {CommonModule, DeprecatedI18NPipesModule} from './common_module';
22-
export {NgClass, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';
22+
export {NgClass, NgClassBase, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgStyleBase, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';
2323
export {DOCUMENT} from './dom_tokens';
2424
export {AsyncPipe, DatePipe, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe, KeyValuePipe, KeyValue} from './pipes/index';
2525
export {DeprecatedDatePipe, DeprecatedCurrencyPipe, DeprecatedDecimalPipe, DeprecatedPercentPipe} from './pipes/deprecated/index';
2626
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';
2727
export {VERSION} from './version';
2828
export {ViewportScroller, NullViewportScroller as ɵNullViewportScroller} from './viewport_scroller';
29+
30+
export {NgClassImplProvider__POST_R3__ as ɵNgClassImplProvider__POST_R3__, NgClassR2Impl as ɵNgClassR2Impl, NgClassImpl as ɵNgClassImpl} from './directives/ng_class_impl';
31+
export {NgStyleImplProvider__POST_R3__ as ɵNgStyleImplProvider__POST_R3__, NgStyleR2Impl as ɵNgStyleR2Impl, NgStyleImpl as ɵNgStyleImpl} from './directives/ng_style_impl';
32+
export {ngStyleDirectiveDef__POST_R3__ as ɵngStyleDirectiveDef__POST_R3__} from './directives/ng_style';
33+
export {ngClassDirectiveDef__POST_R3__ as ɵngClassDirectiveDef__POST_R3__} from './directives/ng_class';

packages/common/src/directives/index.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
*/
88

99
import {Provider} from '@angular/core';
10-
11-
import {NgClass} from './ng_class';
10+
import {NgClass, NgClassBase} from './ng_class';
1211
import {NgComponentOutlet} from './ng_component_outlet';
1312
import {NgForOf, NgForOfContext} from './ng_for_of';
1413
import {NgIf, NgIfContext} from './ng_if';
1514
import {NgPlural, NgPluralCase} from './ng_plural';
16-
import {NgStyle} from './ng_style';
15+
import {NgStyle, NgStyleBase} from './ng_style';
1716
import {NgSwitch, NgSwitchCase, NgSwitchDefault} from './ng_switch';
1817
import {NgTemplateOutlet} from './ng_template_outlet';
1918

2019
export {
2120
NgClass,
21+
NgClassBase,
2222
NgComponentOutlet,
2323
NgForOf,
2424
NgForOfContext,
@@ -27,10 +27,11 @@ export {
2727
NgPlural,
2828
NgPluralCase,
2929
NgStyle,
30+
NgStyleBase,
3031
NgSwitch,
3132
NgSwitchCase,
3233
NgSwitchDefault,
33-
NgTemplateOutlet
34+
NgTemplateOutlet,
3435
};
3536

3637

packages/common/src/directives/ng_class.ts

Lines changed: 68 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,68 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8+
import {Directive, DoCheck, Input, ɵRenderFlags, ɵdefineDirective, ɵelementStyling, ɵelementStylingApply, ɵelementStylingMap} from '@angular/core';
89

9-
import {Directive, DoCheck, ElementRef, Input, IterableChanges, IterableDiffer, IterableDiffers, KeyValueChanges, KeyValueDiffer, KeyValueDiffers, Renderer2, ɵisListLikeIterable as isListLikeIterable, ɵstringify as stringify} from '@angular/core';
10+
import {NgClassImpl, NgClassImplProvider} from './ng_class_impl';
11+
12+
13+
14+
/*
15+
* NgClass (as well as NgStyle) behaves differently when loaded in the VE and when not.
16+
*
17+
* If the VE is present (which is for older versions of Angular) then NgClass will inject
18+
* the legacy diffing algorithm as a service and delegate all styling changes to that.
19+
*
20+
* If the VE is not present then NgStyle will normalize (through the injected service) and
21+
* then write all styling changes to the `[style]` binding directly (through a host binding).
22+
* Then Angular will notice the host binding change and treat the changes as styling
23+
* changes and apply them via the core styling instructions that exist within Angular.
24+
*/
25+
26+
// used when the VE is present
27+
export const ngClassDirectiveDef__PRE_R3__ = undefined;
28+
29+
// used when the VE is not present (note the directive will
30+
// never be instantiated normally because it is apart of a
31+
// base class)
32+
export const ngClassDirectiveDef__POST_R3__ = ɵdefineDirective({
33+
type: function() {} as any,
34+
selectors: null as any,
35+
factory: () => {},
36+
hostBindings: function(rf: ɵRenderFlags, ctx: any, elIndex: number) {
37+
if (rf & ɵRenderFlags.Create) {
38+
ɵelementStyling(null, null, null, ctx);
39+
}
40+
if (rf & ɵRenderFlags.Update) {
41+
ɵelementStylingMap(elIndex, ctx.getValue(), null, ctx);
42+
ɵelementStylingApply(elIndex, ctx);
43+
}
44+
}
45+
});
46+
47+
export const ngClassDirectiveDef = ngClassDirectiveDef__PRE_R3__;
48+
49+
/**
50+
* Serves as the base non-VE container for NgClass.
51+
*
52+
* While this is a base class that NgClass extends from, the
53+
* class itself acts as a container for non-VE code to setup
54+
* a link to the `[class]` host binding (via the static
55+
* `ngDirectiveDef` property on the class).
56+
*
57+
* Note that the `ngDirectiveDef` property's code is switched
58+
* depending if VE is present or not (this allows for the
59+
* binding code to be set only for newer versions of Angular).
60+
*
61+
* @publicApi
62+
*/
63+
export class NgClassBase {
64+
static ngDirectiveDef: any = ngClassDirectiveDef;
65+
66+
constructor(protected _delegate: NgClassImpl) {}
67+
68+
getValue() { return this._delegate.getValue(); }
69+
}
1070

1171
/**
1272
* @ngModule CommonModule
@@ -36,126 +96,17 @@ import {Directive, DoCheck, ElementRef, Input, IterableChanges, IterableDiffer,
3696
*
3797
* @publicApi
3898
*/
39-
@Directive({selector: '[ngClass]'})
40-
export class NgClass implements DoCheck {
41-
// TODO(issue/24571): remove '!'.
42-
private _iterableDiffer !: IterableDiffer<string>| null;
43-
// TODO(issue/24571): remove '!'.
44-
private _keyValueDiffer !: KeyValueDiffer<string, any>| null;
45-
private _initialClasses: string[] = [];
46-
// TODO(issue/24571): remove '!'.
47-
private _rawClass !: string[] | Set<string>| {[klass: string]: any};
48-
49-
constructor(
50-
private _iterableDiffers: IterableDiffers, private _keyValueDiffers: KeyValueDiffers,
51-
private _ngEl: ElementRef, private _renderer: Renderer2) {}
99+
@Directive({selector: '[ngClass]', providers: [NgClassImplProvider]})
100+
export class NgClass extends NgClassBase implements DoCheck {
101+
constructor(delegate: NgClassImpl) { super(delegate); }
52102

53103
@Input('class')
54-
set klass(value: string) {
55-
this._removeClasses(this._initialClasses);
56-
this._initialClasses = typeof value === 'string' ? value.split(/\s+/) : [];
57-
this._applyClasses(this._initialClasses);
58-
this._applyClasses(this._rawClass);
59-
}
104+
set klass(value: string) { this._delegate.setClass(value); }
60105

61-
@Input()
106+
@Input('ngClass')
62107
set ngClass(value: string|string[]|Set<string>|{[klass: string]: any}) {
63-
this._removeClasses(this._rawClass);
64-
this._applyClasses(this._initialClasses);
65-
66-
this._iterableDiffer = null;
67-
this._keyValueDiffer = null;
68-
69-
this._rawClass = typeof value === 'string' ? value.split(/\s+/) : value;
70-
71-
if (this._rawClass) {
72-
if (isListLikeIterable(this._rawClass)) {
73-
this._iterableDiffer = this._iterableDiffers.find(this._rawClass).create();
74-
} else {
75-
this._keyValueDiffer = this._keyValueDiffers.find(this._rawClass).create();
76-
}
77-
}
78-
}
79-
80-
ngDoCheck(): void {
81-
if (this._iterableDiffer) {
82-
const iterableChanges = this._iterableDiffer.diff(this._rawClass as string[]);
83-
if (iterableChanges) {
84-
this._applyIterableChanges(iterableChanges);
85-
}
86-
} else if (this._keyValueDiffer) {
87-
const keyValueChanges = this._keyValueDiffer.diff(this._rawClass as{[k: string]: any});
88-
if (keyValueChanges) {
89-
this._applyKeyValueChanges(keyValueChanges);
90-
}
91-
}
92-
}
93-
94-
private _applyKeyValueChanges(changes: KeyValueChanges<string, any>): void {
95-
changes.forEachAddedItem((record) => this._toggleClass(record.key, record.currentValue));
96-
changes.forEachChangedItem((record) => this._toggleClass(record.key, record.currentValue));
97-
changes.forEachRemovedItem((record) => {
98-
if (record.previousValue) {
99-
this._toggleClass(record.key, false);
100-
}
101-
});
108+
this._delegate.setNgClass(value);
102109
}
103110

104-
private _applyIterableChanges(changes: IterableChanges<string>): void {
105-
changes.forEachAddedItem((record) => {
106-
if (typeof record.item === 'string') {
107-
this._toggleClass(record.item, true);
108-
} else {
109-
throw new Error(
110-
`NgClass can only toggle CSS classes expressed as strings, got ${stringify(record.item)}`);
111-
}
112-
});
113-
114-
changes.forEachRemovedItem((record) => this._toggleClass(record.item, false));
115-
}
116-
117-
/**
118-
* Applies a collection of CSS classes to the DOM element.
119-
*
120-
* For argument of type Set and Array CSS class names contained in those collections are always
121-
* added.
122-
* For argument of type Map CSS class name in the map's key is toggled based on the value (added
123-
* for truthy and removed for falsy).
124-
*/
125-
private _applyClasses(rawClassVal: string[]|Set<string>|{[klass: string]: any}) {
126-
if (rawClassVal) {
127-
if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) {
128-
(<any>rawClassVal).forEach((klass: string) => this._toggleClass(klass, true));
129-
} else {
130-
Object.keys(rawClassVal).forEach(klass => this._toggleClass(klass, !!rawClassVal[klass]));
131-
}
132-
}
133-
}
134-
135-
/**
136-
* Removes a collection of CSS classes from the DOM element. This is mostly useful for cleanup
137-
* purposes.
138-
*/
139-
private _removeClasses(rawClassVal: string[]|Set<string>|{[klass: string]: any}) {
140-
if (rawClassVal) {
141-
if (Array.isArray(rawClassVal) || rawClassVal instanceof Set) {
142-
(<any>rawClassVal).forEach((klass: string) => this._toggleClass(klass, false));
143-
} else {
144-
Object.keys(rawClassVal).forEach(klass => this._toggleClass(klass, false));
145-
}
146-
}
147-
}
148-
149-
private _toggleClass(klass: string, enabled: boolean): void {
150-
klass = klass.trim();
151-
if (klass) {
152-
klass.split(/\s+/g).forEach(klass => {
153-
if (enabled) {
154-
this._renderer.addClass(this._ngEl.nativeElement, klass);
155-
} else {
156-
this._renderer.removeClass(this._ngEl.nativeElement, klass);
157-
}
158-
});
159-
}
160-
}
111+
ngDoCheck() { this._delegate.applyChanges(); }
161112
}

0 commit comments

Comments
 (0)