Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit 81c79e0

Browse files
fix(ngClass,ngStyle): support ChangeDetectionStrategy.OnPush
@see https://plnkr.co/edit/jCICQ1GXFnzFqFTFlXwh?p=preview fixes #206.
1 parent 980d412 commit 81c79e0

File tree

4 files changed

+138
-62
lines changed

4 files changed

+138
-62
lines changed

src/lib/flexbox/api/base.ts

+27-19
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,10 @@ export type StyleDefinition = string|{[property: string]: string|number};
2222

2323
/** Abstract base class for the Layout API styling directives. */
2424
export abstract class BaseFxDirective implements OnDestroy {
25-
/**
26-
* Original dom Elements CSS display style
27-
*/
28-
protected _display;
2925

30-
/**
31-
* MediaQuery Activation Tracker
32-
*/
33-
protected _mqActivation: ResponsiveActivation;
34-
35-
/**
36-
* Dictionary of input keys with associated values
37-
*/
38-
protected _inputMap = {};
26+
get hasMQListener() {
27+
return !!this._mqActivation;
28+
}
3929

4030
/**
4131
*
@@ -46,6 +36,7 @@ export abstract class BaseFxDirective implements OnDestroy {
4636
this._display = this._getDisplayStyle();
4737
}
4838

39+
4940
// *********************************************
5041
// Accessor Methods
5142
// *********************************************
@@ -172,12 +163,15 @@ export abstract class BaseFxDirective implements OnDestroy {
172163
protected _listenForMediaQueryChanges(key: string,
173164
defaultValue: any,
174165
onMediaQueryChange: MediaQuerySubscriber): ResponsiveActivation { // tslint:disable-line:max-line-length
175-
let keyOptions = new KeyOptions(key, defaultValue, this._inputMap);
176-
return this._mqActivation = new ResponsiveActivation(
177-
keyOptions,
178-
this._mediaMonitor,
179-
(change) => onMediaQueryChange.call(this, change)
180-
);
166+
if ( !this._mqActivation ) {
167+
let keyOptions = new KeyOptions(key, defaultValue, this._inputMap);
168+
this._mqActivation = new ResponsiveActivation(
169+
keyOptions,
170+
this._mediaMonitor,
171+
(change) => onMediaQueryChange.call(this, change)
172+
);
173+
}
174+
return this._mqActivation;
181175
}
182176

183177
/**
@@ -201,4 +195,18 @@ export abstract class BaseFxDirective implements OnDestroy {
201195
return this._mqActivation.hasKeyValue(key);
202196
}
203197

198+
/**
199+
* Original dom Elements CSS display style
200+
*/
201+
protected _display;
202+
203+
/**
204+
* MediaQuery Activation Tracker
205+
*/
206+
protected _mqActivation: ResponsiveActivation;
207+
208+
/**
209+
* Dictionary of input keys with associated values
210+
*/
211+
protected _inputMap = {};
204212
}

src/lib/flexbox/api/class.spec.ts

+36-11
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,36 @@ describe('class directive', () => {
6565
});
6666
});
6767

