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

Commit d57b293

Browse files
fix(core): improve use of breakpoint priorities (#955)
Use breakpoint priority as the only sorting/scanning mechanism; used to ensure correct MediaChange event notifications. * prioritize breakpoints: non-overlaps hightest, lt- lowest * consistently sort breakpoints ascending by priority * highest priority === smallest range * remove hackery with reverse(), etc. * memoize BreakPointRegistry findBy lookups * fix MatchMedia::observe() to support lazy breakpoint registration * fix fragile logic in MediaMarshaller * fix breakpoint registration usage * clarify update/clear builder function callbacks * fix MediaObserver breakpoint registration usage * cleanup tests: excessive use of `async()`. Fixes #648, Fixes #426.
1 parent f82bbc1 commit d57b293

28 files changed

+662
-608
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ node_modules
1717

1818
# misc
1919
.DS_Store
20+
/.firebase
2021
/.sass-cache
2122
/connect.lock
2223
/coverage/*

src/lib/core/breakpoints/break-point-registry.spec.ts

+3-3
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 {TestBed, inject, async} from '@angular/core/testing';
8+
import {TestBed, inject} from '@angular/core/testing';
99

1010
import {BreakPoint} from './break-point';
1111
import {BreakPointRegistry} from './break-point-registry';
@@ -52,11 +52,11 @@ describe('break-points', () => {
5252
});
5353
});
5454

55-
it('has the custom breakpoints', async(inject([BREAKPOINTS], (list: BreakPoint[]) => {
55+
it('has the custom breakpoints', inject([BREAKPOINTS], (list: BreakPoint[]) => {
5656
expect(list.length).toEqual(CUSTOM_BPS.length);
5757
expect(list[0].alias).toEqual('ab');
5858
expect(list[list.length - 1].suffix).toEqual('Cd');
59-
})));
59+
}));
6060
});
6161

6262
});

src/lib/core/breakpoints/break-point-registry.ts

+32-29
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {Injectable, Inject} from '@angular/core';
99

1010
import {BreakPoint} from './break-point';
1111
import {BREAKPOINTS} from './break-points-token';
12+
import {sortAscendingPriority} from './breakpoint-tools';
1213

14+
type OptionalBreakPoint = BreakPoint | null;
1315

1416
/**
1517
* Registry of 1..n MediaQuery breakpoint ranges
@@ -18,55 +20,36 @@ import {BREAKPOINTS} from './break-points-token';
1820
*/
1921
@Injectable({providedIn: 'root'})
2022
export class BreakPointRegistry {
23+
readonly items: BreakPoint[];
2124

22-
constructor(@Inject(BREAKPOINTS) private _registry: BreakPoint[]) {
23-
}
24-
25-
/**
26-
* Accessor to raw list
27-
*/
28-
get items(): BreakPoint[] {
29-
return [...this._registry];
30-
}
31-
32-
/**
33-
* Accessor to sorted list used for registration with matchMedia API
34-
*
35-
* NOTE: During breakpoint registration, we want to register the overlaps FIRST
36-
* so the non-overlaps will trigger the MatchMedia:BehaviorSubject last!
37-
* And the largest, non-overlap, matching breakpoint should be the lastReplay value
38-
*/
39-
get sortedItems(): BreakPoint[] {
40-
let overlaps = this._registry.filter(it => it.overlapping === true);
41-
let nonOverlaps = this._registry.filter(it => it.overlapping !== true);
42-
43-
return [...overlaps, ...nonOverlaps];
25+
constructor(@Inject(BREAKPOINTS) list: BreakPoint[]) {
26+
this.items = [...list].sort(sortAscendingPriority);
4427
}
4528

4629
/**
4730
* Search breakpoints by alias (e.g. gt-xs)
4831
*/
49-
findByAlias(alias: string): BreakPoint | null {
50-
return this._registry.find(bp => bp.alias == alias) || null;
32+
findByAlias(alias: string): OptionalBreakPoint {
33+
return this.findWithPredicate(alias, (bp) => bp.alias == alias);
5134
}
5235

53-
findByQuery(query: string): BreakPoint | null {
54-
return this._registry.find(bp => bp.mediaQuery == query) || null;
36+
findByQuery(query: string): OptionalBreakPoint {
37+
return this.findWithPredicate(query, (bp) => bp.mediaQuery == query);
5538
}
5639

5740
/**
5841
* Get all the breakpoints whose ranges could overlapping `normal` ranges;
5942
* e.g. gt-sm overlaps md, lg, and xl
6043
*/
6144
get overlappings(): BreakPoint[] {
62-
return this._registry.filter(it => it.overlapping == true);
45+
return this.items.filter(it => it.overlapping == true);
6346
}
6447

6548
/**
6649
* Get list of all registered (non-empty) breakpoint aliases
6750
*/
6851
get aliases(): string[] {
69-
return this._registry.map(it => it.alias);
52+
return this.items.map(it => it.alias);
7053
}
7154

7255
/**
@@ -75,6 +58,26 @@ export class BreakPointRegistry {
7558
* for property layoutGtSM.
7659
*/
7760
get suffixes(): string[] {
78-
return this._registry.map(it => !!it.suffix ? it.suffix : '');
61+
return this.items.map(it => !!it.suffix ? it.suffix : '');
7962
}
63+
64+
/**
65+
* Memoized lookup using custom predicate function
66+
*/
67+
private findWithPredicate(key: string,
68+
searchFn: (bp: BreakPoint) => boolean): OptionalBreakPoint {
69+
let response = this.findByMap.get(key);
70+
if (!response) {
71+
response = this.items.find(searchFn) || null;
72+
this.findByMap.set(key, response);
73+
}
74+
return response || null;
75+
76+
}
77+
78+
/**
79+
* Memoized BreakPoint Lookups
80+
*/
81+
private readonly findByMap = new Map<String, OptionalBreakPoint>();
8082
}
83+

src/lib/core/breakpoints/break-point.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export interface BreakPoint {
99
mediaQuery: string;
1010
alias: string;
1111
suffix?: string;
12-
overlapping?: boolean;
13-
// The priority of the individual breakpoint when overlapping another breakpoint
14-
priority?: number;
12+
overlapping?: boolean; // Does this range overlap with any other ranges
13+
priority?: number; // determine order of activation reporting: higher is last reported
1514
}

src/lib/core/breakpoints/breakpoint-tools.spec.ts

+31-27
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,36 @@
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 {fakeAsync} from '@angular/core/testing';
9-
108
import {BreakPoint} from './break-point';
119
import {validateSuffixes, mergeByAlias} from './breakpoint-tools';
1210
import {DEFAULT_BREAKPOINTS} from './data/break-points';
1311
import {ORIENTATION_BREAKPOINTS} from './data/orientation-break-points';
1412

1513
describe('breakpoint-tools', () => {
16-
let all: BreakPoint[];
17-
let findByAlias = (alias: string): BreakPoint|null => all.reduce((pos: BreakPoint | null, it) => {
18-
return pos || ((it.alias == alias) ? it : null);
19-
}, null);
14+
let allBreakPoints: BreakPoint[] = [];
15+
const findByAlias = (alias: string): BreakPoint => {
16+
let result = {mediaQuery: '', alias: ''};
17+
allBreakPoints.forEach(bp => {
18+
if (bp.alias === alias) {
19+
result = {...bp};
20+
}
21+
});
22+
return result;
23+
};
2024

2125
beforeEach(() => {
22-
all = DEFAULT_BREAKPOINTS.concat(ORIENTATION_BREAKPOINTS);
26+
allBreakPoints = validateSuffixes([...DEFAULT_BREAKPOINTS, ...ORIENTATION_BREAKPOINTS]);
2327
});
2428

25-
describe('validation', () => {
29+
describe('validation ', () => {
30+
2631
it('should not replace an existing suffix', () => {
2732
let validated = validateSuffixes([
2833
{alias: 'handset-portrait', suffix: 'Handset', mediaQuery: 'screen'}
2934
]);
3035
expect(validated[0].suffix).toEqual('Handset');
3136
});
37+
3238
it('should add valid suffixes to breakpoints', () => {
3339
let validated = validateSuffixes([
3440
{alias: 'xs', mediaQuery: 'screen and (max-width: 599px)'},
@@ -43,21 +49,19 @@ describe('breakpoint-tools', () => {
4349
expect(validated[3].suffix).toEqual('GtXs');
4450
expect(validated[4].suffix).toEqual('HandsetPortrait');
4551
});
46-
it('should auto-validate the DEFAULT_BREAKPOINTS', () => {
47-
fakeAsync(() => {
48-
let xsBp: BreakPoint = findByAlias('xs')!;
49-
let gtLgBp: BreakPoint = findByAlias('gt-lg')!;
50-
let xlBp: BreakPoint = findByAlias('xl')!;
5152

52-
expect(xsBp.alias).toEqual('xs');
53-
expect(xsBp.suffix).toEqual('Xs');
53+
it('should auto-validate the DEFAULT_BREAKPOINTS', () => {
54+
let xsBp: BreakPoint = findByAlias('xs');
55+
expect(xsBp.alias).toEqual('xs');
56+
expect(xsBp.suffix).toEqual('Xs');
5457

55-
expect(gtLgBp.alias).toEqual('gt-lg');
56-
expect(gtLgBp.suffix).toEqual('GtLg');
58+
let gtLgBp: BreakPoint = findByAlias('gt-lg');
59+
expect(gtLgBp.alias).toEqual('gt-lg');
60+
expect(gtLgBp.suffix).toEqual('GtLg');
5761

58-
expect(xlBp.alias).toEqual('xl');
59-
expect(xlBp.suffix).toEqual('Xl');
60-
});
62+
let xlBp: BreakPoint = findByAlias('xl');
63+
expect(xlBp.alias).toEqual('xl');
64+
expect(xlBp.suffix).toEqual('Xl');
6165
});
6266
});
6367

@@ -67,19 +71,19 @@ describe('breakpoint-tools', () => {
6771
{alias: 'sm', mediaQuery: 'screen'},
6872
{alias: 'md', mediaQuery: 'screen'},
6973
];
70-
all = mergeByAlias(defaults, custom);
74+
allBreakPoints = mergeByAlias(defaults, custom);
7175

72-
expect(all.length).toEqual(2);
76+
expect(allBreakPoints.length).toEqual(2);
7377
expect(findByAlias('sm')!.suffix).toEqual('Sm');
7478
expect(findByAlias('md')!.suffix).toEqual('Md');
7579
});
7680
it('should add custom breakpoints with unique aliases', () => {
7781
let defaults = [{alias: 'xs', mediaQuery: 'screen and (max-width: 599px)'}],
78-
custom = [{alias: 'sm', mediaQuery: 'screen'}, {alias: 'md', mediaQuery: 'screen'}];
82+
custom = [{alias: 'sm', mediaQuery: 'screen'}, {alias: 'md', mediaQuery: 'screen'}];
7983

80-
all = mergeByAlias(defaults, custom);
84+
allBreakPoints = mergeByAlias(defaults, custom);
8185

82-
expect(all.length).toEqual(3);
86+
expect(allBreakPoints.length).toEqual(3);
8387
expect(findByAlias('xs')!.suffix).toEqual('Xs');
8488
expect(findByAlias('sm')!.suffix).toEqual('Sm');
8589
expect(findByAlias('md')!.suffix).toEqual('Md');
@@ -88,9 +92,9 @@ describe('breakpoint-tools', () => {
8892
let defaults = [{alias: 'xs', mediaQuery: 'screen and (max-width: 599px)'}];
8993
let custom = [{alias: 'xs', mediaQuery: 'screen and none'}];
9094

91-
all = mergeByAlias(defaults, custom);
95+
allBreakPoints = mergeByAlias(defaults, custom);
9296

93-
expect(all.length).toEqual(1);
97+
expect(allBreakPoints.length).toEqual(1);
9498
expect(findByAlias('xs')!.suffix).toEqual('Xs');
9599
expect(findByAlias('xs')!.mediaQuery).toEqual('screen and none');
96100
});

src/lib/core/breakpoints/breakpoint-tools.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,14 @@ export function mergeByAlias(defaults: BreakPoint[], custom: BreakPoint[] = []):
6565
}
6666

6767
/** HOF to sort the breakpoints by priority */
68-
export function prioritySort(a: BreakPoint, b: BreakPoint): number {
68+
export function sortDescendingPriority(a: BreakPoint, b: BreakPoint): number {
6969
const priorityA = a.priority || 0;
7070
const priorityB = b.priority || 0;
7171
return priorityB - priorityA;
7272
}
73+
74+
export function sortAscendingPriority(a: BreakPoint, b: BreakPoint): number {
75+
const pA = a.priority || 0;
76+
const pB = b.priority || 0;
77+
return pA - pB;
78+
}

src/lib/core/breakpoints/data/break-points.spec.ts

+8-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
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 {TestBed, inject, async} from '@angular/core/testing';
8+
import {TestBed, inject} from '@angular/core/testing';
9+
import {sortDescendingPriority} from '../breakpoint-tools';
910

1011
import {BreakPoint} from '../break-point';
1112
import {BREAKPOINTS} from '../break-points-token';
@@ -21,14 +22,14 @@ describe('break-point-provider', () => {
2122
providers: [{provide: BREAKPOINTS, useValue: DEFAULT_BREAKPOINTS}]
2223
});
2324
});
24-
beforeEach(async(inject([BREAKPOINTS], (bps: BreakPoint[]) => {
25-
breakPoints = bps;
26-
})));
25+
beforeEach(inject([BREAKPOINTS], (bps: BreakPoint[]) => {
26+
breakPoints = [...bps].sort(sortDescendingPriority);
27+
}));
2728

2829
it('has the standard breakpoints', () => {
2930
expect(breakPoints.length).toEqual(DEFAULT_BREAKPOINTS.length);
3031
expect(breakPoints[0].alias).toEqual('xs');
31-
expect(breakPoints[breakPoints.length - 1].alias).toEqual('xl');
32+
expect(breakPoints[breakPoints.length - 1].alias).toEqual('gt-xs');
3233
});
3334
});
3435

@@ -57,9 +58,9 @@ describe('break-point-provider', () => {
5758
});
5859
});
5960
// tslint:disable-next-line:no-shadowed-variable
60-
beforeEach(async(inject([BREAKPOINTS], (breakPoints: BreakPoint[]) => {
61+
beforeEach(inject([BREAKPOINTS], (breakPoints: BreakPoint[]) => {
6162
bpList = breakPoints;
62-
})));
63+
}));
6364

6465
it('has the custom breakpoints', () => {
6566
expect(bpList.length).toEqual(CUSTOM_BPS.length);

0 commit comments

Comments
 (0)