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

Commit 51b6ebf

Browse files
authored
fix(core): only emit changed events from MediaObserver (#1377)
1 parent c4f9fe2 commit 51b6ebf

File tree

4 files changed

+55
-23
lines changed

4 files changed

+55
-23
lines changed

projects/libs/flex-layout/core/match-media/mock/mock-match-media.ts

+13-14
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ export class MockMatchMedia extends MatchMedia {
4040
}
4141

4242
/** Feature to support manual, simulated activation of a mediaQuery. */
43-
activate(mediaQuery: string, useOverlaps = false): boolean {
44-
useOverlaps = useOverlaps || this.useOverlaps;
43+
activate(mediaQuery: string, useOverlaps = this.useOverlaps): boolean {
4544
mediaQuery = this._validateQuery(mediaQuery);
4645

4746
if (useOverlaps || !this.isActive(mediaQuery)) {
@@ -55,9 +54,9 @@ export class MockMatchMedia extends MatchMedia {
5554
}
5655

5756
/** Converts an optional mediaQuery alias to a specific, valid mediaQuery */
58-
_validateQuery(queryOrAlias: string) {
57+
_validateQuery(queryOrAlias: string): string {
5958
const bp = this._breakpoints.findByAlias(queryOrAlias);
60-
return (bp && bp.mediaQuery) || queryOrAlias;
59+
return bp?.mediaQuery ?? queryOrAlias;
6160
}
6261

6362
/**
@@ -67,36 +66,36 @@ export class MockMatchMedia extends MatchMedia {
6766
private _activateWithOverlaps(mediaQuery: string, useOverlaps: boolean): boolean {
6867
if (useOverlaps) {
6968
const bp = this._breakpoints.findByQuery(mediaQuery);
70-
const alias = bp ? bp.alias : 'unknown';
69+
const alias = bp?.alias ?? 'unknown';
7170

7271
// Simulate activation of overlapping lt-<XXX> ranges
7372
switch (alias) {
74-
case 'lg' :
73+
case 'lg':
7574
this._activateByAlias(['lt-xl']);
7675
break;
77-
case 'md' :
76+
case 'md':
7877
this._activateByAlias(['lt-xl', 'lt-lg']);
7978
break;
80-
case 'sm' :
79+
case 'sm':
8180
this._activateByAlias(['lt-xl', 'lt-lg', 'lt-md']);
8281
break;
83-
case 'xs' :
82+
case 'xs':
8483
this._activateByAlias(['lt-xl', 'lt-lg', 'lt-md', 'lt-sm']);
8584
break;
8685
}
8786

8887
// Simulate activation of overlapping gt-<xxxx> mediaQuery ranges
8988
switch (alias) {
90-
case 'xl' :
89+
case 'xl':
9190
this._activateByAlias(['gt-lg', 'gt-md', 'gt-sm', 'gt-xs']);
9291
break;
93-
case 'lg' :
92+
case 'lg':
9493
this._activateByAlias(['gt-md', 'gt-sm', 'gt-xs']);
9594
break;
96-
case 'md' :
95+
case 'md':
9796
this._activateByAlias(['gt-sm', 'gt-xs']);
9897
break;
99-
case 'sm' :
98+
case 'sm':
10099
this._activateByAlias(['gt-xs']);
101100
break;
102101
}
@@ -112,7 +111,7 @@ export class MockMatchMedia extends MatchMedia {
112111
private _activateByAlias(aliases: string[]) {
113112
const activate = (alias: string) => {
114113
const bp = this._breakpoints.findByAlias(alias);
115-
this._activateByQuery(bp ? bp.mediaQuery : alias);
114+
this._activateByQuery(bp?.mediaQuery ?? alias);
116115
};
117116
aliases.forEach(activate);
118117
}

projects/libs/flex-layout/core/media-marshaller/media-marshaller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export class MediaMarshaller {
135135
if (bpMap) {
136136
const values = this.getActivatedValues(bpMap, key);
137137
if (values) {
138-
return values.get(key) !== undefined ?? false;
138+
return values.get(key) !== undefined || false;
139139
}
140140
}
141141
return false;

projects/libs/flex-layout/core/media-observer/media-observer.spec.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ describe('media-observer', () => {
2323
let media$: Observable<MediaChange>;
2424
let mediaObserver: MediaObserver;
2525
let mediaController: MockMatchMedia;
26-
const activateQuery = (alias: string) => {
27-
mediaController.activate(alias);
26+
const activateQuery = (alias: string, useOverlaps?: boolean) => {
27+
mediaController.activate(alias, useOverlaps);
2828
tick(100); // Since MediaObserver has 50ms debounceTime
2929
};
3030

@@ -101,6 +101,27 @@ describe('media-observer', () => {
101101
subscription.unsubscribe();
102102
}));
103103

104+
it('only gets one substantive update per media change set', fakeAsync(() => {
105+
let count = 0;
106+
const subscription = mediaObserver.asObservable()
107+
.subscribe(_changes => {
108+
count += 1;
109+
});
110+
111+
// Not a duplicate. This is intentional.
112+
activateQuery('sm', true);
113+
activateQuery('sm', true);
114+
expect(count).toEqual(1);
115+
116+
activateQuery('md', true);
117+
expect(count).toEqual(2);
118+
119+
activateQuery('xl', true);
120+
expect(count).toEqual(3);
121+
122+
subscription.unsubscribe();
123+
}));
124+
104125
it('can subscribe to built-in mediaQueries', fakeAsync(() => {
105126
let current: MediaChange = new MediaChange(true);
106127
let subscription = media$.subscribe((change: MediaChange) => {

projects/libs/flex-layout/core/media-observer/media-observer.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {Injectable, OnDestroy} from '@angular/core';
9-
import {Subject, asapScheduler, Observable, of} from 'rxjs';
9+
import {Subject, asapScheduler, Observable, of, distinctUntilChanged} from 'rxjs';
1010
import {debounceTime, filter, map, switchMap, takeUntil} from 'rxjs/operators';
1111

1212
import {mergeAlias} from '../add-alias';
@@ -153,9 +153,20 @@ export class MediaObserver implements OnDestroy {
153153
const excludeOverlaps = (changes: MediaChange[]) => {
154154
return !this.filterOverlaps ? changes : changes.filter(change => {
155155
const bp = this.breakpoints.findByQuery(change.mediaQuery);
156-
return !bp ? true : !bp.overlapping;
156+
return bp?.overlapping ?? true;
157157
});
158158
};
159+
const ignoreDuplicates = (previous: MediaChange[], current: MediaChange[]): boolean => {
160+
if (previous.length !== current.length) {
161+
return false;
162+
}
163+
164+
const previousMqs = previous.map(mc => mc.mediaQuery);
165+
const currentMqs = new Set(current.map(mc => mc.mediaQuery));
166+
const difference = new Set(previousMqs.filter(mq => !currentMqs.has(mq)));
167+
168+
return difference.size === 0;
169+
};
159170

160171
/**
161172
*/
@@ -167,6 +178,7 @@ export class MediaObserver implements OnDestroy {
167178
switchMap(_ => of(this.findAllActivations())),
168179
map(excludeOverlaps),
169180
filter(hasChanges),
181+
distinctUntilChanged(ignoreDuplicates),
170182
takeUntil(this.destroyed$)
171183
);
172184
}
@@ -177,7 +189,7 @@ export class MediaObserver implements OnDestroy {
177189
*/
178190
private findAllActivations(): MediaChange[] {
179191
const mergeMQAlias = (change: MediaChange) => {
180-
let bp: OptionalBreakPoint = this.breakpoints.findByQuery(change.mediaQuery);
192+
const bp: OptionalBreakPoint = this.breakpoints.findByQuery(change.mediaQuery);
181193
return mergeAlias(change, bp);
182194
};
183195
const replaceWithPrintAlias = (change: MediaChange) => {
@@ -199,9 +211,9 @@ export class MediaObserver implements OnDestroy {
199211
/**
200212
* Find associated breakpoint (if any)
201213
*/
202-
function toMediaQuery(query: string, locator: BreakPointRegistry) {
203-
const bp = locator.findByAlias(query) || locator.findByQuery(query);
204-
return bp ? bp.mediaQuery : null;
214+
function toMediaQuery(query: string, locator: BreakPointRegistry): string | null {
215+
const bp = locator.findByAlias(query) ?? locator.findByQuery(query);
216+
return bp?.mediaQuery ?? null;
205217
}
206218

207219
/**

0 commit comments

Comments
 (0)