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 `;`