Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit 8307655

Browse files
ThomasBurlesonCaerusKaru
authored andcommitted
feat(core): MediaObserver can report 1..n activations (#994)
Previous versions of MediaObserver suffered from a significant design flaw. Those versions assumed that a breakpoint change would only activate/match a single mediaQuery. Additionally those versions would not (by default) report overlapping activations. Applications interested in notifications for all current activations would therefore not receive proper event-notifications. The current enhancements provide several features: * Report 1..n mediaQuery activations (matches === true) in a single event * Report activations sorted by descending priority * By default, reports include overlapping breakpoint activations * Debounce notifications to a single grouped event > useful to reduce browser reflow thrashing BREAKING CHANGE: The stream data type for `asObservable` is now **MediaChange[]** instead of *MediaChange* and `media$` is deprecated in favor of `asObservable()`. * `filterOverlaps` now defaults to `false`
1 parent ca4c03c commit 8307655

File tree

10 files changed

+278
-189
lines changed

10 files changed

+278
-189
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
import {ChangeDetectionStrategy, Component} from '@angular/core';
1+
import {Component} from '@angular/core';
22
import {MediaChange, MediaObserver} from '@angular/flex-layout';
33
import {Observable} from 'rxjs';
44

55
@Component({
66
selector: 'media-query-status',
77
template: `
8-
<div class="mqInfo" *ngIf="media$ | async as event">
9-
<span title="Active MediaQuery">{{ extractQuery(event) }}</span>
8+
<div class="mqInfo">
9+
Active MediaQuery(s):
10+
<ul>
11+
<li *ngFor="let change of (media$ | async) as changes">
12+
{{change.mqAlias}} = {{change.mediaQuery}}
13+
</li>
14+
</ul>
1015
</div>
1116
`,
1217
styleUrls: ['./media-query-status.component.scss'],
13-
changeDetection : ChangeDetectionStrategy.OnPush
1418
})
1519
export class MediaQueryStatusComponent {
16-
media$: Observable<MediaChange>;
20+
media$: Observable<MediaChange[]>;
1721

18-
constructor(mediaObserver: MediaObserver) {
19-
this.media$ = mediaObserver.media$;
20-
}
21-
22-
extractQuery(change: MediaChange): string {
23-
return change ? `'${change.mqAlias}' = (${change.mediaQuery})` : '';
22+
constructor(media: MediaObserver) {
23+
this.media$ = media.asObservable();
2424
}
2525
}

src/apps/demo-app/src/app/responsive/responsive-row-column/responsive-row-column.component.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export class ResponsiveRowColumnComponent implements OnDestroy {
1717
};
1818
isVisible = true;
1919

20-
private activeMQC: MediaChange;
20+
private activeMQC: MediaChange[];
2121
private subscription: Subscription;
2222

23-
constructor(mediaObserver: MediaObserver) {
24-
this.subscription = mediaObserver.media$
25-
.subscribe((e: MediaChange) => {
26-
this.activeMQC = e;
23+
constructor(mediaService: MediaObserver) {
24+
this.subscription = mediaService.asObservable()
25+
.subscribe((events: MediaChange[]) => {
26+
this.activeMQC = events;
2727
});
2828
}
2929

@@ -32,16 +32,18 @@ export class ResponsiveRowColumnComponent implements OnDestroy {
3232
}
3333

3434
toggleLayoutFor(col: number) {
35-
switch (col) {
36-
case 1:
37-
const set1 = `firstCol${this.activeMQC ? this.activeMQC.suffix : ''}`;
38-
this.cols[set1] = (this.cols[set1] === 'column') ? 'row' : 'column';
39-
break;
35+
this.activeMQC.forEach((change: MediaChange) => {
36+
switch (col) {
37+
case 1:
38+
const set1 = `firstCol${change ? change.suffix : ''}`;
39+
this.cols[set1] = (this.cols[set1] === 'column') ? 'row' : 'column';
40+
break;
4041

41-
case 2:
42-
const set2 = 'secondCol';
43-
this.cols[set2] = (this.cols[set2] === 'row') ? 'column' : 'row';
44-
break;
45-
}
42+
case 2:
43+
const set2 = 'secondCol';
44+
this.cols[set2] = (this.cols[set2] === 'row') ? 'column' : 'row';
45+
break;
46+
}
47+
});
4648
}
4749
}

src/lib/core/add-alias.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,18 @@
77
*/
88
import {MediaChange} from './media-change';
99
import {BreakPoint} from './breakpoints/break-point';
10-
import {extendObject} from '../utils/object-extend';
1110

1211
/**
1312
* For the specified MediaChange, make sure it contains the breakpoint alias
1413
* and suffix (if available).
1514
*/
1615
export function mergeAlias(dest: MediaChange, source: BreakPoint | null): MediaChange {
17-
return extendObject(dest || {}, source ? {
18-
mqAlias: source.alias,
19-
suffix: source.suffix
20-
} : {});
16+
dest = dest ? dest.clone() : new MediaChange();
17+
if (source) {
18+
dest.mqAlias = source.alias;
19+
dest.mediaQuery = source.mediaQuery;
20+
dest.suffix = source.suffix as string;
21+
dest.priority = source.priority as number;
22+
}
23+
return dest;
2124
}

src/lib/core/match-media/match-media.spec.ts

+38-33
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {TestBed, inject} from '@angular/core/testing';
9+
import {BreakPoint} from '@angular/flex-layout/core';
10+
import {Subscription} from 'rxjs';
911

1012
import {MediaChange} from '../media-change';
1113
import {MockMatchMedia, MockMatchMediaProvider} from './mock/mock-match-media';
@@ -16,7 +18,6 @@ import {MediaObserver} from '../media-observer/media-observer';
1618
describe('match-media', () => {
1719
let breakPoints: BreakPointRegistry;
1820
let mediaController: MockMatchMedia;
19-
let mediaObserver: MediaObserver;
2021

2122
beforeEach(() => {
2223
// Configure testbed to prepare services
@@ -31,11 +32,11 @@ describe('match-media', () => {
3132
_breakPoints: BreakPointRegistry) => {
3233
breakPoints = _breakPoints;
3334
mediaController = _matchMedia; // inject only to manually activate mediaQuery ranges
34-
mediaObserver = _mediaObserver;
3535
}));
3636

3737
afterEach(() => {
38-
mediaController.clearAll();
38+
mediaController.clearAll();
39+
mediaController.useOverlaps = false;
3940
});
4041

4142
it('can observe the initial, default activation for mediaQuery == "all". ', () => {
@@ -101,38 +102,41 @@ describe('match-media', () => {
101102
});
102103

103104
describe('match-media-observable', () => {
105+
const watchMedia = (alias: string, observer: (value: MediaChange) => void): Subscription => {
106+
return mediaController
107+
.observe(alias ? [alias] : [])
108+
.subscribe(observer);
109+
};
104110

105111
it('can observe an existing activation', () => {
106112
let current: MediaChange = new MediaChange();
107113
let bp = breakPoints.findByAlias('md')!;
108-
mediaController.activate(bp.mediaQuery);
109-
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
110-
current = change;
111-
});
114+
const onChange = (change: MediaChange) => current = change;
115+
const subscription = watchMedia('md', onChange);
112116

117+
mediaController.activate(bp.mediaQuery);
113118
expect(current.mediaQuery).toEqual(bp.mediaQuery);
114119
subscription.unsubscribe();
115120
});
116121

117122
it('can observe the initial, default activation for mediaQuery == "all". ', () => {
118123
let current: MediaChange = new MediaChange();
119-
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
120-
current = change;
121-
});
124+
const onChange = (change: MediaChange) => current = change;
125+
const subscription = watchMedia('', onChange);
122126

123127
expect(current.mediaQuery).toEqual('all');
124128
subscription.unsubscribe();
125129
});
126130

127131
it('can observe custom mediaQuery ranges', () => {
128132
let current: MediaChange = new MediaChange();
129-
let customQuery = 'screen and (min-width: 610px) and (max-width: 620px)';
130-
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
131-
current = change;
132-
});
133+
const customQuery = 'screen and (min-width: 610px) and (max-width: 620px)';
134+
const onChange = (change: MediaChange) => current = change;
135+
const subscription = watchMedia(customQuery, onChange);
133136

134137
mediaController.useOverlaps = true;
135-
let activated = mediaController.activate(customQuery);
138+
const activated = mediaController.activate(customQuery);
139+
136140
expect(activated).toEqual(true);
137141
expect(current.mediaQuery).toEqual(customQuery);
138142

@@ -141,46 +145,47 @@ describe('match-media', () => {
141145

142146
it('can observe registered breakpoint activations', () => {
143147
let current: MediaChange = new MediaChange();
144-
let bp = breakPoints.findByAlias('md') !;
145-
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
146-
current = change;
147-
});
148+
const onChange = (change: MediaChange) => current = change;
149+
const subscription = watchMedia('md', onChange);
148150

151+
let bp = breakPoints.findByAlias('md') !;
149152
let activated = mediaController.activate(bp.mediaQuery);
150-
expect(activated).toEqual(true);
151153

154+
expect(activated).toEqual(true);
152155
expect(current.mediaQuery).toEqual(bp.mediaQuery);
153156

154157
subscription.unsubscribe();
155158
});
156159

157160
/**
158-
* Only the MediaObserver ignores de-activations;
159161
* MediaMonitor and MatchMedia report both activations and de-activations!
162+
* Only the MediaObserver ignores de-activations;
160163
*/
161-
it('ignores mediaQuery de-activations', () => {
162-
let activationCount = 0;
163-
let deactivationCount = 0;
164-
165-
mediaObserver.filterOverlaps = false;
166-
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
164+
it('reports mediaQuery de-activations', () => {
165+
const lookupMediaQuery = (alias: string) => {
166+
const bp: BreakPoint = breakPoints.findByAlias(alias) as BreakPoint;
167+
return bp.mediaQuery;
168+
};
169+
let activationCount = 0, deactivationCount = 0;
170+
let subscription = watchMedia('', (change: MediaChange) => {
167171
if (change.matches) {
168-
++activationCount;
172+
activationCount += 1;
169173
} else {
170-
++deactivationCount;
174+
deactivationCount += 1;
171175
}
172176
});
173177

174-
mediaController.activate(breakPoints.findByAlias('md')!.mediaQuery);
175-
mediaController.activate(breakPoints.findByAlias('gt-md')!.mediaQuery);
176-
mediaController.activate(breakPoints.findByAlias('lg')!.mediaQuery);
178+
mediaController.activate(lookupMediaQuery('md'));
179+
mediaController.activate(lookupMediaQuery('gt-md'));
180+
mediaController.activate(lookupMediaQuery('lg'));
177181

178182
// 'all' mediaQuery is already active; total count should be (3)
179183
expect(activationCount).toEqual(4);
180-
expect(deactivationCount).toEqual(0);
184+
expect(deactivationCount).toEqual(2);
181185

182186
subscription.unsubscribe();
183187
});
184188

185189
});
186190
});
191+

src/lib/core/match-media/match-media.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ export class MatchMedia {
3232
@Inject(DOCUMENT) protected _document: any) {
3333
}
3434

35+
/**
36+
* Publish list of all current activations
37+
*/
38+
get activations(): string[] {
39+
const results: string[] = [];
40+
this._registry.forEach((mql: MediaQueryList, key: string) => {
41+
if (mql.matches) {
42+
results.push(key);
43+
}
44+
});
45+
return results;
46+
}
47+
3548
/**
3649
* For the specified mediaQuery?
3750
*/
@@ -60,7 +73,7 @@ export class MatchMedia {
6073
* subscribers of notifications.
6174
*/
6275
observe(mqList?: string[], filterOthers = false): Observable<MediaChange> {
63-
if (mqList) {
76+
if (mqList && mqList.length) {
6477
const matchMedia$: Observable<MediaChange> = this._observable$.pipe(
6578
filter((change: MediaChange) => {
6679
return !filterOthers ? true : (mqList.indexOf(change.mediaQuery) > -1);

src/lib/core/media-change.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export class MediaChange {
2323
constructor(public matches = false,
2424
public mediaQuery = 'all',
2525
public mqAlias = '',
26-
public suffix = '') {
26+
public suffix = '',
27+
public priority = 0) {
2728
}
2829

2930
/** Create an exact copy of the MediaChange */

0 commit comments

Comments
 (0)