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

Commit 2ca7848

Browse files
authored
fix(match-media): unregister media query event listeners on destroy (#1236)
1 parent 5085909 commit 2ca7848

File tree

3 files changed

+62
-50
lines changed

3 files changed

+62
-50
lines changed

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

+22-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {Inject, Injectable, NgZone, PLATFORM_ID} from '@angular/core';
8+
import {Inject, Injectable, NgZone, OnDestroy, PLATFORM_ID} from '@angular/core';
99
import {DOCUMENT, isPlatformBrowser} from '@angular/common';
1010
import {BehaviorSubject, Observable, merge, Observer} from 'rxjs';
1111
import {filter} from 'rxjs/operators';
@@ -20,10 +20,11 @@ import {MediaChange} from '../media-change';
2020
* NOTE: both mediaQuery activations and de-activations are announced in notifications
2121
*/
2222
@Injectable({providedIn: 'root'})
23-
export class MatchMedia {
23+
export class MatchMedia implements OnDestroy {
2424
/** Initialize source with 'all' so all non-responsive APIs trigger style updates */
2525
readonly source = new BehaviorSubject<MediaChange>(new MediaChange(true));
2626
registry = new Map<string, MediaQueryList>();
27+
private readonly pendingRemoveListenerFns: Array<() => void> = [];
2728

2829
constructor(protected _zone: NgZone,
2930
@Inject(PLATFORM_ID) protected _platformId: Object,
@@ -73,9 +74,8 @@ export class MatchMedia {
7374
observe(mqList?: string[], filterOthers = false): Observable<MediaChange> {
7475
if (mqList && mqList.length) {
7576
const matchMedia$: Observable<MediaChange> = this._observable$.pipe(
76-
filter((change: MediaChange) => {
77-
return !filterOthers ? true : (mqList.indexOf(change.mediaQuery) > -1);
78-
})
77+
filter((change: MediaChange) =>
78+
!filterOthers ? true : (mqList.indexOf(change.mediaQuery) > -1))
7979
);
8080
const registration$: Observable<MediaChange> = new Observable((observer: Observer<MediaChange>) => { // tslint:disable-line:max-line-length
8181
const matches: Array<MediaChange> = this.registerQuery(mqList);
@@ -113,6 +113,7 @@ export class MatchMedia {
113113
if (!mql) {
114114
mql = this.buildMQL(query);
115115
mql.addListener(onMQLEvent);
116+
this.pendingRemoveListenerFns.push(() => mql!.removeListener(onMQLEvent));
116117
this.registry.set(query, mql);
117118
}
118119

@@ -124,6 +125,13 @@ export class MatchMedia {
124125
return matches;
125126
}
126127

128+
ngOnDestroy(): void {
129+
let fn;
130+
while (fn = this.pendingRemoveListenerFns.pop()) {
131+
fn();
132+
}
133+
}
134+
127135
/**
128136
* Call window.matchMedia() to build a MediaQueryList; which
129137
* supports 0..n listeners for activation/deactivation
@@ -188,6 +196,14 @@ function constructMql(query: string, isBrowser: boolean): MediaQueryList {
188196
addListener: () => {
189197
},
190198
removeListener: () => {
199+
},
200+
onchange: null,
201+
addEventListener() {
202+
},
203+
removeEventListener() {
204+
},
205+
dispatchEvent() {
206+
return false;
191207
}
192-
} as unknown as MediaQueryList;
208+
} as MediaQueryList;
193209
}

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

+39-41
Original file line numberDiff line numberDiff line change
@@ -23,54 +23,52 @@ describe('media-trigger', () => {
2323
tick(100); // Since MediaObserver has 50ms debounceTime
2424
};
2525

26-
describe('', () => {
27-
beforeEach(() => {
28-
// Configure testbed to prepare services
29-
TestBed.configureTestingModule({
30-
providers: [
31-
MockMatchMediaProvider,
32-
MediaTrigger
33-
]
34-
});
26+
beforeEach(() => {
27+
// Configure testbed to prepare services
28+
TestBed.configureTestingModule({
29+
providers: [
30+
MockMatchMediaProvider,
31+
MediaTrigger
32+
]
3533
});
34+
});
3635

37-
beforeEach(inject([MediaObserver, MediaTrigger, MatchMedia],
38-
(_mediaObserver: MediaObserver, _mediaTrigger: MediaTrigger, _matchMedia: MockMatchMedia) => { // tslint:disable-line:max-line-length
39-
mediaObserver = _mediaObserver;
40-
mediaTrigger = _mediaTrigger;
41-
matchMedia = _matchMedia;
36+
beforeEach(inject([MediaObserver, MediaTrigger, MatchMedia],
37+
(_mediaObserver: MediaObserver, _mediaTrigger: MediaTrigger, _matchMedia: MockMatchMedia) => { // tslint:disable-line:max-line-length
38+
mediaObserver = _mediaObserver;
39+
mediaTrigger = _mediaTrigger;
40+
matchMedia = _matchMedia;
4241

43-
_matchMedia.useOverlaps = true;
44-
}));
42+
_matchMedia.useOverlaps = true;
43+
}));
4544

46-
it('can trigger activations with list of breakpoint aliases', fakeAsync(() => {
47-
let activations: MediaChange[] = [];
48-
let subscription = mediaObserver.asObservable().subscribe(
49-
(changes: MediaChange[]) => {
50-
activations = [...changes];
51-
});
45+
it('can trigger activations with list of breakpoint aliases', fakeAsync(() => {
46+
let activations: MediaChange[] = [];
47+
let subscription = mediaObserver.asObservable().subscribe(
48+
(changes: MediaChange[]) => {
49+
activations = [...changes];
50+
});
5251

53-
// assign default activation(s) with overlaps allowed
54-
matchMedia.activate('xl');
55-
const originalActivations = matchMedia.activations.length;
52+
// assign default activation(s) with overlaps allowed
53+
matchMedia.activate('xl');
54+
const originalActivations = matchMedia.activations.length;
5655

57-
// Activate mediaQuery associated with 'md' alias
58-
activateQuery(['sm']);
59-
expect(activations.length).toEqual(1);
60-
expect(activations[0].mqAlias).toEqual('sm');
56+
// Activate mediaQuery associated with 'md' alias
57+
activateQuery(['sm']);
58+
expect(activations.length).toEqual(1);
59+
expect(activations[0].mqAlias).toEqual('sm');
6160

62-
// Activations are sorted by descending priority
63-
activateQuery(['lt-lg', 'md']);
64-
expect(activations.length).toEqual(2);
65-
expect(activations[0].mqAlias).toEqual('md');
66-
expect(activations[1].mqAlias).toEqual('lt-lg');
61+
// Activations are sorted by descending priority
62+
activateQuery(['lt-lg', 'md']);
63+
expect(activations.length).toEqual(2);
64+
expect(activations[0].mqAlias).toEqual('md');
65+
expect(activations[1].mqAlias).toEqual('lt-lg');
6766

68-
// Clean manual activation overrides
69-
mediaTrigger.restore();
70-
tick(100);
71-
expect(activations.length).toEqual(originalActivations);
67+
// Clean manual activation overrides
68+
mediaTrigger.restore();
69+
tick(100);
70+
expect(activations.length).toEqual(originalActivations);
7271

73-
subscription.unsubscribe();
74-
}));
75-
});
72+
subscription.unsubscribe();
73+
}));
7674
});

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

+1-3
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,9 @@ export class MediaTrigger {
5555
const extractQuery = (change: MediaChange) => change.mediaQuery;
5656
const list = this.originalActivations.map(extractQuery);
5757
try {
58-
5958
this.deactivateAll();
6059
this.restoreRegistryMatches();
6160
this.setActivations(list);
62-
6361
} finally {
6462
this.originalActivations = [];
6563
if (this.resizeSubscription) {
@@ -151,7 +149,7 @@ export class MediaTrigger {
151149
private forceRegistryMatches(queries: string[], matches: boolean) {
152150
const registry = new Map<string, MediaQueryList>();
153151
queries.forEach(query => {
154-
registry.set(query, {matches: matches} as MediaQueryList);
152+
registry.set(query, {matches} as MediaQueryList);
155153
});
156154

157155
this.matchMedia.registry = registry;

0 commit comments

Comments
 (0)