Skip to content

Commit 68b85ed

Browse files
fix(breakpoint-observer): fix the breakpoint observer emit count and accuracy
The breakpoint observer emits multiple and incorrect states when more than one query changes. Debounce the observer emissions to eliminate the incorrect states and emit once. References #10925
1 parent 5259f22 commit 68b85ed

File tree

2 files changed

+50
-25
lines changed

2 files changed

+50
-25
lines changed

src/cdk/layout/breakpoints-observer.spec.ts

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
import {LayoutModule} from './layout-module';
1010
import {BreakpointObserver, BreakpointState} from './breakpoints-observer';
1111
import {MediaMatcher} from './media-matcher';
12-
import {fakeAsync, TestBed, inject, flush} from '@angular/core/testing';
12+
import {fakeAsync, TestBed, inject, flush, tick} from '@angular/core/testing';
1313
import {Injectable} from '@angular/core';
1414
import {Subscription} from 'rxjs';
15-
import {take} from 'rxjs/operators';
15+
import {skip, take} from 'rxjs/operators';
1616

1717
describe('BreakpointObserver', () => {
1818
let breakpointObserver: BreakpointObserver;
@@ -93,10 +93,10 @@ describe('BreakpointObserver', () => {
9393
queryMatchState = state.matches;
9494
});
9595

96-
flush();
96+
tick();
9797
expect(queryMatchState).toBeTruthy();
9898
mediaMatcher.setMatchesQuery(query, false);
99-
flush();
99+
tick();
100100
expect(queryMatchState).toBeFalsy();
101101
}));
102102

@@ -108,36 +108,54 @@ describe('BreakpointObserver', () => {
108108
breakpointObserver.observe([queryOne, queryTwo]).subscribe((breakpoint: BreakpointState) => {
109109
state = breakpoint;
110110
});
111+
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: true});
111112

112113
mediaMatcher.setMatchesQuery(queryOne, false);
113114
mediaMatcher.setMatchesQuery(queryTwo, false);
114-
flush();
115+
tick();
115116
expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false});
116117

117118
mediaMatcher.setMatchesQuery(queryOne, true);
118119
mediaMatcher.setMatchesQuery(queryTwo, false);
119-
flush();
120+
tick();
120121
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false});
121122
}));
122123

123124
it('emits a true matches state when the query is matched', fakeAsync(() => {
124125
const query = '(width: 999px)';
125126
breakpointObserver.observe(query).subscribe();
126127
mediaMatcher.setMatchesQuery(query, true);
128+
tick();
127129
expect(breakpointObserver.isMatched(query)).toBeTruthy();
128130
}));
129131

130132
it('emits a false matches state when the query is not matched', fakeAsync(() => {
131133
const query = '(width: 999px)';
132134
breakpointObserver.observe(query).subscribe();
133135
mediaMatcher.setMatchesQuery(query, false);
136+
tick();
134137
expect(breakpointObserver.isMatched(query)).toBeFalsy();
135138
}));
136139

140+
it('emits one event when multiple queries change', fakeAsync(() => {
141+
const observer = jasmine.createSpy('observer');
142+
const queryOne = '(width: 700px)';
143+
const queryTwo = '(width: 999px)';
144+
breakpointObserver.observe([queryOne, queryTwo])
145+
.pipe(skip(1))
146+
.subscribe(observer);
147+
148+
mediaMatcher.setMatchesQuery(queryOne, false);
149+
mediaMatcher.setMatchesQuery(queryTwo, false);
150+
151+
tick();
152+
expect(observer).toHaveBeenCalledTimes(1);
153+
}));
154+
137155
it('should not complete other subscribers when preceding subscriber completes', fakeAsync(() => {
138156
const queryOne = '(width: 700px)';
139157
const queryTwo = '(width: 999px)';
140-
const breakpoint = breakpointObserver.observe([queryOne, queryTwo]);
158+
const breakpoint = breakpointObserver.observe([queryOne, queryTwo]).pipe(skip(1));
141159
const subscriptions: Subscription[] = [];
142160
let emittedValues: number[] = [];
143161

@@ -148,14 +166,14 @@ describe('BreakpointObserver', () => {
148166

149167
mediaMatcher.setMatchesQuery(queryOne, true);
150168
mediaMatcher.setMatchesQuery(queryTwo, false);
151-
flush();
169+
tick();
152170

153171
expect(emittedValues).toEqual([1, 2, 3, 4]);
154172
emittedValues = [];
155173

156174
mediaMatcher.setMatchesQuery(queryOne, false);
157175
mediaMatcher.setMatchesQuery(queryTwo, true);
158-
flush();
176+
tick();
159177

160178
expect(emittedValues).toEqual([1, 3, 4]);
161179

@@ -172,7 +190,11 @@ export class FakeMediaQueryList {
172190
/** Toggles the matches state and "emits" a change event. */
173191
setMatches(matches: boolean) {
174192
this.matches = matches;
175-
this._listeners.forEach(listener => listener(this as any));
193+
194+
/** Simulate an asynchronous task. */
195+
setTimeout(() => {
196+
this._listeners.forEach(listener => listener(this as any));
197+
});
176198
}
177199

178200
/** Registers a callback method for change events. */

src/cdk/layout/breakpoints-observer.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
import {Injectable, NgZone, OnDestroy} from '@angular/core';
1010
import {MediaMatcher} from './media-matcher';
11-
import {asapScheduler, combineLatest, Observable, Subject, Observer} from 'rxjs';
12-
import {debounceTime, map, startWith, takeUntil} from 'rxjs/operators';
11+
import {combineLatest, concat, Observable, Subject, Observer} from 'rxjs';
12+
import {debounceTime, map, skip, startWith, take, takeUntil} from 'rxjs/operators';
1313
import {coerceArray} from '@angular/cdk/coercion';
1414

1515

@@ -75,19 +75,22 @@ export class BreakpointObserver implements OnDestroy {
7575
const queries = splitQueries(coerceArray(value));
7676
const observables = queries.map(query => this._registerQuery(query).observable);
7777

78-
return combineLatest(observables).pipe(
79-
debounceTime(0, asapScheduler),
80-
map((breakpointStates: InternalBreakpointState[]) => {
81-
const response: BreakpointState = {
82-
matches: false,
83-
breakpoints: {},
84-
};
85-
breakpointStates.forEach((state: InternalBreakpointState) => {
86-
response.matches = response.matches || state.matches;
87-
response.breakpoints[state.query] = state.matches;
88-
});
89-
return response;
90-
}));
78+
let stateObservable = combineLatest(observables);
79+
// Emit the first state immediately, and then debounce the subsequent emissions.
80+
stateObservable = concat(
81+
stateObservable.pipe(take(1)),
82+
stateObservable.pipe(skip(1), debounceTime(0)));
83+
return stateObservable.pipe(map((breakpointStates: InternalBreakpointState[]) => {
84+
const response: BreakpointState = {
85+
matches: false,
86+
breakpoints: {},
87+
};
88+
breakpointStates.forEach((state: InternalBreakpointState) => {
89+
response.matches = response.matches || state.matches;
90+
response.breakpoints[state.query] = state.matches;
91+
});
92+
return response;
93+
}));
9194
}
9295

9396
/** Registers a specific query to be listened for. */

0 commit comments

Comments
 (0)