diff --git a/package.json b/package.json
index 76e57dc71..da0c7099a 100644
--- a/package.json
+++ b/package.json
@@ -131,7 +131,7 @@
"ts-node": "^0.7.3",
"tslint": "^4.2.0",
"tslint-loader": "^3.3.0",
- "typescript": "2.0.10",
+ "typescript": "^2.0.10",
"url-loader": "^0.5.7",
"webpack": "2.2.0-rc.3",
"webpack-bundle-analyzer": "^2.2.0",
diff --git a/src/demo-app/app/github-issues/DemosGithubIssues.ts b/src/demo-app/app/github-issues/DemosGithubIssues.ts
index 5db11f220..1f1d7363b 100644
--- a/src/demo-app/app/github-issues/DemosGithubIssues.ts
+++ b/src/demo-app/app/github-issues/DemosGithubIssues.ts
@@ -5,8 +5,9 @@ import {Component} from '@angular/core';
template: `
-
+
+
`
})
export class DemosGithubIssues {
@@ -19,16 +20,18 @@ import {FlexLayoutModule} from "../../../lib"; // `gulp build:components` to
import {DemoIssue5345} from "./issue.5345.demo";
import {DemoIssue9897} from "./issue.9897.demo";
-import {DemoIssue181} from './issue.181.demo';
import {DemoIssue135} from "./issue.135.demo";
+import {DemoIssue181} from './issue.181.demo';
+import {DemoIssue197} from './issue.197.demo';
@NgModule({
declarations: [
DemosGithubIssues, // used by the Router with the root app component
DemoIssue5345,
DemoIssue9897,
+ DemoIssue135,
DemoIssue181,
- DemoIssue135
+ DemoIssue197
],
imports: [
CommonModule,
diff --git a/src/demo-app/app/github-issues/issue.197.demo.ts b/src/demo-app/app/github-issues/issue.197.demo.ts
new file mode 100644
index 000000000..3cf3fcdf3
--- /dev/null
+++ b/src/demo-app/app/github-issues/issue.197.demo.ts
@@ -0,0 +1,58 @@
+import {Component, OnDestroy} from '@angular/core';
+import {Subscription} from "rxjs/Subscription";
+import 'rxjs/add/operator/filter';
+
+import {MediaChange} from "../../../lib/media-query/media-change";
+import {ObservableMedia} from "../../../lib/media-query/observable-media-service";
+
+// [ngStyle="{'font-size.px': 10, color: 'rgb(0,0,0)', 'text-align':'left'}"
+// style="font-size:10px; color:black; text-align:left;"
+@Component({
+ selector: 'demo-issue-197',
+ styleUrls: [
+ '../demo-app/material2.css'
+ ],
+ template: `
+
+
+ Issue #197
+ Responsive Style directive should merge with default inline style:
+
+
+
+
+ <div fxFlexFill
+ style="font-size:10px; color:black; text-align:left;"
+ [style.md]="{'font-size': '16px', color: 'red'}"
+ ngStyle.lg="font-size: 24px; color : #00f;" >
+ </div>
+
+
+
+
+
+ Active mediaQuery: {{ activeMediaQuery }}
+
+
+ `
+})
+export class DemoIssue197 implements OnDestroy {
+ public activeMediaQuery = "";
+
+ constructor(media$: ObservableMedia) {
+ this._watcher = media$.subscribe((change: MediaChange) => {
+ let value = change ? `'${change.mqAlias}' = (${change.mediaQuery})` : "";
+ this.activeMediaQuery = value;
+ });
+ }
+
+ ngOnDestroy() {
+ this._watcher.unsubscribe();
+ }
+
+ private _watcher: Subscription;
+}
diff --git a/src/lib/flexbox/api/class.spec.ts b/src/lib/flexbox/api/class.spec.ts
index 39ee519ae..647c4b49b 100644
--- a/src/lib/flexbox/api/class.spec.ts
+++ b/src/lib/flexbox/api/class.spec.ts
@@ -57,7 +57,7 @@ describe('class directive', () => {
const selector = `class-${mq}`;
it(`should apply '${selector}' with '${mq}' media query`, () => {
fixture = createTestComponent(`
-
+
`);
activateMediaQuery(mq, true);
@@ -67,7 +67,7 @@ describe('class directive', () => {
it('should keep existing class selector', () => {
fixture = createTestComponent(`
-
+
`);
expectNativeEl(fixture).toHaveCssClass('existing-class');
@@ -77,8 +77,8 @@ describe('class directive', () => {
it('should allow more than one responsive breakpoint on one element', () => {
fixture = createTestComponent(`
-
+
`);
activateMediaQuery('xs', true);
@@ -91,9 +91,9 @@ describe('class directive', () => {
it('should work with ngClass object notation', () => {
fixture = createTestComponent(`
-
-
- `);
+
+
+ `);
activateMediaQuery('xs', true);
expectNativeEl(fixture, {hasXs1: true, hasXs2: false}).toHaveCssClass('xs-1');
expectNativeEl(fixture, {hasXs1: true, hasXs2: false}).not.toHaveCssClass('xs-2');
@@ -104,9 +104,9 @@ describe('class directive', () => {
it('should work with ngClass array notation', () => {
fixture = createTestComponent(`
-
-
- `);
+
+
+ `);
activateMediaQuery('xs', true);
expectNativeEl(fixture).toHaveCssClass('xs-1');
expectNativeEl(fixture).toHaveCssClass('xs-2');
diff --git a/src/lib/flexbox/api/class.ts b/src/lib/flexbox/api/class.ts
index fcc5d69fc..0c72ee938 100644
--- a/src/lib/flexbox/api/class.ts
+++ b/src/lib/flexbox/api/class.ts
@@ -32,64 +32,41 @@ export type NgClassType = string | string[] | Set
| {[klass: string]: an
*/
@Directive({
selector: `
- [class.xs],
- [class.gt-xs],
- [class.sm],
- [class.gt-sm],
- [class.md],
- [class.gt-md],
- [class.lg],
- [class.gt-lg],
- [class.xl]
+ [ngClass.xs], [class.xs],
+ [ngClass.gt-xs], [class.gt-xs],
+ [ngClass.sm], [class.sm],
+ [ngClass.gt-sm], [class.gt-sm],
+ [ngClass.md], [class.md],
+ [ngClass.gt-md], [class.gt-md],
+ [ngClass.lg], [class.lg],
+ [ngClass.gt-lg], [class.gt-lg]
`
})
export class ClassDirective extends NgClass implements OnInit, OnChanges, OnDestroy {
- @Input('class.xs')
- set classXs(val: NgClassType) {
- this._base.cacheInput('classXs', val);
- }
-
- @Input('class.gt-xs')
- set classGtXs(val: NgClassType) {
- this._base.cacheInput('classGtXs', val);
- };
-
- @Input('class.sm')
- set classSm(val: NgClassType) {
- this._base.cacheInput('classSm', val);
- };
-
- @Input('class.gt-sm')
- set classGtSm(val: NgClassType) {
- this._base.cacheInput('classGtSm', val);
- };
-
- @Input('class.md')
- set classMd(val: NgClassType) {
- this._base.cacheInput('classMd', val);
- };
-
- @Input('class.gt-md')
- set classGtMd(val: NgClassType) {
- this._base.cacheInput('classGtMd', val);
- };
-
- @Input('class.lg')
- set classLg(val: NgClassType) {
- this._base.cacheInput('classLg', val);
- };
-
- @Input('class.gt-lg')
- set classGtLg(val: NgClassType) {
- this._base.cacheInput('classGtLg', val);
- };
-
- @Input('class.xl')
- set classXl(val: NgClassType) {
- this._base.cacheInput('classXl', val);
- };
-
+ /* tslint:disable */
+ @Input('ngClass.xs') set ngClassXs(val: NgClassType) { this._base.cacheInput('classXs', val, true); }
+ @Input('ngClass.gt-xs') set ngClassGtXs(val: NgClassType) { this._base.cacheInput('classGtXs', val, true); };
+ @Input('ngClass.sm') set ngClassSm(val: NgClassType) { this._base.cacheInput('classSm', val, true); };
+ @Input('ngClass.gt-sm') set ngClassGtSm(val: NgClassType) { this._base.cacheInput('classGtSm', val, true);} ;
+ @Input('ngClass.md') set ngClassMd(val: NgClassType) { this._base.cacheInput('classMd', val, true); };
+ @Input('ngClass.gt-md') set ngClassGtMd(val: NgClassType) { this._base.cacheInput('classGtMd', val, true);};
+ @Input('ngClass.lg') set ngClassLg(val: NgClassType) { this._base.cacheInput('classLg', val, true);};
+ @Input('ngClass.gt-lg') set ngClassGtLg(val: NgClassType) { this._base.cacheInput('classGtLg', val, true); };
+ @Input('ngClass.xl') set ngClassXl(val: NgClassType) { this._base.cacheInput('classXl', val, true); };
+
+ /** Deprecated selectors */
+ @Input('class.xs') set classXs(val: NgClassType) { this._base.cacheInput('classXs', val, true); }
+ @Input('class.gt-xs') set classGtXs(val: NgClassType) { this._base.cacheInput('classGtXs', val, true); };
+ @Input('class.sm') set classSm(val: NgClassType) { this._base.cacheInput('classSm', val, true); };
+ @Input('class.gt-sm') set classGtSm(val: NgClassType) { this._base.cacheInput('classGtSm', val, true); };
+ @Input('class.md') set classMd(val: NgClassType) { this._base.cacheInput('classMd', val, true);};
+ @Input('class.gt-md') set classGtMd(val: NgClassType) { this._base.cacheInput('classGtMd', val, true);};
+ @Input('class.lg') set classLg(val: NgClassType) { this._base.cacheInput('classLg', val, true); };
+ @Input('class.gt-lg') set classGtLg(val: NgClassType) { this._base.cacheInput('classGtLg', val, true); };
+ @Input('class.xl') set classXl(val: NgClassType) { this._base.cacheInput('classXl', val, true); };
+
+ /* tslint:enable */
constructor(private monitor: MediaMonitor,
private _bpRegistry: BreakPointRegistry,
_iterableDiffers: IterableDiffers, _keyValueDiffers: KeyValueDiffers,
@@ -102,7 +79,9 @@ export class ClassDirective extends NgClass implements OnInit, OnChanges, OnDest
* For @Input changes on the current mq activation property, see onMediaQueryChanges()
*/
ngOnChanges(changes: SimpleChanges) {
- const changed = this._bpRegistry.items.some(it => `class${it.suffix}` in changes);
+ const changed = this._bpRegistry.items.some(it => {
+ return (`ngClass${it.suffix}` in changes) || (`class${it.suffix}` in changes);
+ });
if (changed || this._base.mqActivation) {
this._updateStyle();
}
diff --git a/src/lib/flexbox/api/style.spec.ts b/src/lib/flexbox/api/style.spec.ts
index ea0818bb9..d3d5e09ad 100644
--- a/src/lib/flexbox/api/style.spec.ts
+++ b/src/lib/flexbox/api/style.spec.ts
@@ -5,24 +5,23 @@
* 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, Inject
-} from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { MockMatchMedia } from '../../media-query/mock/mock-match-media';
-import { MatchMedia } from '../../media-query/match-media';
-import { ObservableMedia } from '../../media-query/observable-media-service';
-import { BreakPointsProvider } from '../../media-query/breakpoints/break-points';
-import { BreakPointRegistry } from '../../media-query/breakpoints/break-point-registry';
-
-import { customMatchers } from '../../utils/testing/custom-matchers';
+import {Component} from '@angular/core';
+import {CommonModule} from '@angular/common';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+
+import {MockMatchMedia} from '../../media-query/mock/mock-match-media';
+import {MatchMedia} from '../../media-query/match-media';
+import {BreakPointsProvider} from '../../media-query/breakpoints/break-points';
+import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry';
+
+import {LayoutDirective} from './layout';
+import {StyleDirective} from './style';
+import {MediaQueriesModule} from '../../media-query/_module';
+
+import {customMatchers} from '../../utils/testing/custom-matchers';
import {
makeCreateTestComponent, expectNativeEl
} from '../../utils/testing/helpers';
-import { StyleDirective } from './style';
-import { MediaQueriesModule } from '../../media-query/_module';
describe('style directive', () => {
let fixture: ComponentFixture;
@@ -38,10 +37,10 @@ describe('style directive', () => {
// Configure testbed to prepare services
TestBed.configureTestingModule({
imports: [CommonModule, MediaQueriesModule],
- declarations: [TestStyleComponent, StyleDirective],
+ declarations: [TestStyleComponent, LayoutDirective, StyleDirective],
providers: [
BreakPointRegistry, BreakPointsProvider,
- { provide: MatchMedia, useClass: MockMatchMedia }
+ {provide: MatchMedia, useClass: MockMatchMedia}
]
});
});
@@ -53,62 +52,92 @@ describe('style directive', () => {
});
[
- { mq: 'xs', styleStr: "{'font-size': '15px'}", styleObj: { 'font-size': '15px' } },
- { mq: 'sm', styleStr: "{'font-size': '16px'}", styleObj: { 'font-size': '16px' } },
- { mq: 'md', styleStr: "{'font-size': '17px'}", styleObj: { 'font-size': '17px' } },
- { mq: 'lg', styleStr: "{'font-size': '18px'}", styleObj: { 'font-size': '18px' } }
+ {mq: 'xs', styleStr: "{'font-size': '15px'}", styleObj: {'font-size': '15px'}},
+ {mq: 'sm', styleStr: "{'font-size': '16px'}", styleObj: {'font-size': '16px'}},
+ {mq: 'md', styleStr: "{'font-size': '17px'}", styleObj: {'font-size': '17px'}},
+ {mq: 'lg', styleStr: "{'font-size': '18px'}", styleObj: {'font-size': '18px'}}
]
- .forEach(testData => {
- it(`should apply '${testData.styleStr}' with '${testData.mq}' media query`, () => {
- fixture = createTestComponent(`
-
-
- `);
- activateMediaQuery(testData.mq, true);
- expectNativeEl(fixture).toHaveCssStyle(testData.styleObj);
- });
+ .forEach(testData => {
+ it(`should apply '${testData.styleStr}' with '${testData.mq}' media query`, () => {
+ fixture = createTestComponent(`
+
+
+ `);
+ activateMediaQuery(testData.mq);
+ expectNativeEl(fixture).toHaveCssStyle(testData.styleObj);
});
+ });
+
+ it('should merge with default inline styles', () => {
+ fixture = createTestComponent(`
+
+
+ `);
+ expectNativeEl(fixture).toHaveCssStyle({color: 'blue'});
+ activateMediaQuery('xs');
+ expectNativeEl(fixture).toHaveCssStyle({color: 'blue', 'font-size': '15px'});
+ });
- it('should override existing styles', () => {
+ it('should support raw-string notations', () => {
fixture = createTestComponent(`
-
-
- `);
- expectNativeEl(fixture).toHaveCssStyle({ color: 'blue' });
- activateMediaQuery('xs', true);
- expectNativeEl(fixture).toHaveCssStyle({ 'font-size': '15px' });
+
+
+ `);
+ expectNativeEl(fixture).toHaveCssStyle({color: 'blue'});
+ activateMediaQuery('xs');
+ expectNativeEl(fixture).toHaveCssStyle({
+ 'color': 'blue',
+ 'font-size': '15px',
+ 'background-color': 'rgb(252, 41, 41)'
+ });
});
it('should allow more than one responsive breakpoint on one element', () => {
fixture = createTestComponent(`
-
-
- `);
- activateMediaQuery('xs', true);
- expectNativeEl(fixture).toHaveCssStyle({ 'font-size': '16px' });
- expectNativeEl(fixture).not.toHaveCssStyle({ 'font-size': '12px' });
- activateMediaQuery('md', true);
- expectNativeEl(fixture).not.toHaveCssStyle({ 'font-size': '16px' });
- expectNativeEl(fixture).toHaveCssStyle({ 'font-size': '12px' });
+
+
+ `);
+
+ fixture.detectChanges();
+
+ activateMediaQuery('xs');
+ expectNativeEl(fixture).toHaveCssStyle({'display': 'flex'});
+ expectNativeEl(fixture).toHaveCssStyle({'font-size': '16px'});
+ expectNativeEl(fixture).not.toHaveCssStyle({'font-size': '12px'});
+
+ activateMediaQuery('md');
+ expectNativeEl(fixture).not.toHaveCssStyle({'font-size': '16px'});
+ expectNativeEl(fixture).toHaveCssStyle({'font-size': '12px'});
+
+ activateMediaQuery('lg');
+ expectNativeEl(fixture).not.toHaveCssStyle({'font-size': '12px'});
+ expectNativeEl(fixture).not.toHaveCssStyle({'font-size': '16px'});
+ expectNativeEl(fixture).toHaveCssStyle({'font-size': '10px'}); // original is gone
+ expectNativeEl(fixture).toHaveCssStyle({'margin-left': '13px'}); // portion remains
+
});
it('should work with special ngStyle px notation', () => {
fixture = createTestComponent(`
-
-
- `);
- activateMediaQuery('xs', true);
- expectNativeEl(fixture).toHaveCssStyle({ 'font-size': '15px' });
+
+
+ `);
+ activateMediaQuery('xs');
+ expectNativeEl(fixture).toHaveCssStyle({'font-size': '15px'});
});
it('should work with bound values', () => {
fixture = createTestComponent(`
-
-
- `);
- activateMediaQuery('xs', true);
- expectNativeEl(fixture, { fontSize: 19 }).toHaveCssStyle({ 'font-size': '19px' });
+
+
+ `);
+ activateMediaQuery('xs');
+ expectNativeEl(fixture, {fontSize: 19}).toHaveCssStyle({'font-size': '19px'});
});
});
@@ -120,14 +149,8 @@ describe('style directive', () => {
selector: 'test-style-api',
template: `PlaceHolder Template HTML`
})
-export class TestStyleComponent implements OnInit {
+export class TestStyleComponent {
fontSize: number;
-
- constructor( @Inject(ObservableMedia) private media) {
- }
-
- ngOnInit() {
- }
}
diff --git a/src/lib/flexbox/api/style.ts b/src/lib/flexbox/api/style.ts
index b63d23dcf..148c6cf74 100644
--- a/src/lib/flexbox/api/style.ts
+++ b/src/lib/flexbox/api/style.ts
@@ -11,10 +11,10 @@ import {
Input,
OnDestroy,
OnInit,
- Renderer,
OnChanges,
- SimpleChanges,
- KeyValueDiffers
+ Renderer,
+ KeyValueDiffers,
+ SimpleChanges, SecurityContext
} from '@angular/core';
import {NgStyle} from '@angular/common';
@@ -22,9 +22,15 @@ 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';
+import {extendObject} from '../../utils/object-extend';
+import {DomSanitizer} from '@angular/platform-browser';
-/** NgStyle allowed inputs **/
-export type NgStyleType = string | string[] | Set | {[klass: string]: any};
+import {
+ NgStyleRawList,
+ NgStyleType,
+ NgStyleSanitizer,
+ ngStyleUtils as _
+} from '../../utils/style-transforms';
/**
* Directive to add responsive support for ngStyle.
@@ -32,79 +38,76 @@ export type NgStyleType = string | string[] | Set | {[klass: string]: an
*/
@Directive({
selector: `
- [style.xs],
- [style.gt-xs],
- [style.sm],
- [style.gt-sm],
- [style.md],
- [style.gt-md],
- [style.lg],
- [style.gt-lg],
- [style.xl]
+ [ngStyle],
+ [ngStyle.xs], [style.xs],
+ [ngStyle.gt-xs], [style.gt-xs],
+ [ngStyle.sm], [style.sm],
+ [ngStyle.gt-sm], [style.gt-sm],
+ [ngStyle.md], [style.md],
+ [ngStyle.gt-md], [style.gt-md],
+ [ngStyle.lg], [style.lg],
+ [ngStyle.gt-lg], [style.gt-lg],
+ [ngStyle.xl], [style.xl]
`
})
export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDestroy {
- @Input('style.xs')
- set styleXs(val: NgStyleType) {
- this._base.cacheInput('styleXs', val, true);
+ /**
+ * Intercept ngStyle assignments so we cache the default styles
+ * which are merged with activated styles or used as fallbacks.
+ */
+ @Input('ngStyle')
+ set styleBase(val: NgStyleType) {
+ this._base.cacheInput('style', val, true);
+ this.ngStyle = this._base.inputMap['style'];
}
- @Input('style.gt-xs')
- set styleGtXs(val: NgStyleType) {
- this._base.cacheInput('styleGtXs', val, true);
- };
-
- @Input('style.sm')
- set styleSm(val: NgStyleType) {
- this._base.cacheInput('styleSm', val, true);
- };
-
- @Input('style.gt-sm')
- set styleGtSm(val: NgStyleType) {
- this._base.cacheInput('styleGtSm', val, true);
- };
-
- @Input('style.md')
- set styleMd(val: NgStyleType) {
- this._base.cacheInput('styleMd', val, true);
- };
-
- @Input('style.gt-md')
- set styleGtMd(val: NgStyleType) {
- this._base.cacheInput('styleGtMd', val, true);
- };
-
- @Input('style.lg')
- set styleLg(val: NgStyleType) {
- this._base.cacheInput('styleLg', val, true);
- };
-
- @Input('style.gt-lg')
- set styleGtLg(val: NgStyleType) {
- this._base.cacheInput('styleGtLg', val, true);
- };
-
- @Input('style.xl')
- set styleXl(val: NgStyleType) {
- this._base.cacheInput('styleXl', val, true);
- };
-
+ /* tslint:disable */
+ @Input('ngStyle.xs') set ngStyleXs(val: NgStyleType) { this._base.cacheInput('styleXs', val, true); }
+ @Input('ngStyle.gt-xs') set ngStyleGtXs(val: NgStyleType) { this._base.cacheInput('styleGtXs', val, true); };
+ @Input('ngStyle.sm') set ngStyleSm(val: NgStyleType) { this._base.cacheInput('styleSm', val, true); };
+ @Input('ngStyle.gt-sm') set ngStyleGtSm(val: NgStyleType) { this._base.cacheInput('styleGtSm', val, true);} ;
+ @Input('ngStyle.md') set ngStyleMd(val: NgStyleType) { this._base.cacheInput('styleMd', val, true); };
+ @Input('ngStyle.gt-md') set ngStyleGtMd(val: NgStyleType) { this._base.cacheInput('styleGtMd', val, true);};
+ @Input('ngStyle.lg') set ngStyleLg(val: NgStyleType) { this._base.cacheInput('styleLg', val, true);};
+ @Input('ngStyle.gt-lg') set ngStyleGtLg(val: NgStyleType) { this._base.cacheInput('styleGtLg', val, true); };
+ @Input('ngStyle.xl') set ngStyleXl(val: NgStyleType) { this._base.cacheInput('styleXl', val, true); };
+
+ /** Deprecated selectors */
+ @Input('style.xs') set styleXs(val: NgStyleType) { this._base.cacheInput('styleXs', val, true); }
+ @Input('style.gt-xs') set styleGtXs(val: NgStyleType) { this._base.cacheInput('styleGtXs', val, true); };
+ @Input('style.sm') set styleSm(val: NgStyleType) { this._base.cacheInput('styleSm', val, true); };
+ @Input('style.gt-sm') set styleGtSm(val: NgStyleType) { this._base.cacheInput('styleGtSm', val, true); };
+ @Input('style.md') set styleMd(val: NgStyleType) { this._base.cacheInput('styleMd', val, true);};
+ @Input('style.gt-md') set styleGtMd(val: NgStyleType) { this._base.cacheInput('styleGtMd', val, true);};
+ @Input('style.lg') set styleLg(val: NgStyleType) { this._base.cacheInput('styleLg', val, true); };
+ @Input('style.gt-lg') set styleGtLg(val: NgStyleType) { this._base.cacheInput('styleGtLg', val, true); };
+ @Input('style.xl') set styleXl(val: NgStyleType) { this._base.cacheInput('styleXl', val, true); };
+
+ /* tslint:enable */
/**
- *
+ * Constructor for the ngStyle subclass; which adds selectors and
+ * a MediaQuery Activation Adapter
*/
constructor(private monitor: MediaMonitor,
private _bpRegistry: BreakPointRegistry,
- _differs: KeyValueDiffers, _ngEl: ElementRef, _renderer: Renderer) {
+ private _sanitizer: DomSanitizer,
+ _differs: KeyValueDiffers,
+ _ngEl: ElementRef, _renderer: Renderer) {
super(_differs, _ngEl, _renderer);
- this._base = new BaseFxDirectiveAdapter(monitor, _ngEl, _renderer);
+ this._buildAdapter(monitor, _ngEl, _renderer);
+
+ // Get current inline style if any
+ this._base.cacheInput('style', _ngEl.nativeElement.getAttribute("style"), true);
}
/**
* For @Input changes on the current mq activation property, see onMediaQueryChanges()
*/
ngOnChanges(changes: SimpleChanges) {
- const changed = this._bpRegistry.items.some(it => `style${it.suffix}` in changes);
+ const changed = this._bpRegistry.items.some(it => {
+ return (`ngStyle${it.suffix}` in changes) || (`style${it.suffix}` in changes);
+ });
if (changed || this._base.mqActivation) {
this._updateStyle();
}
@@ -118,22 +121,72 @@ export class StyleDirective extends NgStyle implements OnInit, OnChanges, OnDest
this._base.listenForMediaQueryChanges('style', '', (changes: MediaChange) => {
this._updateStyle(changes.value);
});
- this._updateStyle();
}
ngOnDestroy() {
this._base.ngOnDestroy();
}
+ // ************************************************************************
+ // Private Internal Methods
+ // ************************************************************************
+
+ /**
+ * Use the currently activated input property and assign to
+ * `ngStyle` which does the style injections...
+ */
private _updateStyle(value?: NgStyleType) {
let style = value || this._base.queryInput("style") || '';
if (this._base.mqActivation) {
style = this._base.mqActivation.activatedInput;
}
+
// Delegate subsequent activity to the NgStyle logic
this.ngStyle = style;
}
+
+ /**
+ * Build MediaQuery Activation Adapter
+ * This adapter manages listening to mediaQuery change events and identifying
+ * which property value should be used for the style update
+ */
+ private _buildAdapter(monitor: MediaMonitor, _ngEl: ElementRef, _renderer: Renderer) {
+ this._base = new BaseFxDirectiveAdapter(monitor, _ngEl, _renderer);
+
+ // Build intercept to convert raw strings to ngStyleMap
+ let cacheInput = this._base.cacheInput.bind(this._base);
+ this._base.cacheInput = (key?: string, source?: any, cacheRaw = false, merge = true) => {
+ let styles = this._buildStyleMap(source);
+ if (merge) {
+ styles = extendObject({}, this._base.inputMap['style'], styles);
+ }
+ cacheInput(key, styles, cacheRaw);
+ };
+ }
+
+ /**
+ * Convert raw strings to ngStyleMap; which is required by ngStyle
+ * NOTE: Raw string key-value pairs MUST be delimited by `;`
+ * Comma-delimiters are not supported due to complexities of
+ * possible style values such as `rgba(x,x,x,x)` and others
+ */
+ private _buildStyleMap(styles: NgStyleType) {
+ let sanitizer: NgStyleSanitizer = (val: any) => {
+ // Always safe-guard (aka sanitize) style property values
+ return this._sanitizer.sanitize(SecurityContext.STYLE, val);
+ };
+ if (styles) {
+ switch ( _.getType(styles) ) {
+ case 'string': return _.buildMapFromList(_.buildRawList(styles), sanitizer);
+ case 'array' : return _.buildMapFromList(styles as NgStyleRawList, sanitizer);
+ case 'set' : return _.buildMapFromSet(styles, sanitizer);
+ default : return _.buildMapFromSet(styles, sanitizer);
+ }
+ }
+ return styles;
+ }
+
/**
* Special adapter to cross-cut responsive behaviors
* into the StyleDirective
diff --git a/src/lib/utils/style-transforms.spec.ts b/src/lib/utils/style-transforms.spec.ts
new file mode 100644
index 000000000..74acb9524
--- /dev/null
+++ b/src/lib/utils/style-transforms.spec.ts
@@ -0,0 +1,71 @@
+/**
+ * @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 {customMatchers, expect} from './testing/custom-matchers';
+import {NgStyleRawList, NgStyleMap, ngStyleUtils as _} from './style-transforms';
+
+describe('ngStyleUtils', () => {
+ beforeEach(() => {
+ jasmine.addMatchers(customMatchers);
+ });
+
+ it('should parse a raw string of key:value pairs', () => {
+ let list: NgStyleRawList = _.buildRawList(`
+ color:'red';
+ font-size :16px;
+ background-color:rgba(116, 37, 49, 0.72);
+ `);
+
+ expect(list[0]).toEqual("color:'red'");
+ expect(list[1]).toEqual("font-size :16px");
+ expect(list[2]).toEqual("background-color:rgba(116, 37, 49, 0.72)");
+ });
+
+ it('should build an iterable map from a raw string of key:value pairs', () => {
+ let map: NgStyleMap = _.buildMapFromList(_.buildRawList(`
+ color:'red';
+ font-size :16px;
+ background-color:rgba(116, 37, 49, 0.72);
+ `));
+
+ expect(map).toHaveMap({
+ 'color': 'red',
+ 'font-size': '16px',
+ 'background-color': 'rgba(116, 37, 49, 0.72)'
+ });
+ });
+
+ it('should build an iterable map from an Array of key:value strings', () => {
+ let map: NgStyleMap = _.buildMapFromList(_.buildRawList(`
+ color:'red';
+ font-size :16px;
+ background-color:rgba(116, 37, 49, 0.72);
+ `));
+
+ expect(map).toHaveMap({
+ 'color': 'red',
+ 'font-size': '16px',
+ 'background-color': 'rgba(116, 37, 49, 0.72)'
+ });
+ });
+
+ it('should build an iterable map from an Set of key:value pairs', () => {
+ let customSet = new Set();
+ customSet.add("color:'red'");
+ customSet.add("font-size :16px;");
+ customSet.add("background-color:rgba(116, 37, 49, 0.72)");
+
+ let map: NgStyleMap = _.buildMapFromSet(customSet);
+
+ expect(map).toHaveMap({
+ 'color': 'red',
+ 'font-size': '16px',
+ 'background-color': 'rgba(116, 37, 49, 0.72)'
+ });
+ });
+
+});
diff --git a/src/lib/utils/style-transforms.ts b/src/lib/utils/style-transforms.ts
new file mode 100644
index 000000000..67cdc273a
--- /dev/null
+++ b/src/lib/utils/style-transforms.ts
@@ -0,0 +1,112 @@
+/**
+ * @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
+ */
+
+export type NgStyleRawList = string[];
+export type NgStyleMap = {[klass: string]: string};
+// NgStyle selectors accept NgStyleType values
+export type NgStyleType = string | Set | NgStyleRawList | NgStyleMap;
+
+/**
+ * Callback function for SecurityContext.STYLE sanitization
+ */
+export type NgStyleSanitizer = (val: any) => string;
+
+/**
+ * NgStyle allowed inputs
+ */
+export class NgStyleKeyValue {
+ constructor(public key: string, public value: string, noQuotes = true) {
+ this.key = noQuotes ? key.replace(/['"]/g, "").trim() : key.trim();
+
+ this.value = noQuotes ? value.replace(/['"]/g, "").trim() : value.trim();
+ this.value = this.value.replace(/;/, "");
+ }
+}
+
+/**
+ * Transform Operators for @angular/flex-layout NgStyle Directive
+ */
+export const ngStyleUtils = {
+ getType,
+ buildRawList,
+ buildMapFromList,
+ buildMapFromSet
+};
+
+function getType(target: any): string {
+ let what = typeof target;
+ if (what === 'object') {
+ return (target.constructor === Array) ? 'array' :
+ (target.constructor === Set ) ? 'set' : 'object';
+ }
+ return what;
+}
+
+/**
+ * Split string of key:value pairs into Array of k-v pairs
+ * e.g. 'key:value; key:value; key:value;' -> ['key:value',...]
+ */
+function buildRawList(source: any, delimiter = ";"): NgStyleRawList {
+ return String(source)
+ .trim()
+ .split(delimiter)
+ .map((val: string) => val.trim())
+ .filter(val => val !== "");
+}
+
+/**
+ * Convert array of key:value strings to a iterable map object
+ */
+function buildMapFromList(styles: NgStyleRawList, sanitize?: NgStyleSanitizer): NgStyleMap {
+ let sanitizeValue = (it: NgStyleKeyValue) => {
+ if (sanitize) {
+ it.value = sanitize(it.value);
+ }
+ return it;
+ };
+
+ return styles
+ .map(stringToKeyValue)
+ .filter(entry => !!entry)
+ .map(sanitizeValue)
+ .reduce(keyValuesToMap, {});
+};
+
+/**
+ * Convert Set or raw Object to an iterable NgStyleMap
+ */
+function buildMapFromSet(source: any, sanitize?: NgStyleSanitizer): NgStyleMap {
+ let list = new Array();
+ if (getType(source) == 'set') {
+ source.forEach(entry => list.push(entry));
+ } else { // simple hashmap
+ Object.keys(source).forEach(key => {
+ list.push(`${key}:${source[key]}`);
+ });
+ }
+ return buildMapFromList(list, sanitize);
+}
+
+
+/**
+ * Convert "key:value" -> [key, value]
+ */
+function stringToKeyValue(it: string): NgStyleKeyValue {
+ let [key, val] = it.split(":");
+ return val ? new NgStyleKeyValue(key, val) : null;
+};
+
+/**
+ * Convert [ [key,value] ] -> { key : value }
+ */
+function keyValuesToMap(map: NgStyleMap, entry: NgStyleKeyValue): NgStyleMap {
+ if (!!entry.key) {
+ map[entry.key] = entry.value;
+ }
+ return map;
+}
diff --git a/src/lib/utils/testing/custom-matchers.ts b/src/lib/utils/testing/custom-matchers.ts
index 4100d9b3f..d077729f2 100644
--- a/src/lib/utils/testing/custom-matchers.ts
+++ b/src/lib/utils/testing/custom-matchers.ts
@@ -27,6 +27,11 @@ export interface NgMatchers extends jasmine.Matchers {
*/
toHaveText(expected: string): boolean;
+ /**
+ * Compare key:value pairs as matching EXACTLY
+ */
+ toHaveMap(expected: {[k: string]: string}): boolean;
+
/**
* Expect the element to have the given CSS class.
*
@@ -111,6 +116,28 @@ export const customMatchers: jasmine.CustomMatcherFactories = {
}
},
+ toHaveMap : function() {
+ return {
+ compare: function (actual: {[k: string]: string}, map: {[k: string]: string}) {
+ let allPassed: boolean;
+ allPassed = Object.keys(map).length !== 0;
+ Object.keys(map).forEach(key => {
+ allPassed = allPassed && (actual[key] === map[key]);
+ });
+
+ return {
+ pass: allPassed,
+ get message() {
+ return `
+ Expected ${JSON.stringify(actual)} ${!allPassed ? ' ' : 'not '} to contain the
+ "${JSON.stringify(map)}"
+ `;
+ }
+ };
+ }
+ };
+ },
+
toHaveCssStyle: function () {
return {
compare: function (actual: any, styles: {[k: string]: string}|string) {