diff --git a/CHANGELOG.md b/CHANGELOG.md index 842dbf50c..c14e944a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ + +# [2.0.0-beta.3](https://github.com/angular/flex-layout/compare/v2.0.0-beta.2...v2.0.0-beta.3) (2017-01-17) + + # [2.0.0-beta.2](https://github.com/angular/flex-layout/compare/v2.0.0-beta.1...v2.0.0-beta.2) (2017-01-13) diff --git a/src/demo-app/app/docs-layout-responsive/responsiveFlexDirective.demo.ts b/src/demo-app/app/docs-layout-responsive/responsiveFlexDirective.demo.ts index 6e6a9c384..bab889aad 100644 --- a/src/demo-app/app/docs-layout-responsive/responsiveFlexDirective.demo.ts +++ b/src/demo-app/app/docs-layout-responsive/responsiveFlexDirective.demo.ts @@ -3,7 +3,7 @@ import {Subscription} from "rxjs/Subscription"; import 'rxjs/add/operator/filter'; import {MediaChange} from "../../../lib/media-query/media-change"; -import {MatchMediaObservable} from "../../../lib/media-query/match-media"; +import {ObservableMediaService} from "../../../lib/media-query/observable-media-service"; @Component({ selector: 'demo-responsive-flex-directive', @@ -32,7 +32,7 @@ export class DemoResponsiveFlexDirectives implements OnInit, OnDestroy { private _watcher : Subscription; public activeMediaQuery = ""; - constructor(@Inject(MatchMediaObservable) private _media$) { } + constructor(@Inject(ObservableMediaService) private _media$) { } ngOnInit() { this._watcher = this.watchMQChanges(); diff --git a/src/demo-app/app/docs-layout-responsive/responsiveFlexOrder.demo.ts b/src/demo-app/app/docs-layout-responsive/responsiveFlexOrder.demo.ts index 84525cfd3..17475bc28 100644 --- a/src/demo-app/app/docs-layout-responsive/responsiveFlexOrder.demo.ts +++ b/src/demo-app/app/docs-layout-responsive/responsiveFlexOrder.demo.ts @@ -3,7 +3,7 @@ import {Subscription} from "rxjs/Subscription"; import 'rxjs/add/operator/filter'; import {MediaChange} from "../../../lib/media-query/media-change"; -import {MatchMediaObservable} from "../../../lib/media-query/match-media"; +import {ObservableMediaService} from "../../../lib/media-query/observable-media-service"; @Component({ selector: 'demo-responsive-flex-order', @@ -44,7 +44,7 @@ export class DemoResponsiveFlexOrder implements OnInit, OnDestroy { public activeMediaQuery = ""; private _watcher : Subscription; - constructor(@Inject(MatchMediaObservable) private _media$) { } + constructor(@Inject(ObservableMediaService) private _media$) { } ngOnInit() { this._watcher = this.watchMQChanges(); diff --git a/src/demo-app/app/docs-layout-responsive/responsiveRowColumns.demo.ts b/src/demo-app/app/docs-layout-responsive/responsiveRowColumns.demo.ts index 64d7e9ecd..467cc8880 100644 --- a/src/demo-app/app/docs-layout-responsive/responsiveRowColumns.demo.ts +++ b/src/demo-app/app/docs-layout-responsive/responsiveRowColumns.demo.ts @@ -3,7 +3,7 @@ import {Subscription} from "rxjs/Subscription"; import 'rxjs/add/operator/filter'; import {MediaChange} from "../../../lib/media-query/media-change"; -import {MatchMediaObservable} from "../../../lib/media-query/match-media"; +import {ObservableMediaService} from "../../../lib/media-query/observable-media-service"; @Component({ selector: 'demo-responsive-row-column', @@ -64,7 +64,7 @@ export class DemoResponsiveRows implements OnDestroy { isVisible = true; - constructor(@Inject(MatchMediaObservable) private _media$) { + constructor(@Inject(ObservableMediaService) private _media$) { this._watcher = this._media$ .subscribe((e:MediaChange) => { this._activeMQC = e; diff --git a/src/lib/flexbox/api/hide.spec.ts b/src/lib/flexbox/api/hide.spec.ts index bdab4c446..0babea938 100644 --- a/src/lib/flexbox/api/hide.spec.ts +++ b/src/lib/flexbox/api/hide.spec.ts @@ -12,7 +12,8 @@ import {CommonModule} from '@angular/common'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {MockMatchMedia} from '../../media-query/mock/mock-match-media'; -import {MatchMedia, MatchMediaObservable} from '../../media-query/match-media'; +import {MatchMedia} from '../../media-query/match-media'; +import {ObservableMediaService} from '../../media-query/observable-media-service'; import {BreakPointsProvider} from '../../media-query/providers/break-points-provider'; import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry'; @@ -258,7 +259,7 @@ export class TestHideComponent implements OnInit { isHidden = true; menuHidden = true; - constructor(@Inject(MatchMediaObservable) private media) { + constructor(@Inject(ObservableMediaService) private media) { } toggleMenu() { diff --git a/src/lib/flexbox/api/show.spec.ts b/src/lib/flexbox/api/show.spec.ts index 28fda7c83..e13bebdc8 100644 --- a/src/lib/flexbox/api/show.spec.ts +++ b/src/lib/flexbox/api/show.spec.ts @@ -9,8 +9,9 @@ import {Component, OnInit, Inject} from '@angular/core'; import {CommonModule} from '@angular/common'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatchMedia} from '../../media-query/match-media'; import {MockMatchMedia} from '../../media-query/mock/mock-match-media'; -import {MatchMedia, MatchMediaObservable} from '../../media-query/match-media'; +import {ObservableMediaService} from '../../media-query/observable-media-service'; import {BreakPointsProvider} from '../../media-query/providers/break-points-provider'; import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry'; import {FlexLayoutModule} from '../_module'; @@ -214,7 +215,7 @@ export class TestShowComponent implements OnInit { isHidden = false; menuOpen: boolean = true; - constructor(@Inject(MatchMediaObservable) private media) { + constructor(@Inject(ObservableMediaService) private media) { } toggleMenu() { diff --git a/src/lib/media-query/_module.ts b/src/lib/media-query/_module.ts index d1adf1269..8168e9ea5 100644 --- a/src/lib/media-query/_module.ts +++ b/src/lib/media-query/_module.ts @@ -13,7 +13,7 @@ import {BreakPointsProvider} from "./providers/break-points-provider"; import {MatchMedia} from './match-media'; import {MediaMonitor} from './media-monitor'; -import {MatchMediaObservableProvider} from './providers/match-media-observable-provider'; +import {ObservableMediaServiceProvider} from './providers/observable-media-service-provider'; /** * ***************************************************************** @@ -27,7 +27,7 @@ import {MatchMediaObservableProvider} from './providers/match-media-observable-p MediaMonitor, // MediaQuery monitor service observes all known breakpoints BreakPointRegistry, // Registry of known/used BreakPoint(s) BreakPointsProvider, // Supports developer overrides of list of known breakpoints - MatchMediaObservableProvider // easy subscription injectable `media$` matchMedia observable + ObservableMediaServiceProvider // easy subscription injectable `media$` matchMedia observable ] }) export class MediaQueriesModule { diff --git a/src/lib/media-query/index.ts b/src/lib/media-query/index.ts index e4480ef4d..d95dd8485 100644 --- a/src/lib/media-query/index.ts +++ b/src/lib/media-query/index.ts @@ -7,7 +7,7 @@ */ export * from './breakpoints/break-point-registry'; export * from './providers/break-points-provider'; -export * from './providers/match-media-observable-provider'; +export * from './providers/observable-media-service-provider'; export * from './match-media'; export * from './media-change'; export * from './media-monitor'; diff --git a/src/lib/media-query/match-media.spec.ts b/src/lib/media-query/match-media.spec.ts index e682f0835..ed85cb6e2 100644 --- a/src/lib/media-query/match-media.spec.ts +++ b/src/lib/media-query/match-media.spec.ts @@ -18,8 +18,9 @@ import {BreakPoint} from './breakpoints/break-point'; import {MockMatchMedia} from './mock/mock-match-media'; import {BreakPointRegistry} from './breakpoints/break-point-registry'; import {BreakPointsProvider} from './providers/break-points-provider'; -import {MatchMedia, MatchMediaObservable} from './match-media'; -import {MatchMediaObservableProvider} from './providers/match-media-observable-provider'; +import {MatchMedia} from './match-media'; +import {ObservableMediaService} from './observable-media-service'; +import {ObservableMediaServiceProvider} from './providers/observable-media-service-provider'; describe('match-media', () => { let matchMedia: MockMatchMedia; @@ -130,14 +131,14 @@ describe('match-media-observable', () => { BreakPointsProvider, // Supports developer overrides of list of known breakpoints BreakPointRegistry, // Registry of known/used BreakPoint(s) {provide: MatchMedia, useClass: MockMatchMedia}, - MatchMediaObservableProvider // injectable `media$` matchMedia observable + ObservableMediaServiceProvider // injectable `media$` matchMedia observable ] }); }); // Single async inject to save references; which are used in all tests below beforeEach(async(inject( - [MatchMediaObservable, MatchMedia, BreakPointRegistry], + [ObservableMediaService, MatchMedia, BreakPointRegistry], (_media$_, _matchMedia_, _breakPoints_) => { matchMedia = _matchMedia_; // inject only to manually activate mediaQuery ranges breakPoints = _breakPoints_; @@ -204,7 +205,7 @@ describe('match-media-observable', () => { }); /** - * Only the MatchMediaObservable ignores de-activations; + * Only the ObservableMediaService ignores de-activations; * MediaMonitor and MatchMedia report both activations and de-activations! */ it('ignores mediaQuery de-activations', () => { diff --git a/src/lib/media-query/match-media.ts b/src/lib/media-query/match-media.ts index 41939e2da..5e07f84cc 100644 --- a/src/lib/media-query/match-media.ts +++ b/src/lib/media-query/match-media.ts @@ -5,7 +5,6 @@ * 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 {OpaqueToken} from '@angular/core'; import {Injectable, NgZone} from '@angular/core'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; @@ -35,16 +34,6 @@ export interface MediaQueryList { removeListener(listener: MediaQueryListListener): void; } -/** - * Opaque Token unique to the flex-layout library. - * Note: Developers must use this token when building their own custom - * `MatchMediaObservableProvider` provider. - * - * @see ./providers/match-media-observable-provider.ts - */ -// tslint:disable-next-line:variable-name -export const MatchMediaObservable: OpaqueToken = new OpaqueToken('fxObservableMatchMedia'); - /** * MediaMonitor configures listeners to mediaQuery changes and publishes an Observable facade to diff --git a/src/lib/media-query/observable-media-service.ts b/src/lib/media-query/observable-media-service.ts new file mode 100644 index 000000000..f2ccd9660 --- /dev/null +++ b/src/lib/media-query/observable-media-service.ts @@ -0,0 +1,157 @@ +/** + * @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 {OpaqueToken} from '@angular/core'; // tslint:disable-line:no-unused-variable + +import {Subscription} from 'rxjs/Subscription'; +import {Observable, Subscribable} from "rxjs/Observable"; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/filter'; + +import {BreakPointRegistry} from './breakpoints/break-point-registry'; + +import {MediaChange} from './media-change'; +import {MatchMedia} from './match-media'; +import {mergeAlias} from './../utils/add-alias'; +import {BreakPoint} from './breakpoints/break-point'; + + +/** + * Opaque Token unique to the flex-layout library. + * Note: Developers must use this token when building their own custom + * `ObservableMediaServiceProvider` provider. + * + * @see ./providers/match-media-observable-provider.ts + */ +// tslint:disable-next-line:variable-name +export const ObservableMediaService: OpaqueToken = new OpaqueToken('flex-layout-media-service'); + + +/** + * Class internalizes a MatchMedia service and exposes an Subscribable and Observable interface. + + * This an Observable with that exposes a feature to subscribe to mediaQuery + * changes and a validator method (`isActive()`) to test if a mediaQuery (or alias) is + * currently active. + * + * !! Only mediaChange activations (not de-activations) are announced by the ObservableMediaService + * + * This class uses the BreakPoint Registry to inject alias information into the raw MediaChange + * notification. For custom mediaQuery notifications, alias information will not be injected and + * those fields will be ''. + * + * !! This is not an actual Observable. It is a wrapper of an Observable used to publish additional + * methods like `isActive(). To access the Observable and use RxJS operators, use + * `.asObservable()` with syntax like media.asObservable().map(....). + * + * @usage + * + * // RxJS + * import 'rxjs/add/operator/map'; + * + * @Component({ ... }) + * export class AppComponent { + * constructor( @Inject(ObservableMediaService) media) { + * media.asObservable() + * .map( (change:MediaChange) => change.mqAlias == 'md' ) + * .subscribe((change:MediaChange) => { + * console.log( change ? `'${change.mqAlias}' = (${change.mediaQuery})` : "" ); + * }); + * } + * } + */ +export class MediaService implements Subscribable { + private observable$: Observable; + + constructor(private mediaWatcher: MatchMedia, + private breakpoints: BreakPointRegistry) { + this._registerBreakPoints(); + this.observable$ = this._buildObservable(); + } + + /** + * Test if specified query/alias is active. + */ + isActive(alias): boolean { + let query = this._toMediaQuery(alias); + return this.mediaWatcher.isActive(query); + }; + + /** + * Proxy to the Observable subscribe method + */ + subscribe(next?: (value: MediaChange) => void, + error?: (error: any) => void, + complete?: () => void): Subscription { + return this.observable$.subscribe(next, error, complete); + }; + + /** + * Access to observable for use with operators like + * .filter(), .map(), etc. + */ + asObservable(): Observable { + return this.observable$; + } + + // ************************************************ + // Internal Methods + // ************************************************ + + /** + * Register all the mediaQueries registered in the BreakPointRegistry + * This is needed so subscribers can be auto-notified of all standard, registered + * mediaQuery activations + */ + private _registerBreakPoints() { + this.breakpoints.items.forEach((bp: BreakPoint) => { + this.mediaWatcher.registerQuery(bp.mediaQuery); + return bp; + }); + } + + /** + * Prepare internal observable + * NOTE: the raw MediaChange events [from MatchMedia] do not contain important alias information + * these must be injected into the MediaChange + */ + private _buildObservable() { + return this.mediaWatcher.observe() + .filter((change: MediaChange) => { + // Only pass/announce activations (not de-activations) + return change.matches === true; + }) + .map((change: MediaChange) => { + // Inject associated (if any) alias information into the MediaChange event + return mergeAlias(change, this._findByQuery(change.mediaQuery)); + }); + } + + /** + * Breakpoint locator by alias + */ + private _findByAlias(alias) { + return this.breakpoints.findByAlias(alias); + }; + + /** + * Breakpoint locator by mediaQuery + */ + private _findByQuery(query) { + return this.breakpoints.findByQuery(query); + }; + + /** + * Find associated breakpoint (if any) + */ + private _toMediaQuery(query) { + let bp: BreakPoint = this._findByAlias(query) || this._findByQuery(query); + return bp ? bp.mediaQuery : query; + }; + +} diff --git a/src/lib/media-query/providers/match-media-observable-provider.ts b/src/lib/media-query/providers/match-media-observable-provider.ts deleted file mode 100644 index ef7a8201f..000000000 --- a/src/lib/media-query/providers/match-media-observable-provider.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @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 {OpaqueToken} from '@angular/core'; // tslint:disable-line:no-unused-variable - -import {Subscription} from 'rxjs/Subscription'; -import {Observable} from "rxjs/Observable"; -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/filter'; - -import {BreakPointRegistry} from '../breakpoints/break-point-registry'; - -import {MediaChange} from '../media-change'; -import {MatchMedia, MatchMediaObservable} from '../match-media'; -import {mergeAlias} from '../../utils/add-alias'; -import {BreakPoint} from '../breakpoints/break-point'; - - -/** - * Factory returns a simple service instance that exposes a feature to subscribe to mediaQuery - * changes and a validator to test if a mediaQuery (or alias) is currently active. - * - * !! Only mediaChange activations (not de-activations) are announced by the MatchMediaObservable - * - * This factory uses the BreakPoint Registry to inject alias information into the raw MediaChange - * notification. For custom mediaQuery notifications, alias information will not be injected and - * those fields will be ''. - * - * @return Object with two (2) methods: subscribe(observer) and isActive(alias|query) - */ -export function MatchMediaObservableFactory( - mediaWatcher: MatchMedia, - breakpoints: BreakPointRegistry) { - /** - * Only pass/announce activations (not de-activations) - */ - const onlyActivations = function (change: MediaChange) { - return change.matches === true; - }; - /** - * Inject associated (if any) alias information into the MediaChange event - */ - const injectAlias = function (change: MediaChange) { - return mergeAlias(change, findByQuery(change.mediaQuery)); - }; - /** - * Breakpoint locator by alias - */ - const findByAlias = function (alias) { - return breakpoints.findByAlias(alias); - }; - /** - * Breakpoint locator by mediaQuery - */ - const findByQuery = function (query) { - return breakpoints.findByQuery(query); - }; - /** - * Find associated breakpoint (if any) - */ - const toMediaQuery = function (query) { - let bp: BreakPoint = findByAlias(query) || findByQuery(query); - return bp ? bp.mediaQuery : query; - }; - /** - * Proxy to the Observable subscribe method - */ - const subscribe = function (next?: (value: MediaChange) => void, - error?: (error: any) => void, - complete?: () => void): Subscription { - return observable$.subscribe(next, error, complete); - }; - /** - * Test if specified query/alias is active. - */ - const isActive = function (alias): boolean { - return mediaWatcher.isActive(toMediaQuery(alias)); - }; - - // Register all the mediaQueries registered in the BreakPointRegistry - // This is needed so subscribers can be auto-notified of all standard, registered - // mediaQuery activations - breakpoints.items.forEach((bp: BreakPoint) => mediaWatcher.observe(bp.mediaQuery)); - - // Note: the raw MediaChange events [from MatchMedia] do not contain important alias information - // these must be injected into the MediaChange - const observable$: Observable = mediaWatcher.observe() - .filter(onlyActivations).map(injectAlias); - - // Publish service - return { - "subscribe": subscribe, - "isActive": isActive - }; -} - -/** - * Provider to return observable to ALL MediaQuery events - * Developers should build custom providers to override this default MediaQuery Observable - */ -export const MatchMediaObservableProvider = { // tslint:disable-line:variable-name - provide: MatchMediaObservable, - deps: [MatchMedia, BreakPointRegistry], - useFactory: MatchMediaObservableFactory -}; diff --git a/src/lib/media-query/providers/match-media-observable-provider.spec.ts b/src/lib/media-query/providers/observable-media-service-provider.spec.ts similarity index 71% rename from src/lib/media-query/providers/match-media-observable-provider.spec.ts rename to src/lib/media-query/providers/observable-media-service-provider.spec.ts index 8a64c6f10..b9eda0f10 100644 --- a/src/lib/media-query/providers/match-media-observable-provider.spec.ts +++ b/src/lib/media-query/providers/observable-media-service-provider.spec.ts @@ -16,8 +16,9 @@ import {TestBed, inject, async} from '@angular/core/testing'; import {BreakPointRegistry} from '../breakpoints/break-point-registry'; import {MediaChange} from '../media-change'; import {MockMatchMedia} from '../mock/mock-match-media'; -import {MatchMediaObservable, MatchMedia} from '../match-media'; -import {MatchMediaObservableProvider} from './match-media-observable-provider'; +import {MatchMedia} from '../match-media'; +import {ObservableMediaServiceProvider} from './observable-media-service-provider'; +import {ObservableMediaService} from '../observable-media-service'; import {BreakPointsProvider, RAW_DEFAULTS} from './break-points-provider'; describe('match-media-observable-provider', () => { @@ -37,13 +38,13 @@ describe('match-media-observable-provider', () => { BreakPointRegistry, // Registry of known/used BreakPoint(s) BreakPointsProvider, // Supports developer overrides of list of known breakpoints {provide: MatchMedia, useClass: MockMatchMedia}, - MatchMediaObservableProvider + ObservableMediaServiceProvider ] }); }); it('can supports the `.isActive()` API', async(inject( - [MatchMediaObservable, MatchMedia], + [ObservableMediaService, MatchMedia], (media, matchMedia) => { expect(media).toBeDefined(); @@ -57,8 +58,41 @@ describe('match-media-observable-provider', () => { }))); + it('can supports RxJS operators', inject( + [ObservableMediaService, MatchMedia], + (mediaService, matchMedia) => { + let count = 0, + subscription = mediaService + .asObservable() + .filter(change => change.mqAlias == 'md') + .map(change => change.mqAlias) + .subscribe((alias) => { + count += 1; + }); + + // Activate mediaQuery associated with 'md' alias + matchMedia.activate('sm'); + expect(count).toEqual(0); + + matchMedia.activate('md'); + expect(count).toEqual(1); + + matchMedia.activate('lg'); + expect(count).toEqual(1); + + matchMedia.activate('md'); + expect(count).toEqual(2); + + matchMedia.activate('gt-md'); + matchMedia.activate('gt-lg'); + matchMedia.activate('invalid'); + expect(count).toEqual(2); + + subscription.unsubscribe(); + })); + it('can can subscribe to built-in mediaQueries', async(inject( - [MatchMediaObservable, MatchMedia], + [ObservableMediaService, MatchMedia], (media$, matchMedia) => { let current: MediaChange; @@ -93,7 +127,7 @@ describe('match-media-observable-provider', () => { }))); it('can `.unsubscribe()` properly', async(inject( - [MatchMediaObservable, MatchMedia], + [ObservableMediaService, MatchMedia], (media, matchMedia) => { let current: MediaChange; let subscription = media.subscribe((change: MediaChange) => { diff --git a/src/lib/media-query/providers/observable-media-service-provider.ts b/src/lib/media-query/providers/observable-media-service-provider.ts new file mode 100644 index 000000000..f909e88f3 --- /dev/null +++ b/src/lib/media-query/providers/observable-media-service-provider.ts @@ -0,0 +1,22 @@ +/** + * @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 {MediaService, ObservableMediaService} from '../observable-media-service'; +import {BreakPointRegistry} from '../breakpoints/break-point-registry'; +import {MatchMedia} from '../match-media'; + +import {OpaqueToken} from '@angular/core'; // tslint:disable-line:no-unused-variable + +/** + * Provider to return observable to ALL MediaQuery events + * Developers should build custom providers to override this default MediaQuery Observable + */ +export const ObservableMediaServiceProvider = { // tslint:disable-line:variable-name + provide: ObservableMediaService, + useClass: MediaService, + deps: [MatchMedia, BreakPointRegistry] +};