68-
it('should keep existing class selector', () => {
68+
it('should NOT keep the existing class', () => {
6969
fixture = createTestComponent(`
7070
<div class="existing-class" ngClass.xs="xs-class">
7171
</div>
7272
`);
7373

7474
expectNativeEl(fixture).toHaveCssClass('existing-class');
75+
7576
activateMediaQuery('xs');
7677
expectNativeEl(fixture).not.toHaveCssClass('existing-class');
78+
expectNativeEl(fixture).toHaveCssClass('xs-class');
79+
80+
activateMediaQuery('lg');
81+
expectNativeEl(fixture).toHaveCssClass('existing-class');
82+
expectNativeEl(fixture).not.toHaveCssClass('xs-class');
83+
});
84+
85+
86+
it('should keep allow removal of class selector', () => {
87+
fixture = createTestComponent(`
88+
<div
89+
class="existing-class"
90+
[ngClass.xs]="{'xs-class':true, 'existing-class':false}">
91+
</div>
92+
`);
93+
94+
expectNativeEl(fixture).toHaveCssClass('existing-class');
95+
activateMediaQuery('xs');
96+
expectNativeEl(fixture).not.toHaveCssClass('existing-class');
97+
expectNativeEl(fixture).toHaveCssClass('xs-class');
7798

7899
activateMediaQuery('lg');
79100
expectNativeEl(fixture).toHaveCssClass('existing-class');
@@ -90,6 +111,7 @@ describe('class directive', () => {
90111
expectNativeEl(fixture).toHaveCssClass('existing-class');
91112
activateMediaQuery('xs');
92113
expectNativeEl(fixture).toHaveCssClass('existing-class');
114+
expectNativeEl(fixture).toHaveCssClass('xs-class');
93115

94116
activateMediaQuery('lg');
95117
expectNativeEl(fixture).toHaveCssClass('existing-class');
@@ -112,21 +134,23 @@ describe('class directive', () => {
112134

113135
it('should work with ngClass object notation', () => {
114136
fixture = createTestComponent(`
115-
<div [ngClass]="{'xs-1': hasXs1, 'xs-3': hasXs3}"
116-
[ngClass.xs]="{'xs-1': hasXs1, 'xs-2': hasXs2}">
137+
<div [ngClass]="{'x1': hasX1, 'x3': hasX3}"
138+
[ngClass.xs]="{'x1': hasX1, 'x2': hasX2}">
117139
</div>
118140
`);
119-
activateMediaQuery('xs');
120-
expectNativeEl(fixture, {hasXs1: true, hasXs2: false}).toHaveCssClass('xs-1');
121-
expectNativeEl(fixture, {hasXs1: true, hasXs2: false}).not.toHaveCssClass('xs-2');
141+
expectNativeEl(fixture, {hasX1: true, hasX2: true, hasX3: true}).toHaveCssClass('x1');
142+
expectNativeEl(fixture, {hasX1: true, hasX2: true, hasX3: true}).not.toHaveCssClass('x2');
143+
expectNativeEl(fixture, {hasX1: true, hasX2: true, hasX3: true}).toHaveCssClass('x3');
122144

123-
expectNativeEl(fixture, {hasXs1: false, hasXs2: true}).toHaveCssClass('xs-2');
124-
expectNativeEl(fixture, {hasXs1: false, hasXs2: true}).not.toHaveCssClass('xs-1');
145+
activateMediaQuery('X');
146+
expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: false}).toHaveCssClass('x1');
147+
expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: false}).not.toHaveCssClass('x2');
148+
expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: false}).not.toHaveCssClass('x3');
125149

126150
activateMediaQuery('md');
127-
expectNativeEl(fixture, {hasXs1: true, hasX2: false, hasXs3: true}).toHaveCssClass('xs-3');
128-
expectNativeEl(fixture, {hasXs1: true, hasX2: false, hasXs3: true}).not.toHaveCssClass('xs-2');
129-
expectNativeEl(fixture, {hasXs1: true, hasX2: false, hasXs3: true}).toHaveCssClass('xs-1');
151+
expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: true}).toHaveCssClass('x1');
152+
expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: true}).not.toHaveCssClass('x2');
153+
expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: true}).toHaveCssClass('x3');
130154
});
131155

