diff --git a/src/lib/flexbox/api/base-adapter.spec.ts b/src/lib/flexbox/api/base-adapter.spec.ts
index 1ed4d650a..6790472ce 100644
--- a/src/lib/flexbox/api/base-adapter.spec.ts
+++ b/src/lib/flexbox/api/base-adapter.spec.ts
@@ -20,7 +20,7 @@ export class MockElementRef extends ElementRef {
describe('BaseFxDirectiveAdapter class', () => {
let component;
beforeEach(() => {
- component = new BaseFxDirectiveAdapter(null, new MockElementRef(), null);
+ component = new BaseFxDirectiveAdapter(null, null, new MockElementRef(), null);
});
describe('cacheInput', () => {
it('should call _cacheInputArray when source is an array', () => {
diff --git a/src/lib/flexbox/api/base-adapter.ts b/src/lib/flexbox/api/base-adapter.ts
index 73c6340da..2464fc8d4 100644
--- a/src/lib/flexbox/api/base-adapter.ts
+++ b/src/lib/flexbox/api/base-adapter.ts
@@ -1,12 +1,36 @@
+/**
+ * @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, Renderer} from '@angular/core';
+
import {BaseFxDirective} from './base';
import {ResponsiveActivation} from './../responsive/responsive-activation';
import {MediaQuerySubscriber} from '../../media-query/media-change';
+import {MediaMonitor} from '../../media-query/media-monitor';
+
/**
* Adapter to the BaseFxDirective abstract class so it can be used via composition.
* @see BaseFxDirective
*/
export class BaseFxDirectiveAdapter extends BaseFxDirective {
+
+ /**
+ * Accessor to determine which @Input property is "active"
+ * e.g. which property value will be used.
+ */
+ get activeKey() {
+ let mqa = this._mqActivation;
+ let key = mqa ? mqa.activatedInputKey : this._baseKey;
+ // Note: ClassDirective::SimpleChanges uses 'klazz' instead of 'class' as a key
+ return (key === 'class') ? 'klazz' : key;
+ }
+
+ /** Hash map of all @Input keys/values defined/used */
get inputMap() {
return this._inputMap;
}
@@ -18,11 +42,22 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective {
return this._mqActivation;
}
+ /**
+ * BaseFxDirectiveAdapter constructor
+ */
+ constructor(protected _baseKey: string, // non-responsive @Input property name
+ protected _mediaMonitor: MediaMonitor,
+ protected _elementRef: ElementRef,
+ protected _renderer: Renderer ) {
+ super(_mediaMonitor, _elementRef, _renderer);
+ }
+
+
/**
* @see BaseFxDirective._queryInput
*/
queryInput(key) {
- return this._queryInput(key);
+ return key ? this._queryInput(key) : undefined;
}
/**
diff --git a/src/lib/flexbox/api/base.ts b/src/lib/flexbox/api/base.ts
index dc7a2e8bf..9f3055f9b 100644
--- a/src/lib/flexbox/api/base.ts
+++ b/src/lib/flexbox/api/base.ts
@@ -22,20 +22,10 @@ export type StyleDefinition = string|{[property: string]: string|number};
/** Abstract base class for the Layout API styling directives. */
export abstract class BaseFxDirective implements OnDestroy {
- /**
- * Original dom Elements CSS display style
- */
- protected _display;
- /**
- * MediaQuery Activation Tracker
- */
- protected _mqActivation: ResponsiveActivation;
-
- /**
- * Dictionary of input keys with associated values
- */
- protected _inputMap = {};
+ get hasMediaQueryListener() {
+ return !!this._mqActivation;
+ }
/**
*
@@ -46,6 +36,7 @@ export abstract class BaseFxDirective implements OnDestroy {
this._display = this._getDisplayStyle();
}
+
// *********************************************
// Accessor Methods
// *********************************************
@@ -172,12 +163,15 @@ export abstract class BaseFxDirective implements OnDestroy {
protected _listenForMediaQueryChanges(key: string,
defaultValue: any,
onMediaQueryChange: MediaQuerySubscriber): ResponsiveActivation { // tslint:disable-line:max-line-length
- let keyOptions = new KeyOptions(key, defaultValue, this._inputMap);
- return this._mqActivation = new ResponsiveActivation(
- keyOptions,
- this._mediaMonitor,
- (change) => onMediaQueryChange.call(this, change)
- );
+ if ( !this._mqActivation ) {
+ let keyOptions = new KeyOptions(key, defaultValue, this._inputMap);
+ this._mqActivation = new ResponsiveActivation(
+ keyOptions,
+ this._mediaMonitor,
+ (change) => onMediaQueryChange(change)
+ );
+ }
+ return this._mqActivation;
}
/**
@@ -201,4 +195,16 @@ export abstract class BaseFxDirective implements OnDestroy {
return this._mqActivation.hasKeyValue(key);
}
+ /** Original dom Elements CSS display style */
+ protected _display;
+
+ /**
+ * MediaQuery Activation Tracker
+ */
+ protected _mqActivation: ResponsiveActivation;
+
+ /**
+ * Dictionary of input keys with associated values
+ */
+ protected _inputMap = {};
}
diff --git a/src/lib/flexbox/api/class.spec.ts b/src/lib/flexbox/api/class.spec.ts
index 68cb2ac47..327749f1f 100644
--- a/src/lib/flexbox/api/class.spec.ts
+++ b/src/lib/flexbox/api/class.spec.ts
@@ -5,11 +5,12 @@
* 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 {
- Component, OnInit
-} from '@angular/core';
+import {Component, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common';
-import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {ComponentFixture, TestBed, async} from '@angular/core/testing';
+
+import {customMatchers} from '../../utils/testing/custom-matchers';
+import {makeCreateTestComponent, expectNativeEl} from '../../utils/testing/helpers';
import {MockMatchMedia} from '../../media-query/mock/mock-match-media';
import {MatchMedia} from '../../media-query/match-media';
@@ -17,10 +18,6 @@ import {ObservableMedia} from '../../media-query/observable-media';
import {DEFAULT_BREAKPOINTS_PROVIDER} from '../../media-query/breakpoints/break-points-provider';
import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry';
-import {customMatchers} from '../../utils/testing/custom-matchers';
-import {
- makeCreateTestComponent, expectNativeEl
-} from '../../utils/testing/helpers';
import {ClassDirective} from './class';
import {MediaQueriesModule} from '../../media-query/_module';
@@ -65,15 +62,73 @@ describe('class directive', () => {
});
});
- it('should keep existing class selector', () => {
+ it('should merge `ngClass` values with any `class` values', () => {
fixture = createTestComponent(`
-
+
+
+ `);
+
+ expectNativeEl(fixture).toHaveCssClass('class0');
+ expectNativeEl(fixture).toHaveCssClass('class1');
+ expectNativeEl(fixture).toHaveCssClass('class2');
+ });
+
+ it('should override base `class` values with responsive values', () => {
+ fixture = createTestComponent(`
+
+
+ `);
+
+ expectNativeEl(fixture).toHaveCssClass('class0');
+ expectNativeEl(fixture).not.toHaveCssClass('class1');
+ expectNativeEl(fixture).not.toHaveCssClass('class2');
+
+ activateMediaQuery('xs');
+ expectNativeEl(fixture).not.toHaveCssClass('class0');
+ expectNativeEl(fixture).toHaveCssClass('class1');
+ expectNativeEl(fixture).toHaveCssClass('class2');
+
+ // activateMediaQuery('lg');
+ // expectNativeEl(fixture).toHaveCssClass('class0');
+ // expectNativeEl(fixture).not.toHaveCssClass('class1');
+ // expectNativeEl(fixture).not.toHaveCssClass('class2');
+ });
+
+ it('should keep the raw existing `class` with responsive updates', () => {
+ fixture = createTestComponent(`
+
+
+ `);
+
+ expectNativeEl(fixture).toHaveCssClass('existing-class');
+ expectNativeEl(fixture).toHaveCssClass('class1');
+
+ activateMediaQuery('xs');
+ expectNativeEl(fixture).toHaveCssClass('xs-class');
+ expectNativeEl(fixture).toHaveCssClass('existing-class');
+ expectNativeEl(fixture).not.toHaveCssClass('class1');
+
+ activateMediaQuery('lg');
+ expectNativeEl(fixture).not.toHaveCssClass('xs-class');
+ expectNativeEl(fixture).toHaveCssClass('existing-class');
+ expectNativeEl(fixture).toHaveCssClass('class1');
+ });
+
+
+ it('should keep allow removal of class selector', () => {
+ fixture = createTestComponent(`
+
`);
expectNativeEl(fixture).toHaveCssClass('existing-class');
activateMediaQuery('xs');
expectNativeEl(fixture).not.toHaveCssClass('existing-class');
+ expectNativeEl(fixture).toHaveCssClass('xs-class');
activateMediaQuery('lg');
expectNativeEl(fixture).toHaveCssClass('existing-class');
@@ -83,15 +138,22 @@ describe('class directive', () => {
it('should keep existing ngClass selector', () => {
// @see documentation for @angular/core ngClass =http://bit.ly/2mz0LAa
fixture = createTestComponent(`
-
+
`);
+ expectNativeEl(fixture).toHaveCssClass('always');
expectNativeEl(fixture).toHaveCssClass('existing-class');
+
activateMediaQuery('xs');
+ expectNativeEl(fixture).toHaveCssClass('always');
expectNativeEl(fixture).toHaveCssClass('existing-class');
+ expectNativeEl(fixture).toHaveCssClass('xs-class');
activateMediaQuery('lg');
+ expectNativeEl(fixture).toHaveCssClass('always');
expectNativeEl(fixture).toHaveCssClass('existing-class');
expectNativeEl(fixture).not.toHaveCssClass('xs-class');
});
@@ -112,21 +174,23 @@ describe('class directive', () => {
it('should work with ngClass object notation', () => {
fixture = createTestComponent(`
-
+
`);
- activateMediaQuery('xs');
- expectNativeEl(fixture, {hasXs1: true, hasXs2: false}).toHaveCssClass('xs-1');
- expectNativeEl(fixture, {hasXs1: true, hasXs2: false}).not.toHaveCssClass('xs-2');
+ expectNativeEl(fixture, {hasX1: true, hasX2: true, hasX3: true}).toHaveCssClass('x1');
+ expectNativeEl(fixture, {hasX1: true, hasX2: true, hasX3: true}).not.toHaveCssClass('x2');
+ expectNativeEl(fixture, {hasX1: true, hasX2: true, hasX3: true}).toHaveCssClass('x3');
- expectNativeEl(fixture, {hasXs1: false, hasXs2: true}).toHaveCssClass('xs-2');
- expectNativeEl(fixture, {hasXs1: false, hasXs2: true}).not.toHaveCssClass('xs-1');
+ activateMediaQuery('X');
+ expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: false}).toHaveCssClass('x1');
+ expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: false}).not.toHaveCssClass('x2');
+ expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: false}).not.toHaveCssClass('x3');
activateMediaQuery('md');
- expectNativeEl(fixture, {hasXs1: true, hasX2: false, hasXs3: true}).toHaveCssClass('xs-3');
- expectNativeEl(fixture, {hasXs1: true, hasX2: false, hasXs3: true}).not.toHaveCssClass('xs-2');
- expectNativeEl(fixture, {hasXs1: true, hasX2: false, hasXs3: true}).toHaveCssClass('xs-1');
+ expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: true}).toHaveCssClass('x1');
+ expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: true}).not.toHaveCssClass('x2');
+ expectNativeEl(fixture, {hasX1: true, hasX2: false, hasX3: true}).toHaveCssClass('x3');
});
it('should work with ngClass array notation', () => {
@@ -141,7 +205,7 @@ describe('class directive', () => {
});
// *****************************************************************
-// Template Component
+// Template Components
// *****************************************************************
@Component({
@@ -151,6 +215,7 @@ describe('class directive', () => {
export class TestClassComponent implements OnInit {
hasXs1: boolean;
hasXs2: boolean;
+ hasXs3: boolean;
constructor(private media: ObservableMedia) {
}
@@ -160,4 +225,354 @@ export class TestClassComponent implements OnInit {
}
+// *******************************************************************************
+// Standard tests from `angular/packages/common/test/directives/ng_class_spec.ts`
+// *******************************************************************************
+
+describe('binding to CSS class list', () => {
+ let createTestComponent = makeCreateTestComponent(() => TestComponent);
+ let fixture: ComponentFixture
;
+
+ function normalizeClassNames(classes: string) {
+ return classes.trim().split(' ').sort().join(' ');
+ }
+
+ function detectChangesAndExpectClassName(classes: string): void {
+ fixture.detectChanges();
+ let nonNormalizedClassName = fixture.debugElement.children[0].nativeElement.className;
+ expect(normalizeClassNames(nonNormalizedClassName)).toEqual(normalizeClassNames(classes));
+ }
+
+ function getComponent(): TestComponent { return fixture.debugElement.componentInstance; }
+
+ afterEach(() => { fixture = null; });
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestComponent],
+ });
+ });
+
+ it('should clean up when the directive is destroyed', async(() => {
+ fixture = createTestComponent('');
+
+ getComponent().items = [['0']];
+ fixture.detectChanges();
+ getComponent().items = [['1']];
+ detectChangesAndExpectClassName('1');
+ }));
+
+ describe('expressions evaluating to objects', () => {
+
+ it('should add classes specified in an object literal', async(() => {
+ fixture = createTestComponent('');
+
+ detectChangesAndExpectClassName('foo');
+ }));
+
+ it('should add classes specified in an object literal without change in class names',
+ async(() => {
+ fixture =
+ createTestComponent(``);
+
+ detectChangesAndExpectClassName('foo-bar fooBar');
+ }));
+
+ it('should add and remove classes based on changes in object literal values', async(() => {
+ fixture =
+ createTestComponent('');
+
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().condition = false;
+ detectChangesAndExpectClassName('bar');
+ }));
+
+ it('should add and remove classes based on changes to the expression object', async(() => {
+ fixture = createTestComponent('');
+ const objExpr = getComponent().objExpr;
+
+ detectChangesAndExpectClassName('foo');
+
+ objExpr['bar'] = true;
+ detectChangesAndExpectClassName('foo bar');
+
+ objExpr['baz'] = true;
+ detectChangesAndExpectClassName('foo bar baz');
+
+ delete (objExpr['bar']);
+ detectChangesAndExpectClassName('foo baz');
+ }));
+
+ it('should add and remove classes based on reference changes to the expression object',
+ async(() => {
+ fixture = createTestComponent('');
+
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().objExpr = {foo: true, bar: true};
+ detectChangesAndExpectClassName('foo bar');
+
+ getComponent().objExpr = {baz: true};
+ detectChangesAndExpectClassName('baz');
+ }));
+
+ it('should remove active classes when expression evaluates to null', async(() => {
+ fixture = createTestComponent('');
+
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().objExpr = null;
+ detectChangesAndExpectClassName('');
+
+ getComponent().objExpr = {'foo': false, 'bar': true};
+ detectChangesAndExpectClassName('bar');
+ }));
+
+
+ it('should allow multiple classes per expression', async(() => {
+ fixture = createTestComponent('');
+
+ getComponent().objExpr = {'bar baz': true, 'bar1 baz1': true};
+ detectChangesAndExpectClassName('bar baz bar1 baz1');
+
+ getComponent().objExpr = {'bar baz': false, 'bar1 baz1': true};
+ detectChangesAndExpectClassName('bar1 baz1');
+ }));
+
+ it('should split by one or more spaces between classes', async(() => {
+ fixture = createTestComponent('');
+
+ getComponent().objExpr = {'foo bar baz': true};
+ detectChangesAndExpectClassName('foo bar baz');
+ }));
+ });
+
+ describe('expressions evaluating to lists', () => {
+
+ it('should add classes specified in a list literal', async(() => {
+ fixture =
+ createTestComponent(``);
+
+ detectChangesAndExpectClassName('foo bar foo-bar fooBar');
+ }));
+
+ it('should add and remove classes based on changes to the expression', async(() => {
+ fixture = createTestComponent('');
+ const arrExpr = getComponent().arrExpr;
+ detectChangesAndExpectClassName('foo');
+
+ arrExpr.push('bar');
+ detectChangesAndExpectClassName('foo bar');
+
+ arrExpr[1] = 'baz';
+ detectChangesAndExpectClassName('foo baz');
+
+ getComponent().arrExpr = arrExpr.filter((v: string) => v !== 'baz');
+ detectChangesAndExpectClassName('foo');
+ }));
+
+ it('should add and remove classes when a reference changes', async(() => {
+ fixture = createTestComponent('');
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().arrExpr = ['bar'];
+ detectChangesAndExpectClassName('bar');
+ }));
+
+ it('should take initial classes into account when a reference changes', async(() => {
+ fixture = createTestComponent('');
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().arrExpr = ['bar'];
+ detectChangesAndExpectClassName('foo bar');
+ }));
+
+ it('should ignore empty or blank class names', async(() => {
+ fixture = createTestComponent('');
+ getComponent().arrExpr = ['', ' '];
+ detectChangesAndExpectClassName('foo');
+ }));
+
+ it('should trim blanks from class names', async(() => {
+ fixture = createTestComponent('');
+
+ getComponent().arrExpr = [' bar '];
+ detectChangesAndExpectClassName('foo bar');
+ }));
+
+
+ it('should allow multiple classes per item in arrays', async(() => {
+ fixture = createTestComponent('');
+
+ getComponent().arrExpr = ['foo bar baz', 'foo1 bar1 baz1'];
+ detectChangesAndExpectClassName('foo bar baz foo1 bar1 baz1');
+
+ getComponent().arrExpr = ['foo bar baz foobar'];
+ detectChangesAndExpectClassName('foo bar baz foobar');
+ }));
+
+ it('should throw with descriptive error message when CSS class is not a string', () => {
+ fixture = createTestComponent(``);
+ expect(() => fixture.detectChanges())
+ .toThrowError(
+ /NgClass can only toggle CSS classes expressed as strings, got \[object Object\]/);
+ });
+ });
+
+ describe('expressions evaluating to sets', () => {
+
+ it('should add and remove classes if the set instance changed', async(() => {
+ fixture = createTestComponent('');
+ let setExpr = new Set();
+ setExpr.add('bar');
+ getComponent().setExpr = setExpr;
+ detectChangesAndExpectClassName('bar');
+
+ setExpr = new Set();
+ setExpr.add('baz');
+ getComponent().setExpr = setExpr;
+ detectChangesAndExpectClassName('baz');
+ }));
+ });
+
+ describe('expressions evaluating to string', () => {
+
+ it('should add classes specified in a string literal', async(() => {
+ fixture = createTestComponent(``);
+ detectChangesAndExpectClassName('foo bar foo-bar fooBar');
+ }));
+
+ it('should add and remove classes based on changes to the expression', async(() => {
+ fixture = createTestComponent('');
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().strExpr = 'foo bar';
+ detectChangesAndExpectClassName('foo bar');
+
+
+ getComponent().strExpr = 'baz';
+ detectChangesAndExpectClassName('baz');
+ }));
+
+ it('should remove active classes when switching from string to null', async(() => {
+ fixture = createTestComponent(``);
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().strExpr = null;
+ detectChangesAndExpectClassName('');
+ }));
+
+ it('should take initial classes into account when switching from string to null',
+ async(() => {
+ fixture = createTestComponent(``);
+ detectChangesAndExpectClassName('foo');
+
+ getComponent().strExpr = null;
+ detectChangesAndExpectClassName('foo');
+ }));
+
+ it('should ignore empty and blank strings', async(() => {
+ fixture = createTestComponent(``);
+ getComponent().strExpr = '';
+ detectChangesAndExpectClassName('foo');
+ }));
+
+ });
+
+ describe('cooperation with other class-changing constructs', () => {
+
+ it('should co-operate with the class attribute', async(() => {
+ fixture = createTestComponent('');
+ const objExpr = getComponent().objExpr;
+
+ objExpr['bar'] = true;
+ detectChangesAndExpectClassName('init foo bar');
+
+ objExpr['foo'] = false;
+ detectChangesAndExpectClassName('init bar');
+
+ getComponent().objExpr = null;
+ detectChangesAndExpectClassName('init foo');
+ }));
+
+ it('should co-operate with the interpolated class attribute', async(() => {
+ fixture = createTestComponent(``);
+ const objExpr = getComponent().objExpr;
+
+ objExpr['bar'] = true;
+ detectChangesAndExpectClassName(`init foo bar`);
+
+ objExpr['foo'] = false;
+ detectChangesAndExpectClassName(`init bar`);
+
+ getComponent().objExpr = null;
+ detectChangesAndExpectClassName(`init foo`);
+ }));
+
+ it('should co-operate with the class attribute and binding to it', async(() => {
+ fixture =
+ createTestComponent(``);
+ const objExpr = getComponent().objExpr;
+
+ objExpr['bar'] = true;
+ detectChangesAndExpectClassName(`init foo bar`);
+
+ objExpr['foo'] = false;
+ detectChangesAndExpectClassName(`init bar`);
+
+ getComponent().objExpr = null;
+ detectChangesAndExpectClassName(`init foo`);
+ }));
+
+ it('should co-operate with the class attribute and class.name binding', async(() => {
+ const template =
+ '';
+ fixture = createTestComponent(template);
+ const objExpr = getComponent().objExpr;
+
+ detectChangesAndExpectClassName('init foo baz');
+
+ objExpr['bar'] = true;
+ detectChangesAndExpectClassName('init foo baz bar');
+
+ objExpr['foo'] = false;
+ detectChangesAndExpectClassName('init baz bar');
+
+ getComponent().condition = false;
+ detectChangesAndExpectClassName('init bar');
+ }));
+
+ it('should co-operate with initial class and class attribute binding when binding changes',
+ async(() => {
+ const template = '';
+ fixture = createTestComponent(template);
+ const cmp = getComponent();
+
+ detectChangesAndExpectClassName('init foo');
+
+ cmp.objExpr['bar'] = true;
+ detectChangesAndExpectClassName('init foo bar');
+
+ cmp.strExpr = 'baz';
+ detectChangesAndExpectClassName('init bar baz foo');
+
+ cmp.objExpr = null;
+ detectChangesAndExpectClassName('init baz');
+ }));
+ });
+ });
+
+@Component({selector: 'test-cmp', template: ''})
+class TestComponent {
+ condition: boolean = true;
+ items: any[];
+ arrExpr: string[] = ['foo'];
+ setExpr: Set = new Set();
+ objExpr: {[klass: string]: any} = {'foo': true, 'bar': false};
+ strExpr = 'foo';
+
+ constructor() { this.setExpr.add('foo'); }
+}
+
diff --git a/src/lib/flexbox/api/class.ts b/src/lib/flexbox/api/class.ts
index 5daee8752..74635e2a7 100644
--- a/src/lib/flexbox/api/class.ts
+++ b/src/lib/flexbox/api/class.ts
@@ -9,18 +9,15 @@ import {
Directive,
ElementRef,
Input,
+ DoCheck,
OnDestroy,
- OnInit,
Renderer,
- OnChanges,
- SimpleChanges,
IterableDiffers,
- KeyValueDiffers
+ KeyValueDiffers, SimpleChanges, OnChanges
} from '@angular/core';
import {NgClass} from '@angular/common';
import {BaseFxDirectiveAdapter} from './base-adapter';
-import {BreakPointRegistry} from './../../media-query/breakpoints/break-point-registry';
import {MediaChange} from '../../media-query/media-change';
import {MediaMonitor} from '../../media-query/media-monitor';
@@ -32,112 +29,171 @@ export type NgClassType = string | string[] | Set | {[klass: string]: an
*/
@Directive({
selector: `
- [class],
- [class.xs], [class.sm], [class.md], [class.lg], [class.xl],
+ [class], [class.xs], [class.sm], [class.md], [class.lg], [class.xl],
[class.lt-sm], [class.lt-md], [class.lt-lg], [class.lt-xl],
- [class.gt-xs], [class.gt-sm], [class.gt-md], [class.gt-lg],
- [ngClass],
- [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl],
+ [class.gt-xs], [class.gt-sm], [class.gt-md], [class.gt-lg],
+
+ [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl],
[ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl],
[ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg]
`
})
-export class ClassDirective extends NgClass implements OnInit, OnChanges, OnDestroy {
+export class ClassDirective extends NgClass implements DoCheck, OnChanges, OnDestroy {
/**
* Intercept ngClass assignments so we cache the default classes
* which are merged with activated styles or used as fallbacks.
+ * Note: Base ngClass values are applied during ngDoCheck()
*/
@Input('ngClass')
set ngClassBase(val: NgClassType) {
- this._base.cacheInput('class', val, true);
- this.ngClass = this._base.inputMap['class'];
+ this._ngClassAdapter.cacheInput('ngClass', val, true);
+ this.ngClass = val;
}
/* tslint:disable */
- @Input('ngClass.xs') set ngClassXs(val: NgClassType) { this._base.cacheInput('classXs', val, true); }
- @Input('ngClass.sm') set ngClassSm(val: NgClassType) { this._base.cacheInput('classSm', val, true); };
- @Input('ngClass.md') set ngClassMd(val: NgClassType) { this._base.cacheInput('classMd', val, true); };
- @Input('ngClass.lg') set ngClassLg(val: NgClassType) { this._base.cacheInput('classLg', val, true);};
- @Input('ngClass.xl') set ngClassXl(val: NgClassType) { this._base.cacheInput('classXl', val, true); };
-
- @Input('ngClass.lt-xs') set ngClassLtXs(val: NgClassType) { this._base.cacheInput('classLtXs', val, true); };
- @Input('ngClass.lt-sm') set ngClassLtSm(val: NgClassType) { this._base.cacheInput('classLtSm', val, true);} ;
- @Input('ngClass.lt-md') set ngClassLtMd(val: NgClassType) { this._base.cacheInput('classLtMd', val, true);};
- @Input('ngClass.lt-lg') set ngClassLtLg(val: NgClassType) { this._base.cacheInput('classLtLg', val, true); };
-
- @Input('ngClass.gt-xs') set ngClassGtXs(val: NgClassType) { this._base.cacheInput('classGtXs', val, true); };
- @Input('ngClass.gt-sm') set ngClassGtSm(val: NgClassType) { this._base.cacheInput('classGtSm', val, true);} ;
- @Input('ngClass.gt-md') set ngClassGtMd(val: NgClassType) { this._base.cacheInput('classGtMd', val, true);};
- @Input('ngClass.gt-lg') set ngClassGtLg(val: NgClassType) { this._base.cacheInput('classGtLg', val, true); };
+ @Input('ngClass.xs') set ngClassXs(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassXs', val, true); }
+ @Input('ngClass.sm') set ngClassSm(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassSm', val, true); }
+ @Input('ngClass.md') set ngClassMd(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassMd', val, true); }
+ @Input('ngClass.lg') set ngClassLg(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassLg', val, true); }
+ @Input('ngClass.xl') set ngClassXl(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassXl', val, true); }
+
+ @Input('ngClass.lt-sm') set ngClassLtSm(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassLtSm', val, true); }
+ @Input('ngClass.lt-md') set ngClassLtMd(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassLtMd', val, true); }
+ @Input('ngClass.lt-lg') set ngClassLtLg(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassLtLg', val, true); }
+ @Input('ngClass.lt-xl') set ngClassLtXl(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassLtXl', val, true); }
+
+ @Input('ngClass.gt-xs') set ngClassGtXs(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassGtXs', val, true); }
+ @Input('ngClass.gt-sm') set ngClassGtSm(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassGtSm', val, true); }
+ @Input('ngClass.gt-md') set ngClassGtMd(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassGtMd', val, true); }
+ @Input('ngClass.gt-lg') set ngClassGtLg(val: NgClassType) { this._ngClassAdapter.cacheInput('ngClassGtLg', val, true); }
/** Deprecated selectors */
- @Input('class') set classBase(val: NgClassType) { this._base.cacheInput('class', val, true); }
- @Input('class.xs') set classXs(val: NgClassType) { this._base.cacheInput('classXs', val, true); }
- @Input('class.sm') set classSm(val: NgClassType) { this._base.cacheInput('classSm', val, true); };
- @Input('class.md') set classMd(val: NgClassType) { this._base.cacheInput('classMd', val, true);};
- @Input('class.lg') set classLg(val: NgClassType) { this._base.cacheInput('classLg', val, true); };
- @Input('class.xl') set classXl(val: NgClassType) { this._base.cacheInput('classXl', val, true); };
-
- @Input('class.lt-xs') set classLtXs(val: NgClassType) { this._base.cacheInput('classLtXs', val, true); };
- @Input('class.lt-sm') set classLtSm(val: NgClassType) { this._base.cacheInput('classLtSm', val, true); };
- @Input('class.lt-md') set classLtMd(val: NgClassType) { this._base.cacheInput('classLtMd', val, true);};
- @Input('class.lt-lg') set classLtLg(val: NgClassType) { this._base.cacheInput('classLtLg', val, true); };
-
- @Input('class.gt-xs') set classGtXs(val: NgClassType) { this._base.cacheInput('classGtXs', val, true); };
- @Input('class.gt-sm') set classGtSm(val: NgClassType) { this._base.cacheInput('classGtSm', val, true); };
- @Input('class.gt-md') set classGtMd(val: NgClassType) { this._base.cacheInput('classGtMd', val, true);};
- @Input('class.gt-lg') set classGtLg(val: NgClassType) { this._base.cacheInput('classGtLg', val, true); };
+
+ /**
+ * Base class selector values get applied immediately and are considered destructive overwrites to
+ * all previous class assignments
+ *
+ * Delegate to NgClass:klass setter and cache value for base fallback from responsive APIs.
+ */
+ @Input('class')
+ set classBase(val: string) {
+ this._classAdapter.cacheInput('_rawClass', val, true);
+ this.klass = val;
+ }
+
+ @Input('class.xs') set classXs(val: NgClassType) { this._classAdapter.cacheInput('classXs', val, true); }
+ @Input('class.sm') set classSm(val: NgClassType) { this._classAdapter.cacheInput('classSm', val, true); }
+ @Input('class.md') set classMd(val: NgClassType) { this._classAdapter.cacheInput('classMd', val, true); }
+ @Input('class.lg') set classLg(val: NgClassType) { this._classAdapter.cacheInput('classLg', val, true); }
+ @Input('class.xl') set classXl(val: NgClassType) { this._classAdapter.cacheInput('classXl', val, true); }
+
+ @Input('class.lt-sm') set classLtSm(val: NgClassType) { this._classAdapter.cacheInput('classLtSm', val, true); }
+ @Input('class.lt-md') set classLtMd(val: NgClassType) { this._classAdapter.cacheInput('classLtMd', val, true); }
+ @Input('class.lt-lg') set classLtLg(val: NgClassType) { this._classAdapter.cacheInput('classLtLg', val, true); }
+ @Input('class.lt-xl') set classLtXl(val: NgClassType) { this._classAdapter.cacheInput('classLtXl', val, true); }
+
+ @Input('class.gt-xs') set classGtXs(val: NgClassType) { this._classAdapter.cacheInput('classGtXs', val, true); }
+ @Input('class.gt-sm') set classGtSm(val: NgClassType) { this._classAdapter.cacheInput('classGtSm', val, true); }
+ @Input('class.gt-md') set classGtMd(val: NgClassType) { this._classAdapter.cacheInput('classGtMd', val, true); }
+ @Input('class.gt-lg') set classGtLg(val: NgClassType) { this._classAdapter.cacheInput('classGtLg', val, true); }
+
+ /**
+ * Initial value of the `class` attribute; used as
+ * fallback and will be merged with nay `ngClass` values
+ */
+ get initialClasses() : string {
+ return this._classAdapter.queryInput('_rawClass') || "";
+ }
/* tslint:enable */
constructor(protected monitor: MediaMonitor,
- protected _bpRegistry: BreakPointRegistry,
_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers,
_ngEl: ElementRef, _renderer: Renderer) {
super(_iterableDiffers, _keyValueDiffers, _ngEl, _renderer);
- this._base = new BaseFxDirectiveAdapter(monitor, _ngEl, _renderer);
+
+ this._classAdapter = new BaseFxDirectiveAdapter('class', monitor, _ngEl, _renderer);
+ this._ngClassAdapter = new BaseFxDirectiveAdapter('ngClass', monitor, _ngEl, _renderer);
}
+ // ******************************************************************
+ // Lifecycle Hookks
+ // ******************************************************************
+
/**
- * For @Input changes on the current mq activation property, see onMediaQueryChanges()
+ * For @Input changes on the current mq activation property
*/
ngOnChanges(changes: SimpleChanges) {
- const changed = this._bpRegistry.items.some(it => {
- return (`ngClass${it.suffix}` in changes) || (`class${it.suffix}` in changes);
- });
- if (changed || this._base.mqActivation) {
- this._updateClass();
+ if (this._classAdapter.activeKey in changes) {
+ this._updateKlass();
+ }
+ if (this._ngClassAdapter.activeKey in changes) {
+ this._updateNgClass();
+ }
+ }
+
+ /**
+ * For ChangeDetectionStrategy.onPush and ngOnChanges() updates
+ */
+ ngDoCheck() {
+ if (!this._classAdapter.hasMediaQueryListener) {
+ this._configureMQListener();
}
+ super.ngDoCheck();
}
+ ngOnDestroy() {
+ this._classAdapter.ngOnDestroy();
+ this._ngClassAdapter.ngOnDestroy();
+ }
+
+ // ******************************************************************
+ // Internal Methods
+ // ******************************************************************
+
/**
- * After the initial onChanges, build an mqActivation object that bridges
+ * Build an mqActivation object that bridges
* mql change events to onMediaQueryChange handlers
*/
- ngOnInit() {
- this._base.listenForMediaQueryChanges('class', '', (changes: MediaChange) => {
- this._updateClass(changes.value);
+ protected _configureMQListener() {
+ this._classAdapter.listenForMediaQueryChanges('class', '', (changes: MediaChange) => {
+ this._updateKlass(changes.value);
+ });
+
+ this._ngClassAdapter.listenForMediaQueryChanges('ngClass', '', (changes: MediaChange) => {
+ this._updateNgClass(changes.value);
+ super.ngDoCheck(); // trigger NgClass::_applyIterableChanges()
});
- this._updateClass();
}
- ngOnDestroy() {
- this._base.ngOnDestroy();
+ /**
+ * Apply updates directly to the NgClass:klass property
+ * ::ngDoCheck() is not needed
+ */
+ protected _updateKlass(value?: NgClassType) {
+ let klass = value || this._classAdapter.queryInput('class') || '';
+ if (this._classAdapter.mqActivation) {
+ klass = this._classAdapter.mqActivation.activatedInput;
+ }
+ this.klass = klass || this.initialClasses;
}
- protected _updateClass(value?: NgClassType) {
- let clazz = value || this._base.queryInput("class") || '';
- if (this._base.mqActivation) {
- clazz = this._base.mqActivation.activatedInput;
+ /**
+ * Identify the activated input value and update the ngClass iterables...
+ * needs ngDoCheck() to actually apply the values to the element
+ */
+ protected _updateNgClass(value?: NgClassType) {
+ if (this._ngClassAdapter.mqActivation) {
+ value = this._ngClassAdapter.mqActivation.activatedInput;
}
- // Delegate subsequent activity to the NgClass logic
- this.ngClass = clazz;
+ this.ngClass = value || ''; // Delegate subsequent activity to the NgClass logic
}
/**
* Special adapter to cross-cut responsive behaviors
* into the ClassDirective
*/
- protected _base: BaseFxDirectiveAdapter;
+ protected _classAdapter: BaseFxDirectiveAdapter; // used for `class.xxx` selectores
+ protected _ngClassAdapter: BaseFxDirectiveAdapter; // used for `ngClass.xxx` selectors
}
diff --git a/src/lib/flexbox/api/style.ts b/src/lib/flexbox/api/style.ts
index 13aab3a1e..cdf1e2c97 100644
--- a/src/lib/flexbox/api/style.ts
+++ b/src/lib/flexbox/api/style.ts
@@ -10,11 +10,11 @@ import {
ElementRef,
Input,
OnDestroy,
- OnInit,
- OnChanges,
+ DoCheck,
Renderer,
KeyValueDiffers,
- SimpleChanges, SecurityContext
+ SimpleChanges, OnChanges,
+ SecurityContext
} from '@angular/core';
import {NgStyle} from '@angular/common';
@@ -47,7 +47,7 @@ import {
[ngStyle.gt-xs], [ngStyle.gt-sm], [ngStyle.gt-md], [ngStyle.gt-lg]
`
})
-export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDestroy {
+export class StyleDirective extends NgStyle implements DoCheck, OnChanges, OnDestroy {
/**
* Intercept ngStyle assignments so we cache the default styles
@@ -78,15 +78,15 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
/** Deprecated selectors */
@Input('style.xs') set styleXs(val: NgStyleType) { this._base.cacheInput('styleXs', val, true); }
- @Input('style.sm') set styleSm(val: NgStyleType) { this._base.cacheInput('styleSm', val, true); };
+ @Input('style.sm') set styleSm(val: NgStyleType) { this._base.cacheInput('styleSm', val, true); };
@Input('style.md') set styleMd(val: NgStyleType) { this._base.cacheInput('styleMd', val, true);};
@Input('style.lg') set styleLg(val: NgStyleType) { this._base.cacheInput('styleLg', val, true); };
@Input('style.xl') set styleXl(val: NgStyleType) { this._base.cacheInput('styleXl', val, true); };
- @Input('style.lt-xs') set styleLtXs(val: NgStyleType) { this._base.cacheInput('styleLtXs', val, true); };
@Input('style.lt-sm') set styleLtSm(val: NgStyleType) { this._base.cacheInput('styleLtSm', val, true); };
- @Input('style.lt-md') set styleLtMd(val: NgStyleType) { this._base.cacheInput('styleLtMd', val, true);};
- @Input('style.lt-lg') set styleLtLg(val: NgStyleType) { this._base.cacheInput('styleLtLg', val, true); };
+ @Input('style.lt-md') set styleLtMd(val: NgStyleType) { this._base.cacheInput('styleLtMd', val, true); };
+ @Input('style.lt-lg') set styleLtLg(val: NgStyleType) { this._base.cacheInput('styleLtLg', val, true);};
+ @Input('style.lt-xl') set styleLtXl(val: NgStyleType) { this._base.cacheInput('styleLtXl', val, true); };
@Input('style.gt-xs') set styleGtXs(val: NgStyleType) { this._base.cacheInput('styleGtXs', val, true); };
@Input('style.gt-sm') set styleGtSm(val: NgStyleType) { this._base.cacheInput('styleGtSm', val, true); };
@@ -110,32 +110,50 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
this._base.cacheInput('style', _ngEl.nativeElement.getAttribute("style"), true);
}
+ // ******************************************************************
+ // Lifecycle Hookks
+ // ******************************************************************
+
/**
- * For @Input changes on the current mq activation property, see onMediaQueryChanges()
+ * For @Input changes on the current mq activation property
*/
ngOnChanges(changes: SimpleChanges) {
- const changed = this._bpRegistry.items.some(it => {
- return (`ngStyle${it.suffix}` in changes) || (`style${it.suffix}` in changes);
- });
- if (changed || this._base.mqActivation) {
+ if (this._base.activeKey in changes) {
this._updateStyle();
}
}
/**
- * After the initial onChanges, build an mqActivation object that bridges
- * mql change events to onMediaQueryChange handlers
+ * For ChangeDetectionStrategy.onPush and ngOnChanges() updates
*/
- ngOnInit() {
- this._base.listenForMediaQueryChanges('style', '', (changes: MediaChange) => {
- this._updateStyle(changes.value);
- });
+ ngDoCheck() {
+ if (!this._base.hasMediaQueryListener) {
+ this._configureMQListener();
+ }
+ super.ngDoCheck();
}
ngOnDestroy() {
this._base.ngOnDestroy();
}
+ // ******************************************************************
+ // Internal Methods
+ // ******************************************************************
+
+ /**
+ * Build an mqActivation object that bridges
+ * mql change events to onMediaQueryChange handlers
+ */
+ protected _configureMQListener() {
+ this._base.listenForMediaQueryChanges('style', '', (changes: MediaChange) => {
+ this._updateStyle(changes.value);
+
+ // trigger NgClass::_applyIterableChanges()
+ super.ngDoCheck();
+ });
+ }
+
// ************************************************************************
// Private Internal Methods
// ************************************************************************
@@ -161,9 +179,15 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
* which property value should be used for the style update
*/
protected _buildAdapter(monitor: MediaMonitor, _ngEl: ElementRef, _renderer: Renderer) {
- this._base = new BaseFxDirectiveAdapter(monitor, _ngEl, _renderer);
+ this._base = new BaseFxDirectiveAdapter('style', monitor, _ngEl, _renderer);
+ this._buildCacheInterceptor();
+ }
+
- // Build intercept to convert raw strings to ngStyleMap
+ /**
+ * Build intercept to convert raw strings to ngStyleMap
+ */
+ protected _buildCacheInterceptor() {
let cacheInput = this._base.cacheInput.bind(this._base);
this._base.cacheInput = (key?: string, source?: any, cacheRaw = false, merge = true) => {
let styles = this._buildStyleMap(source);
@@ -173,7 +197,6 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
cacheInput(key, styles, cacheRaw);
};
}
-
/**
* Convert raw strings to ngStyleMap; which is required by ngStyle
* NOTE: Raw string key-value pairs MUST be delimited by `;`