diff --git a/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts b/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts
index 24073c11d..c98dc0421 100644
--- a/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts
+++ b/src/apps/demo-app/src/app/media-query-status/media-query-status.component.ts
@@ -1,25 +1,25 @@
-import {ChangeDetectionStrategy, Component} from '@angular/core';
+import {Component} from '@angular/core';
import {MediaChange, MediaObserver} from '@angular/flex-layout';
import {Observable} from 'rxjs';
@Component({
selector: 'media-query-status',
template: `
-
-
{{ extractQuery(event) }}
+
+ Active MediaQuery(s):
+
+ -
+ {{change.mqAlias}} = {{change.mediaQuery}}
+
+
`,
styleUrls: ['./media-query-status.component.scss'],
- changeDetection : ChangeDetectionStrategy.OnPush
})
export class MediaQueryStatusComponent {
- media$: Observable
;
+ media$: Observable;
- constructor(mediaObserver: MediaObserver) {
- this.media$ = mediaObserver.media$;
- }
-
- extractQuery(change: MediaChange): string {
- return change ? `'${change.mqAlias}' = (${change.mediaQuery})` : '';
+ constructor(media: MediaObserver) {
+ this.media$ = media.asObservable();
}
}
diff --git a/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts b/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts
index 9d2b6b291..3eeb58e8f 100644
--- a/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts
+++ b/src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts
@@ -17,13 +17,13 @@ export class ResponsiveRowColumnComponent implements OnDestroy {
};
isVisible = true;
- private activeMQC: MediaChange;
+ private activeMQC: MediaChange[];
private subscription: Subscription;
- constructor(mediaObserver: MediaObserver) {
- this.subscription = mediaObserver.media$
- .subscribe((e: MediaChange) => {
- this.activeMQC = e;
+ constructor(mediaService: MediaObserver) {
+ this.subscription = mediaService.asObservable()
+ .subscribe((events: MediaChange[]) => {
+ this.activeMQC = events;
});
}
@@ -32,16 +32,18 @@ export class ResponsiveRowColumnComponent implements OnDestroy {
}
toggleLayoutFor(col: number) {
- switch (col) {
- case 1:
- const set1 = `firstCol${this.activeMQC ? this.activeMQC.suffix : ''}`;
- this.cols[set1] = (this.cols[set1] === 'column') ? 'row' : 'column';
- break;
+ this.activeMQC.forEach((change: MediaChange) => {
+ switch (col) {
+ case 1:
+ const set1 = `firstCol${change ? change.suffix : ''}`;
+ this.cols[set1] = (this.cols[set1] === 'column') ? 'row' : 'column';
+ break;
- case 2:
- const set2 = 'secondCol';
- this.cols[set2] = (this.cols[set2] === 'row') ? 'column' : 'row';
- break;
- }
+ case 2:
+ const set2 = 'secondCol';
+ this.cols[set2] = (this.cols[set2] === 'row') ? 'column' : 'row';
+ break;
+ }
+ });
}
}
diff --git a/src/lib/core/add-alias.ts b/src/lib/core/add-alias.ts
index f098bb7bf..af61c0572 100644
--- a/src/lib/core/add-alias.ts
+++ b/src/lib/core/add-alias.ts
@@ -7,15 +7,18 @@
*/
import {MediaChange} from './media-change';
import {BreakPoint} from './breakpoints/break-point';
-import {extendObject} from '../utils/object-extend';
/**
* For the specified MediaChange, make sure it contains the breakpoint alias
* and suffix (if available).
*/
export function mergeAlias(dest: MediaChange, source: BreakPoint | null): MediaChange {
- return extendObject(dest || {}, source ? {
- mqAlias: source.alias,
- suffix: source.suffix
- } : {});
+ dest = dest ? dest.clone() : new MediaChange();
+ if (source) {
+ dest.mqAlias = source.alias;
+ dest.mediaQuery = source.mediaQuery;
+ dest.suffix = source.suffix as string;
+ dest.priority = source.priority as number;
+ }
+ return dest;
}
diff --git a/src/lib/core/match-media/match-media.spec.ts b/src/lib/core/match-media/match-media.spec.ts
index b466e9073..1a1f9b3f7 100644
--- a/src/lib/core/match-media/match-media.spec.ts
+++ b/src/lib/core/match-media/match-media.spec.ts
@@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {TestBed, inject} from '@angular/core/testing';
+import {BreakPoint} from '@angular/flex-layout/core';
+import {Subscription} from 'rxjs';
import {MediaChange} from '../media-change';
import {MockMatchMedia, MockMatchMediaProvider} from './mock/mock-match-media';
@@ -16,7 +18,6 @@ import {MediaObserver} from '../media-observer/media-observer';
describe('match-media', () => {
let breakPoints: BreakPointRegistry;
let mediaController: MockMatchMedia;
- let mediaObserver: MediaObserver;
beforeEach(() => {
// Configure testbed to prepare services
@@ -31,11 +32,11 @@ describe('match-media', () => {
_breakPoints: BreakPointRegistry) => {
breakPoints = _breakPoints;
mediaController = _matchMedia; // inject only to manually activate mediaQuery ranges
- mediaObserver = _mediaObserver;
}));
afterEach(() => {
- mediaController.clearAll();
+ mediaController.clearAll();
+ mediaController.useOverlaps = false;
});
it('can observe the initial, default activation for mediaQuery == "all". ', () => {
@@ -101,24 +102,27 @@ describe('match-media', () => {
});
describe('match-media-observable', () => {
+ const watchMedia = (alias: string, observer: (value: MediaChange) => void): Subscription => {
+ return mediaController
+ .observe(alias ? [alias] : [])
+ .subscribe(observer);
+ };
it('can observe an existing activation', () => {
let current: MediaChange = new MediaChange();
let bp = breakPoints.findByAlias('md')!;
- mediaController.activate(bp.mediaQuery);
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
- current = change;
- });
+ const onChange = (change: MediaChange) => current = change;
+ const subscription = watchMedia('md', onChange);
+ mediaController.activate(bp.mediaQuery);
expect(current.mediaQuery).toEqual(bp.mediaQuery);
subscription.unsubscribe();
});
it('can observe the initial, default activation for mediaQuery == "all". ', () => {
let current: MediaChange = new MediaChange();
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
- current = change;
- });
+ const onChange = (change: MediaChange) => current = change;
+ const subscription = watchMedia('', onChange);
expect(current.mediaQuery).toEqual('all');
subscription.unsubscribe();
@@ -126,13 +130,13 @@ describe('match-media', () => {
it('can observe custom mediaQuery ranges', () => {
let current: MediaChange = new MediaChange();
- let customQuery = 'screen and (min-width: 610px) and (max-width: 620px)';
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
- current = change;
- });
+ const customQuery = 'screen and (min-width: 610px) and (max-width: 620px)';
+ const onChange = (change: MediaChange) => current = change;
+ const subscription = watchMedia(customQuery, onChange);
mediaController.useOverlaps = true;
- let activated = mediaController.activate(customQuery);
+ const activated = mediaController.activate(customQuery);
+
expect(activated).toEqual(true);
expect(current.mediaQuery).toEqual(customQuery);
@@ -141,46 +145,47 @@ describe('match-media', () => {
it('can observe registered breakpoint activations', () => {
let current: MediaChange = new MediaChange();
- let bp = breakPoints.findByAlias('md') !;
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
- current = change;
- });
+ const onChange = (change: MediaChange) => current = change;
+ const subscription = watchMedia('md', onChange);
+ let bp = breakPoints.findByAlias('md') !;
let activated = mediaController.activate(bp.mediaQuery);
- expect(activated).toEqual(true);
+ expect(activated).toEqual(true);
expect(current.mediaQuery).toEqual(bp.mediaQuery);
subscription.unsubscribe();
});
/**
- * Only the MediaObserver ignores de-activations;
* MediaMonitor and MatchMedia report both activations and de-activations!
+ * Only the MediaObserver ignores de-activations;
*/
- it('ignores mediaQuery de-activations', () => {
- let activationCount = 0;
- let deactivationCount = 0;
-
- mediaObserver.filterOverlaps = false;
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
+ it('reports mediaQuery de-activations', () => {
+ const lookupMediaQuery = (alias: string) => {
+ const bp: BreakPoint = breakPoints.findByAlias(alias) as BreakPoint;
+ return bp.mediaQuery;
+ };
+ let activationCount = 0, deactivationCount = 0;
+ let subscription = watchMedia('', (change: MediaChange) => {
if (change.matches) {
- ++activationCount;
+ activationCount += 1;
} else {
- ++deactivationCount;
+ deactivationCount += 1;
}
});
- mediaController.activate(breakPoints.findByAlias('md')!.mediaQuery);
- mediaController.activate(breakPoints.findByAlias('gt-md')!.mediaQuery);
- mediaController.activate(breakPoints.findByAlias('lg')!.mediaQuery);
+ mediaController.activate(lookupMediaQuery('md'));
+ mediaController.activate(lookupMediaQuery('gt-md'));
+ mediaController.activate(lookupMediaQuery('lg'));
// 'all' mediaQuery is already active; total count should be (3)
expect(activationCount).toEqual(4);
- expect(deactivationCount).toEqual(0);
+ expect(deactivationCount).toEqual(2);
subscription.unsubscribe();
});
});
});
+
diff --git a/src/lib/core/match-media/match-media.ts b/src/lib/core/match-media/match-media.ts
index dfc9d70c3..a66ce93b2 100644
--- a/src/lib/core/match-media/match-media.ts
+++ b/src/lib/core/match-media/match-media.ts
@@ -32,6 +32,19 @@ export class MatchMedia {
@Inject(DOCUMENT) protected _document: any) {
}
+ /**
+ * Publish list of all current activations
+ */
+ get activations(): string[] {
+ const results: string[] = [];
+ this._registry.forEach((mql: MediaQueryList, key: string) => {
+ if (mql.matches) {
+ results.push(key);
+ }
+ });
+ return results;
+ }
+
/**
* For the specified mediaQuery?
*/
@@ -60,7 +73,7 @@ export class MatchMedia {
* subscribers of notifications.
*/
observe(mqList?: string[], filterOthers = false): Observable {
- if (mqList) {
+ if (mqList && mqList.length) {
const matchMedia$: Observable = this._observable$.pipe(
filter((change: MediaChange) => {
return !filterOthers ? true : (mqList.indexOf(change.mediaQuery) > -1);
diff --git a/src/lib/core/media-change.ts b/src/lib/core/media-change.ts
index 9e9b1e91c..3bdc2b033 100644
--- a/src/lib/core/media-change.ts
+++ b/src/lib/core/media-change.ts
@@ -23,7 +23,8 @@ export class MediaChange {
constructor(public matches = false,
public mediaQuery = 'all',
public mqAlias = '',
- public suffix = '') {
+ public suffix = '',
+ public priority = 0) {
}
/** Create an exact copy of the MediaChange */
diff --git a/src/lib/core/media-observer/media-observer.spec.ts b/src/lib/core/media-observer/media-observer.spec.ts
index 3b6066c0f..ce1dbfbec 100644
--- a/src/lib/core/media-observer/media-observer.spec.ts
+++ b/src/lib/core/media-observer/media-observer.spec.ts
@@ -5,8 +5,9 @@
* 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 {TestBed, inject} from '@angular/core/testing';
-import {filter, map} from 'rxjs/operators';
+import {TestBed, inject, fakeAsync, tick} from '@angular/core/testing';
+import {Observable} from 'rxjs';
+import {filter} from 'rxjs/operators';
import {BreakPoint} from '../breakpoints/break-point';
import {BREAKPOINTS} from '../breakpoints/break-points-token';
@@ -19,8 +20,13 @@ import {DEFAULT_CONFIG, LAYOUT_CONFIG} from '../tokens/library-config';
describe('media-observer', () => {
let knownBreakPoints: BreakPoint[] = [];
+ let media$: Observable;
let mediaObserver: MediaObserver;
let mediaController: MockMatchMedia;
+ const activateQuery = (alias: string) => {
+ mediaController.activate(alias);
+ tick(100); // Since MediaObserver has 50ms debounceTime
+ };
describe('with default BreakPoints', () => {
beforeEach(() => {
@@ -35,10 +41,13 @@ describe('media-observer', () => {
knownBreakPoints = breakpoints;
mediaObserver = _mediaObserver;
mediaController = _mediaController;
+
+ media$ = _mediaObserver.media$;
}));
afterEach(() => {
mediaController.clearAll();
+ mediaController.useOverlaps = false;
});
let findMediaQuery: (alias: string) => string = (alias) => {
@@ -48,7 +57,7 @@ describe('media-observer', () => {
}, null) as string || NOT_FOUND;
};
it('can supports the `.isActive()` API', () => {
- expect(mediaObserver).toBeDefined();
+ expect(media$).toBeDefined();
// Activate mediaQuery associated with 'md' alias
mediaController.activate('md');
@@ -61,44 +70,45 @@ describe('media-observer', () => {
mediaController.clearAll();
});
- it('can supports RxJS operators', () => {
+ it('can supports RxJS operators', fakeAsync(() => {
let count = 0,
- subscription = mediaObserver.media$.pipe(
- filter((change: MediaChange) => change.mqAlias == 'md'),
- map((change: MediaChange) => change.mqAlias)
- ).subscribe(_ => {
+ onlyMd = (change: MediaChange) => (change.mqAlias == 'md'),
+ subscription = media$
+ .pipe(filter(onlyMd))
+ .subscribe(_ => {
count += 1;
});
// Activate mediaQuery associated with 'md' alias
- mediaController.activate('sm');
+ activateQuery('sm');
expect(count).toEqual(0);
- mediaController.activate('md');
+ activateQuery('md');
expect(count).toEqual(1);
- mediaController.activate('lg');
+ activateQuery('lg');
expect(count).toEqual(1);
- mediaController.activate('md');
+ activateQuery('md');
expect(count).toEqual(2);
- mediaController.activate('gt-md');
- mediaController.activate('gt-lg');
- mediaController.activate('invalid');
+ activateQuery('gt-md');
+ activateQuery('gt-lg');
+ activateQuery('invalid');
expect(count).toEqual(2);
subscription.unsubscribe();
- mediaController.clearAll();
- });
+ }));
- it('can subscribe to built-in mediaQueries', () => {
+ it('can subscribe to built-in mediaQueries', fakeAsync(() => {
let current: MediaChange = new MediaChange(true);
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
+ let subscription = media$.subscribe((change: MediaChange) => {
current = change;
});
- expect(mediaObserver).toBeDefined();
+ expect(media$).toBeDefined();
+
+ tick(100);
// Confirm initial match is for 'all'
expect(current).toBeDefined();
@@ -111,63 +121,61 @@ describe('media-observer', () => {
mediaObserver.filterOverlaps = false;
// Activate mediaQuery associated with 'md' alias
- mediaController.activate('md');
+ activateQuery('md');
expect(current.mediaQuery).toEqual(findMediaQuery('md'));
- mediaController.activate('gt-lg');
+ activateQuery('gt-lg');
expect(current.mediaQuery).toEqual(findMediaQuery('gt-lg'));
- mediaController.activate('unknown');
+ activateQuery('unknown');
expect(current.mediaQuery).toEqual(findMediaQuery('gt-lg'));
} finally {
mediaController.autoRegisterQueries = true;
subscription.unsubscribe();
-
- mediaController.clearAll();
}
- });
+ }));
- it('can `.unsubscribe()` properly', () => {
+ it('can `.unsubscribe()` properly', fakeAsync(() => {
let current: MediaChange = new MediaChange(true);
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
+ let subscription = media$.subscribe((change: MediaChange) => {
current = change;
});
// Activate mediaQuery associated with 'md' alias
- mediaController.activate('md');
+ activateQuery('md');
expect(current.mediaQuery).toEqual(findMediaQuery('md'));
// Un-subscribe
subscription.unsubscribe();
- mediaController.activate('lg');
+ activateQuery('lg');
expect(current.mqAlias).toBe('md');
- mediaController.activate('xs');
+ activateQuery('xs');
expect(current.mqAlias).toBe('md');
mediaController.clearAll();
- });
+ }));
- it('can observe a startup activation of XS', () => {
+ it('can observe a startup activation of XS', fakeAsync(() => {
let current: MediaChange = new MediaChange(true);
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
+ let subscription = media$.subscribe((change: MediaChange) => {
current = change;
});
// Activate mediaQuery associated with 'md' alias
- mediaController.activate('xs');
+ activateQuery('xs');
expect(current.mediaQuery).toEqual(findMediaQuery('xs'));
// Un-subscribe
subscription.unsubscribe();
- mediaController.activate('lg');
+ activateQuery('lg');
expect(current.mqAlias).toBe('xs');
mediaController.clearAll();
- });
+ }));
});
describe('with custom BreakPoints', () => {
@@ -195,34 +203,36 @@ describe('media-observer', () => {
knownBreakPoints = breakpoints;
mediaObserver = _mediaObserver;
mediaController = _mediaController;
+
+ media$ = _mediaObserver.media$;
}));
afterEach(() => {
mediaController.clearAll();
});
- it('can activate custom alias with custom mediaQueries', () => {
+ it('can activate custom alias with custom mediaQueries', fakeAsync(() => {
let current: MediaChange = new MediaChange(true);
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
- current = change;
- });
+ let subscription = mediaObserver.asObservable()
+ .subscribe((changes: MediaChange[]) => {
+ current = changes[0];
+ });
// Activate mediaQuery associated with 'md' alias
- mediaController.activate('sm');
+ activateQuery('sm');
expect(current.mediaQuery).toEqual(smMediaQuery);
// MediaObserver will not announce print events
// unless a printAlias layout has been configured.
- mediaController.activate('slate.xl');
+ activateQuery('slate.xl');
expect(current.mediaQuery).toEqual(superXLQuery);
- mediaController.activate('tablet-gt-xs');
+ activateQuery('tablet-gt-xs');
expect(current.mqAlias).toBe('tablet-gt-xs');
expect(current.mediaQuery).toBe(gtXsMediaQuery);
subscription.unsubscribe();
- mediaController.clearAll();
- });
+ }));
});
describe('with layout "print" configured', () => {
@@ -249,32 +259,32 @@ describe('media-observer', () => {
knownBreakPoints = breakpoints;
mediaObserver = _mediaObserver;
mediaController = _mediaController;
+
+ media$ = _mediaObserver.media$;
}));
- it('can activate when configured with "md" alias', () => {
+ it('can activate when configured with "md" alias', fakeAsync(() => {
let current: MediaChange = new MediaChange(true);
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
+ let subscription = media$.subscribe((change: MediaChange) => {
current = change;
});
try {
-
- mediaController.activate('lg');
+ activateQuery('lg');
// Activate mediaQuery associated with 'md' alias
- mediaController.activate('print');
+ activateQuery('print');
expect(current.mqAlias).toBe('md');
expect(current.mediaQuery).toEqual(mdMediaQuery);
- mediaController.activate('sm');
+ activateQuery('sm');
expect(current.mqAlias).toBe('sm');
} finally {
- mediaController.clearAll();
subscription.unsubscribe();
}
- });
+ }));
});
describe('with layout print NOT configured', () => {
@@ -294,29 +304,30 @@ describe('media-observer', () => {
knownBreakPoints = breakpoints;
mediaObserver = _mediaObserver;
mediaController = _mediaController;
+
+ media$ = _mediaObserver.media$;
}));
afterEach(() => {
mediaController.clearAll();
});
- it('will skip print activation without alias', () => {
+ it('will skip print activation without alias', fakeAsync(() => {
let current: MediaChange = new MediaChange(true);
- let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
+ let subscription = media$.subscribe((change: MediaChange) => {
current = change;
});
try {
-
- mediaController.activate('sm');
+ activateQuery('sm');
expect(current.mqAlias).toBe('sm');
// Activate mediaQuery associated with 'md' alias
- mediaController.activate('print');
+ activateQuery('print');
expect(current.mqAlias).toBe('sm');
expect(current.mediaQuery).toEqual(smMediaQuery);
- mediaController.activate('xl');
+ activateQuery('xl');
expect(current.mqAlias).toBe('xl');
} finally {
@@ -324,6 +335,6 @@ describe('media-observer', () => {
mediaController.clearAll();
}
- });
+ }));
});
});
diff --git a/src/lib/core/media-observer/media-observer.ts b/src/lib/core/media-observer/media-observer.ts
index 883598ce2..a64bded95 100644
--- a/src/lib/core/media-observer/media-observer.ts
+++ b/src/lib/core/media-observer/media-observer.ts
@@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable} from '@angular/core';
-import {Observable} from 'rxjs';
-import {filter, map} from 'rxjs/operators';
+import {Observable, of} from 'rxjs';
+import {debounceTime, filter, map, switchMap} from 'rxjs/operators';
import {mergeAlias} from '../add-alias';
import {MediaChange} from '../media-change';
@@ -16,21 +16,19 @@ import {PrintHook} from '../media-marshaller/print-hook';
import {BreakPointRegistry, OptionalBreakPoint} from '../breakpoints/break-point-registry';
/**
- * Class internalizes a MatchMedia service and exposes an Observable interface.
-
- * This exposes an Observable with a feature to subscribe to mediaQuery
- * changes and a validator method (`isActive()`) to test if a mediaQuery (or alias) is
- * currently active.
+ * MediaObserver enables applications to listen for 1..n mediaQuery activations and to determine
+ * if a mediaQuery is currently activated.
*
- * !! Only mediaChange activations (not de-activations) are announced by the MediaObserver
+ * Since a breakpoint change will first deactivate 1...n mediaQueries and then possibly activate
+ * 1..n mediaQueries, the MediaObserver will debounce notifications and report ALL *activations*
+ * in 1 event notification. The reported activations will be sorted in descending priority order.
*
* 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
- * `.media$` with syntax like mediaObserver.media$.map(....).
+ * Note: Developers should note that only mediaChange activations (not de-activations)
+ * are announced by the MediaObserver.
*
* @usage
*
@@ -43,41 +41,70 @@ import {BreakPointRegistry, OptionalBreakPoint} from '../breakpoints/break-point
* status: string = '';
*
* constructor(mediaObserver: MediaObserver) {
- * const onChange = (change: MediaChange) => {
- * this.status = change ? `'${change.mqAlias}' = (${change.mediaQuery})` : '';
- * };
+ * const media$ = mediaObserver.asObservable().pipe(
+ * filter((changes: MediaChange[]) => true) // silly noop filter
+ * );
*
- * // Subscribe directly or access observable to use filter/map operators
- * // e.g. mediaObserver.media$.subscribe(onChange);
+ * media$.subscribe((changes: MediaChange[]) => {
+ * let status = '';
+ * changes.forEach( change => {
+ * status += `'${change.mqAlias}' = (${change.mediaQuery})
` ;
+ * });
+ * this.status = status;
+ * });
*
- * mediaObserver.media$()
- * .pipe(
- * filter((change: MediaChange) => true) // silly noop filter
- * ).subscribe(onChange);
* }
* }
*/
@Injectable({providedIn: 'root'})
export class MediaObserver {
+
/**
- * Whether to announce gt- breakpoint activations
+ * @deprecated Use `asObservable()` instead.
+ * @breaking-change 7.0.0-beta.24
+ * @deletion-target v7.0.0-beta.25
*/
- filterOverlaps = true;
- readonly media$: Observable;
+ get media$(): Observable {
+ return this._media$.pipe(
+ filter((changes: MediaChange[]) => changes.length > 0),
+ map((changes: MediaChange[]) => changes[0])
+ );
+ }
+
+ /** Filter MediaChange notifications for overlapping breakpoints */
+ filterOverlaps = false;
constructor(protected breakpoints: BreakPointRegistry,
- protected mediaWatcher: MatchMedia,
- protected hook: PrintHook) {
- this.media$ = this.watchActivations();
+ protected matchMedia: MatchMedia,
+ protected hook: PrintHook) {
+ this._media$ = this.watchActivations();
+ }
+
+
+ // ************************************************
+ // Public Methods
+ // ************************************************
+
+ /**
+ * Observe changes to current activation 'list'
+ */
+ asObservable(): Observable {
+ return this._media$;
}
/**
- * Test if specified query/alias is active.
+ * Allow programmatic query to determine if specified query/alias is active.
*/
isActive(alias: string): boolean {
- return this.mediaWatcher.isActive(this.toMediaQuery(alias));
+ const query = toMediaQuery(alias, this.breakpoints);
+ return this.matchMedia.isActive(query);
}
+ /**
+ * Subscribers to activation list can use this function to easily exclude overlaps
+ */
+
+
// ************************************************
// Internal Methods
// ************************************************
@@ -93,53 +120,79 @@ export class MediaObserver {
}
/**
- * Prepare internal observable
+ * Only pass/announce activations (not de-activations)
+ *
+ * Since multiple-mediaQueries can be activation in a cycle,
+ * gather all current activations into a single list of changes to observers
+ *
+ * Inject associated (if any) alias information into the MediaChange event
+ * - Exclude mediaQuery activations for overlapping mQs. List bounded mQ ranges only
+ * - Exclude print activations that do not have an associated mediaQuery
*
* NOTE: the raw MediaChange events [from MatchMedia] do not
* contain important alias information; as such this info
* must be injected into the MediaChange
*/
- private buildObservable(mqList: string[]): Observable {
- const locator = this.breakpoints;
- const onlyActivations = (change: MediaChange) => change.matches;
- const excludeUnknown = (change: MediaChange) => change.mediaQuery !== '';
- const excludeCustomPrints = (change: MediaChange) => !change.mediaQuery.startsWith('print');
- const excludeOverlaps = (change: MediaChange) => {
- const bp = locator.findByQuery(change.mediaQuery);
- return !bp ? true : !(this.filterOverlaps && bp.overlapping);
+ private buildObservable(mqList: string[]): Observable {
+ const hasChanges = (changes: MediaChange[]) => {
+ const isValidQuery = (change: MediaChange) => (change.mediaQuery.length > 0);
+ return (changes.filter(isValidQuery).length > 0);
};
- const replaceWithPrintAlias = (change: MediaChange) => {
- if (this.hook.isPrintEvent(change)) {
- // replace with aliased substitute (if configured)
- return this.hook.updateEvent(change);
- }
- let bp: OptionalBreakPoint = locator.findByQuery(change.mediaQuery);
- return mergeAlias(change, bp);
+ const excludeOverlaps = (changes: MediaChange[]) => {
+ return !this.filterOverlaps ? changes : changes.filter(change => {
+ const bp = this.breakpoints.findByQuery(change.mediaQuery);
+ return !bp ? true : !bp.overlapping;
+ });
};
/**
- * Only pass/announce activations (not de-activations)
- *
- * Inject associated (if any) alias information into the MediaChange event
- * - Exclude mediaQuery activations for overlapping mQs. List bounded mQ ranges only
- * - Exclude print activations that do not have an associated mediaQuery
*/
- return this.mediaWatcher.observe(this.hook.withPrintQuery(mqList))
+ return this.matchMedia
+ .observe(this.hook.withPrintQuery(mqList))
.pipe(
- filter(onlyActivations),
- filter(excludeOverlaps),
- map(replaceWithPrintAlias),
- filter(excludeCustomPrints),
- filter(excludeUnknown)
+ filter((change: MediaChange) => change.matches),
+ debounceTime(10),
+ switchMap(_ => of(this.findAllActivations())),
+ map(excludeOverlaps),
+ filter(hasChanges)
);
}
/**
- * Find associated breakpoint (if any)
+ * Find all current activations and prepare single list of activations
+ * sorted by descending priority.
*/
- private toMediaQuery(query: string) {
- const locator = this.breakpoints;
- const bp = locator.findByAlias(query) || locator.findByQuery(query);
- return bp ? bp.mediaQuery : query;
+ private findAllActivations(): MediaChange[] {
+ const mergeMQAlias = (change: MediaChange) => {
+ let bp: OptionalBreakPoint = this.breakpoints.findByQuery(change.mediaQuery);
+ return mergeAlias(change, bp);
+ };
+ const replaceWithPrintAlias = (change: MediaChange) => {
+ return this.hook.isPrintEvent(change) ? this.hook.updateEvent(change) : change;
+ };
+
+ return this.matchMedia
+ .activations
+ .map(query => new MediaChange(true, query))
+ .map(replaceWithPrintAlias)
+ .map(mergeMQAlias)
+ .sort(sortChangesByPriority);
}
+
+ private _media$: Observable;
+}
+
+/**
+ * Find associated breakpoint (if any)
+ */
+function toMediaQuery(query: string, locator: BreakPointRegistry) {
+ const bp = locator.findByAlias(query) || locator.findByQuery(query);
+ return bp ? bp.mediaQuery : query;
+}
+
+/** HOF to sort the breakpoints by priority */
+export function sortChangesByPriority(a: MediaChange, b: MediaChange): number {
+ const priorityA = a ? a.priority || 0 : 0;
+ const priorityB = b ? b.priority || 0 : 0;
+ return priorityB - priorityA;
}
diff --git a/src/lib/core/public-api.ts b/src/lib/core/public-api.ts
index 5d43b586a..1bbadec2b 100644
--- a/src/lib/core/public-api.ts
+++ b/src/lib/core/public-api.ts
@@ -6,11 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
-export * from './browser-provider';
export * from './module';
+export * from './browser-provider';
export * from './media-change';
export * from './stylesheet-map/index';
export * from './tokens/index';
+export * from './add-alias';
export * from './base/index';
export * from './breakpoints/index';
diff --git a/tslint.json b/tslint.json
index c87b09e6b..5a2e904c5 100644
--- a/tslint.json
+++ b/tslint.json
@@ -25,7 +25,7 @@
"no-trailing-whitespace": true,
"no-bitwise": true,
"no-shadowed-variable": true,
- "no-unused-expression": true,
+ "no-unused-expression": [true],
"no-var-keyword": true,
"member-access": [true, "no-public"],
"no-debugger": true,