132156
it('should work with ngClass array notation', () => {
@@ -151,6 +175,7 @@ describe('class directive', () => {
151175
export class TestClassComponent implements OnInit {
152176
hasXs1: boolean;
153177
hasXs2: boolean;
178+
hasXs3: boolean;
154179

155180
constructor(private media: ObservableMedia) {
156181
}

src/lib/flexbox/api/class.ts

+35-16
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,18 @@ import {
99
Directive,
1010
ElementRef,
1111
Input,
12+
DoCheck,
1213
OnDestroy,
13-
OnInit,
1414
Renderer,
15-
OnChanges,
16-
SimpleChanges,
1715
IterableDiffers,
18-
KeyValueDiffers
16+
KeyValueDiffers, SimpleChanges, OnChanges
1917
} from '@angular/core';
2018
import {NgClass} from '@angular/common';
2119

2220
import {BaseFxDirectiveAdapter} from './base-adapter';
23-
import {BreakPointRegistry} from './../../media-query/breakpoints/break-point-registry';
2421
import {MediaChange} from '../../media-query/media-change';
2522
import {MediaMonitor} from '../../media-query/media-monitor';
23+
import {BreakPointRegistry} from './../../media-query/breakpoints/break-point-registry';
2624

2725
/** NgClass allowed inputs **/
2826
export type NgClassType = string | string[] | Set<string> | {[klass: string]: any};
@@ -42,7 +40,7 @@ export type NgClassType = string | string[] | Set<string> | {[klass: string]: an
4240
[ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]
4341
`
4442
})
45-
export class ClassDirective extends NgClass implements OnInit, OnChanges, OnDestroy {
43+
export class ClassDirective extends NgClass implements DoCheck, OnChanges, OnDestroy {
4644

4745
/**
4846
* Intercept ngClass assignments so we cache the default classes
@@ -98,33 +96,54 @@ export class ClassDirective extends NgClass implements OnInit, OnChanges, OnDest
9896
this._base = new BaseFxDirectiveAdapter(monitor, _ngEl, _renderer);
9997
}
10098

99+
// ******************************************************************
100+
// Lifecycle Hookks
101+
// ******************************************************************
102+
101103
/**
102-
* For @Input changes on the current mq activation property, see onMediaQueryChanges()
104+
* For @Input changes on the current mq activation property
103105
*/
104106
ngOnChanges(changes: SimpleChanges) {
105107
const changed = this._bpRegistry.items.some(it => {
106108
return (`ngClass${it.suffix}` in changes) || (`class${it.suffix}` in changes);
107109
});
108-
if (changed || this._base.mqActivation) {
109-
this._updateClass();
110+
return changed && this._updateClass();
111+
}
112+
113+
/**
114+
* For ChangeDetectionStrategy.onPush and ngOnChanges() updates
115+
*/
116+
ngDoCheck() {
117+
if (!this._base.hasMQListener) {
118+
this.configureMQListener();
110119
}
120+
super.ngDoCheck();
111121
}
112122

123+
ngOnDestroy() {
124+
this._base.ngOnDestroy();
125+
}
126+
127+
// ******************************************************************
128+
// Internal Methods
129+
// ******************************************************************
130+
113131
/**
114-
* After the initial onChanges, build an mqActivation object that bridges
132+
* Build an mqActivation object that bridges
115133
* mql change events to onMediaQueryChange handlers
116134
*/
117-
ngOnInit() {
135+
protected configureMQListener() {
118136
this._base.listenForMediaQueryChanges('class', '', (changes: MediaChange) => {
119137
this._updateClass(changes.value);
120-
});
121-
this._updateClass();
122-
}
123138

124-
ngOnDestroy() {
125-
this._base.ngOnDestroy();
139+
// trigger NgClass::_applyIterableChanges()
140+
super.ngDoCheck();
141+
});
126142
}
127143

144+
/**
145+
*
146+
*/
128147
protected _updateClass(value?: NgClassType) {
129148
let clazz = value || this._base.queryInput("class") || '';
130149
if (this._base.mqActivation) {

src/lib/flexbox/api/style.ts

+40-16
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ import {
1010
ElementRef,
1111
Input,
1212
OnDestroy,
13-
OnInit,
14-
OnChanges,
13+
DoCheck,
1514
Renderer,
1615
KeyValueDiffers,
17-
SimpleChanges, SecurityContext
16+
SimpleChanges, OnChanges,
17+
SecurityContext
1818
} from '@angular/core';
1919
import {NgStyle} from '@angular/common';
2020

@@ -47,7 +47,7 @@ import {
4747
[ngStyle.gt-xs], [ngStyle.gt-sm], [ngStyle.gt-md], [ngStyle.gt-lg]
4848
`
4949
})
50-
export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDestroy {
50+
export class StyleDirective extends NgStyle implements DoCheck, OnChanges, OnDestroy {
5151

5252
/**
5353
* Intercept ngStyle assignments so we cache the default styles
@@ -110,32 +110,51 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
110110
this._base.cacheInput('style', _ngEl.nativeElement.getAttribute("style"), true);
111111
}
112112

113+
// ******************************************************************
114+
// Lifecycle Hookks
115+
// ******************************************************************
116+
113117
/**
114-
* For @Input changes on the current mq activation property, see onMediaQueryChanges()
118+
* For @Input changes on the current mq activation property
115119
*/
116120
ngOnChanges(changes: SimpleChanges) {
117121
const changed = this._bpRegistry.items.some(it => {
118122
return (`ngStyle${it.suffix}` in changes) || (`style${it.suffix}` in changes);
119123
});
120-
if (changed || this._base.mqActivation) {
121-
this._updateStyle();
122-
}
124+
return changed && this._updateStyle();
123125
}
124126

125127
/**
126-
* After the initial onChanges, build an mqActivation object that bridges
127-
* mql change events to onMediaQueryChange handlers
128+
* For ChangeDetectionStrategy.onPush and ngOnChanges() updates
128129
*/
129-
ngOnInit() {
130-
this._base.listenForMediaQueryChanges('style', '', (changes: MediaChange) => {
131-
this._updateStyle(changes.value);
132-
});
130+
ngDoCheck() {
131+
if (!this._base.hasMQListener) {
132+
this.configureMQListener();
133+
}
134+
super.ngDoCheck();
133135
}
134136

135137
ngOnDestroy() {
136138
this._base.ngOnDestroy();
137139
}
138140

141+
// ******************************************************************
142+
// Internal Methods
143+
// ******************************************************************
144+
145+
/**
146+
* Build an mqActivation object that bridges
147+
* mql change events to onMediaQueryChange handlers
148+
*/
149+
protected configureMQListener() {
150+
this._base.listenForMediaQueryChanges('style', '', (changes: MediaChange) => {
151+
this._updateStyle(changes.value);
152+
153+
// trigger NgClass::_applyIterableChanges()
154+
super.ngDoCheck();
155+
});
156+
}
157+
139158
// ************************************************************************
140159
// Private Internal Methods
141160
// ************************************************************************
@@ -162,8 +181,14 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
162181
*/
163182
protected _buildAdapter(monitor: MediaMonitor, _ngEl: ElementRef, _renderer: Renderer) {
164183
this._base = new BaseFxDirectiveAdapter(monitor, _ngEl, _renderer);
184+
this._buildCacheInterceptor();
185+
}
186+
165187

166-
// Build intercept to convert raw strings to ngStyleMap
188+
/**
189+
* Build intercept to convert raw strings to ngStyleMap
190+
*/
191+
protected _buildCacheInterceptor() {
167192
let cacheInput = this._base.cacheInput.bind(this._base);
168193
this._base.cacheInput = (key?: string, source?: any, cacheRaw = false, merge = true) => {
169194
let styles = this._buildStyleMap(source);
@@ -173,7 +198,6 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
173198
cacheInput(key, styles, cacheRaw);
174199
};
175200
}
176-
177201
/**
178202
* Convert raw strings to ngStyleMap; which is required by ngStyle
179203
* NOTE: Raw string key-value pairs MUST be delimited by `;`

0 commit comments

Comments
 (0)