Skip to content

feat(breakpoint-observer): Emit matching state of each query provided #12506

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions src/cdk/layout/breakpoints-observer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('BreakpointObserver', () => {
});

it('retrieves the whether a query is currently matched', () => {
let query = 'everything starts as true in the FakeMediaMatcher';
const query = 'everything starts as true in the FakeMediaMatcher';
expect(breakpointManager.isMatched(query)).toBeTruthy();
});

Expand All @@ -61,15 +61,15 @@ describe('BreakpointObserver', () => {
});

it('accepts an array of queries', () => {
let queries = ['1 query', '2 query', 'red query', 'blue query'];
const queries = ['1 query', '2 query', 'red query', 'blue query'];
breakpointManager.observe(queries);
expect(mediaMatcher.queryCount).toBe(queries.length);
});

it('completes all events when the breakpoint manager is destroyed', () => {
let firstTest = jasmine.createSpy('test1');
const firstTest = jasmine.createSpy('test1');
breakpointManager.observe('test1').subscribe(undefined, undefined, firstTest);
let secondTest = jasmine.createSpy('test2');
const secondTest = jasmine.createSpy('test2');
breakpointManager.observe('test2').subscribe(undefined, undefined, secondTest);

expect(firstTest).not.toHaveBeenCalled();
Expand All @@ -82,8 +82,8 @@ describe('BreakpointObserver', () => {
});

it('emits an event on the observable when values change', () => {
let query = '(width: 999px)';
let queryMatchState: boolean = false;
const query = '(width: 999px)';
let queryMatchState = false;
breakpointManager.observe(query).subscribe((state: BreakpointState) => {
queryMatchState = state.matches;
});
Expand All @@ -93,14 +93,31 @@ describe('BreakpointObserver', () => {
expect(queryMatchState).toBeFalsy();
});

it('emits an event on the observable with the matching state of all queries provided', () => {
const queryOne = '(width: 999px)';
const queryTwo = '(width: 700px)';
let state: BreakpointState = {matches: false, breakpoints: {}};
breakpointManager.observe([queryOne, queryTwo]).subscribe((breakpoint: BreakpointState) => {
state = breakpoint;
});

mediaMatcher.setMatchesQuery(queryOne, false);
mediaMatcher.setMatchesQuery(queryTwo, false);
expect(state.breakpoints).toEqual({[queryOne]: false, [queryTwo]: false});

mediaMatcher.setMatchesQuery(queryOne, true);
mediaMatcher.setMatchesQuery(queryTwo, false);
expect(state.breakpoints).toEqual({[queryOne]: true, [queryTwo]: false});
});

it('emits a true matches state when the query is matched', () => {
let query = '(width: 999px)';
const query = '(width: 999px)';
mediaMatcher.setMatchesQuery(query, true);
expect(breakpointManager.isMatched(query)).toBeTruthy();
});

it('emits a false matches state when the query is not matched', () => {
let query = '(width: 999px)';
const query = '(width: 999px)';
mediaMatcher.setMatchesQuery(query, false);
expect(breakpointManager.isMatched(query)).toBeTruthy();
});
Expand Down Expand Up @@ -130,7 +147,7 @@ export class FakeMediaQueryList implements MediaQueryList {
@Injectable()
export class FakeMediaMatcher {
/** A map of match media queries. */
private queries: Map<string, FakeMediaQueryList> = new Map();
private queries = new Map<string, FakeMediaQueryList>();

/** The number of distinct queries created in the media matcher during a test. */
get queryCount(): number {
Expand All @@ -139,7 +156,7 @@ export class FakeMediaMatcher {

/** Fakes the match media response to be controlled in tests. */
matchMedia(query: string): FakeMediaQueryList {
let mql = new FakeMediaQueryList(true, query);
const mql = new FakeMediaQueryList(true, query);
this.queries.set(query, mql);
return mql;
}
Expand Down
37 changes: 29 additions & 8 deletions src/cdk/layout/breakpoints-observer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,35 @@ import {coerceArray} from '@angular/cdk/coercion';
export interface BreakpointState {
/** Whether the breakpoint is currently matching. */
matches: boolean;
/**
* A key boolean pair for each query provided to the observe method,
* with its current matched state.
*/
breakpoints: {
[key: string]: boolean;
};
}

/** The current state of a layout breakpoint. */
interface InternalBreakpointState {
/** Whether the breakpoint is currently matching. */
matches: boolean;
/** The media query being to be matched */
query: string;
}

interface Query {
observable: Observable<BreakpointState>;
observable: Observable<InternalBreakpointState>;
mql: MediaQueryList;
}

/** Utility for checking the matching state of @media queries. */
@Injectable({providedIn: 'root'})
export class BreakpointObserver implements OnDestroy {
/** A map of all media queries currently being listened for. */
private _queries: Map<string, Query> = new Map();
private _queries = new Map<string, Query>();
/** A subject for all other observables to takeUntil based on. */
private _destroySubject: Subject<{}> = new Subject();
private _destroySubject = new Subject<void>();

constructor(private mediaMatcher: MediaMatcher, private zone: NgZone) {}

Expand Down Expand Up @@ -59,10 +74,16 @@ export class BreakpointObserver implements OnDestroy {
const queries = splitQueries(coerceArray(value));
const observables = queries.map(query => this._registerQuery(query).observable);

return combineLatest(observables).pipe(map((breakpointStates: BreakpointState[]) => {
return {
matches: breakpointStates.some(state => state && state.matches)
return combineLatest(observables).pipe(map((breakpointStates: InternalBreakpointState[]) => {
const response: BreakpointState = {
matches: false,
breakpoints: {},
};
breakpointStates.forEach((state: InternalBreakpointState) => {
response.matches = response.matches || state.matches;
response.breakpoints[state.query] = state.matches;
});
return response;
}));
}

Expand Down Expand Up @@ -90,11 +111,11 @@ export class BreakpointObserver implements OnDestroy {
.pipe(
takeUntil(this._destroySubject),
startWith(mql),
map((nextMql: MediaQueryList) => ({matches: nextMql.matches}))
map((nextMql: MediaQueryList) => ({query, matches: nextMql.matches}))
);

// Add the MediaQueryList to the set of queries.
const output = {observable: queryObservable, mql: mql};
const output = {observable: queryObservable, mql};
this._queries.set(query, output);
return output;
}
Expand Down