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

Commit 1205588

Browse files
CaerusKaruThomasBurleson
authored andcommitted
feat(media-observer): migrate ObservableMedia (#892)
BREAKING CHANGE: `ObservableMedia` is now deprecated in anticipation of RxJS v7. The new API is called **`MediaObserver`**, and provides the exact same functionality as ObservableMedia, except you cannot directly subscribe to it, You can subscribe to MediaObserver's `media$` property; in place of subscribing directly to ObservableMedia. Fixes #885.
1 parent ad3e9c9 commit 1205588

File tree

17 files changed

+520
-229
lines changed

17 files changed

+520
-229
lines changed

src/lib/core/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The `core` entrypoint contains all of the common utilities to build Layout
22
components. Its primary exports are the `MediaQuery` utilities (`MatchMedia`,
3-
`ObservableMedia`) and the module that encapsulates the imports of these
3+
`MediaObserver`) and the module that encapsulates the imports of these
44
providers, the `CoreModule`, and the base directive for layout
55
components, `BaseDirective`. These utilies can be imported separately
66
from the root module to take advantage of tree shaking.
@@ -22,4 +22,4 @@ export class AppModule {}
2222
import {BaseDirective} from '@angular/flex-layout/core';
2323

2424
export class NewLayoutDirective extends BaseDirective {}
25-
```
25+
```

src/lib/core/base/base.ts

+7-16
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ import {StyleBuilder} from '../style-builder/style-builder';
2525

2626
/** Abstract base class for the Layout API styling directives. */
2727
export abstract class BaseDirective implements OnDestroy, OnChanges {
28-
get hasMediaQueryListener() {
29-
return !!this._mqActivation;
30-
}
3128

3229
/**
3330
* Imperatively determine the current activated [input] value;
@@ -52,7 +49,7 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
5249
previousVal = this._inputMap[key];
5350
this._inputMap[key] = value;
5451
}
55-
let change = new SimpleChange(previousVal, value, false);
52+
const change = new SimpleChange(previousVal, value, false);
5653

5754
this.ngOnChanges({[key]: change} as SimpleChanges);
5855
}
@@ -137,8 +134,8 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
137134
* If not, use the fallback value!
138135
*/
139136
protected _getDefaultVal(key: string, fallbackVal: any): string | boolean {
140-
let val = this._queryInput(key);
141-
let hasDefaultVal = (val !== undefined && val !== null);
137+
const val = this._queryInput(key);
138+
const hasDefaultVal = (val !== undefined && val !== null);
142139
return (hasDefaultVal && val !== '') ? val : fallbackVal;
143140
}
144141

@@ -165,20 +162,19 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
165162
* And optionally add the flow value to element's inline style.
166163
*/
167164
protected _getFlexFlowDirection(target: HTMLElement, addIfMissing = false): string {
168-
let value = 'row';
169-
let hasInlineValue = '';
170-
171165
if (target) {
172-
[value, hasInlineValue] = this._styler.getFlowDirection(target);
166+
let [value, hasInlineValue] = this._styler.getFlowDirection(target);
173167

174168
if (!hasInlineValue && addIfMissing) {
175169
const style = buildLayoutCSS(value);
176170
const elements = [target];
177171
this._styler.applyStyleToElements(style, elements);
178172
}
173+
174+
return value.trim();
179175
}
180176

181-
return value.trim() || 'row';
177+
return 'row';
182178
}
183179

184180
/** Applies styles given via string pair or object map to the directive element */
@@ -237,11 +233,6 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
237233
return buffer;
238234
}
239235

240-
/** Fast validator for presence of attribute on the host element */
241-
protected hasKeyValue(key: string) {
242-
return this._mqActivation!.hasKeyValue(key);
243-
}
244-
245236
protected get hasInitialized() {
246237
return this._hasInitialized;
247238
}

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

+12-11
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {BreakPoint} from '../breakpoints/break-point';
1212
import {MockMatchMedia, MockMatchMediaProvider} from './mock/mock-match-media';
1313
import {BreakPointRegistry} from '../breakpoints/break-point-registry';
1414
import {MatchMedia} from './match-media';
15-
import {ObservableMedia, ObservableMediaProvider} from '../observable-media/observable-media';
15+
import {MediaObserver} from '../media-observer/media-observer';
1616

1717
describe('match-media', () => {
1818
let matchMedia: MockMatchMedia;
@@ -111,22 +111,23 @@ describe('match-media', () => {
111111
describe('match-media-observable', () => {
112112
let breakPoints: BreakPointRegistry;
113113
let matchMedia: MockMatchMedia;
114-
let mediaQuery$: ObservableMedia;
114+
let mediaObserver: MediaObserver;
115115

116116
beforeEach(() => {
117117
// Configure testbed to prepare services
118118
TestBed.configureTestingModule({
119-
providers: [MockMatchMediaProvider, ObservableMediaProvider]
119+
providers: [MockMatchMediaProvider]
120120
});
121121
});
122122

123123
// Single async inject to save references; which are used in all tests below
124124
beforeEach(async(inject(
125-
[ObservableMedia, MatchMedia, BreakPointRegistry],
126-
(_media$: ObservableMedia, _matchMedia: MockMatchMedia, _breakPoints: BreakPointRegistry) => {
125+
[MediaObserver, MatchMedia, BreakPointRegistry],
126+
(_mediaObserver: MediaObserver, _matchMedia: MockMatchMedia,
127+
_breakPoints: BreakPointRegistry) => {
127128
matchMedia = _matchMedia; // inject only to manually activate mediaQuery ranges
128129
breakPoints = _breakPoints;
129-
mediaQuery$ = _media$;
130+
mediaObserver = _mediaObserver;
130131

131132
// Quick register all breakpoint mediaQueries
132133
breakPoints.items.forEach((bp: BreakPoint) => {
@@ -141,7 +142,7 @@ describe('match-media-observable', () => {
141142
let current: MediaChange;
142143
let bp = breakPoints.findByAlias('md') !;
143144
matchMedia.activate(bp.mediaQuery);
144-
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
145+
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
145146
current = change;
146147
});
147148

@@ -154,7 +155,7 @@ describe('match-media-observable', () => {
154155

155156
it('can observe the initial, default activation for mediaQuery == "all". ', () => {
156157
let current: MediaChange;
157-
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
158+
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
158159
current = change;
159160
});
160161

@@ -168,7 +169,7 @@ describe('match-media-observable', () => {
168169
it('can observe custom mediaQuery ranges', () => {
169170
let current: MediaChange;
170171
let customQuery = 'screen and (min-width: 610px) and (max-width: 620px';
171-
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
172+
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
172173
current = change;
173174
});
174175

@@ -184,7 +185,7 @@ describe('match-media-observable', () => {
184185
it('can observe registered breakpoint activations', () => {
185186
let current: MediaChange;
186187
let bp = breakPoints.findByAlias('md') !;
187-
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
188+
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
188189
current = change;
189190
});
190191

@@ -205,7 +206,7 @@ describe('match-media-observable', () => {
205206
it('ignores mediaQuery de-activations', () => {
206207
let activationCount = 0;
207208
let deactivationCount = 0;
208-
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
209+
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
209210
if (change.matches) {
210211
++activationCount;
211212
} else {

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

+66-89
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,20 @@ import {MediaChange} from '../media-change';
2121
*/
2222
@Injectable({providedIn: 'root'})
2323
export class MatchMedia {
24-
protected _registry: Map<string, MediaQueryList>;
25-
protected _source: BehaviorSubject<MediaChange>;
26-
protected _observable$: Observable<MediaChange>;
24+
protected _registry = new Map<string, MediaQueryList>();
25+
protected _source = new BehaviorSubject<MediaChange>(new MediaChange(true));
26+
protected _observable$ = this._source.asObservable();
2727

2828
constructor(protected _zone: NgZone,
2929
@Inject(PLATFORM_ID) protected _platformId: Object,
3030
@Inject(DOCUMENT) protected _document: any) {
31-
this._registry = new Map<string, MediaQueryList>();
32-
this._source = new BehaviorSubject<MediaChange>(new MediaChange(true));
33-
this._observable$ = this._source.asObservable();
3431
}
3532

3633
/**
3734
* For the specified mediaQuery?
3835
*/
3936
isActive(mediaQuery: string): boolean {
40-
let mql = this._registry.get(mediaQuery);
37+
const mql = this._registry.get(mediaQuery);
4138
return !!mql ? mql.matches : false;
4239
}
4340

@@ -55,9 +52,7 @@ export class MatchMedia {
5552
}
5653

5754
return this._observable$.pipe(
58-
filter((change: MediaChange) => {
59-
return mediaQuery ? (change.mediaQuery === mediaQuery) : true;
60-
})
55+
filter(change => (mediaQuery ? (change.mediaQuery === mediaQuery) : true))
6156
);
6257
}
6358

@@ -66,111 +61,93 @@ export class MatchMedia {
6661
* mediaQuery. Each listener emits specific MediaChange data to observers
6762
*/
6863
registerQuery(mediaQuery: string | string[]) {
69-
let list = normalizeQuery(mediaQuery);
64+
const list = Array.isArray(mediaQuery) ? Array.from(new Set(mediaQuery)) : [mediaQuery];
7065

7166
if (list.length > 0) {
72-
this._prepareQueryCSS(list, this._document);
73-
74-
list.forEach(query => {
75-
let mql = this._registry.get(query);
76-
let onMQLEvent = (e: MediaQueryListEvent) => {
77-
this._zone.run(() => {
78-
let change = new MediaChange(e.matches, query);
79-
this._source.next(change);
80-
});
81-
};
82-
83-
if (!mql) {
84-
mql = this._buildMQL(query);
85-
mql.addListener(onMQLEvent);
86-
this._registry.set(query, mql);
87-
}
88-
89-
if (mql.matches) {
90-
onMQLEvent(mql as unknown as MediaQueryListEvent);
91-
}
92-
});
67+
buildQueryCss(list, this._document);
9368
}
69+
70+
list.forEach(query => {
71+
const onMQLEvent = (e: MediaQueryListEvent) => {
72+
this._zone.run(() => this._source.next(new MediaChange(e.matches, query)));
73+
};
74+
75+
let mql = this._registry.get(query);
76+
77+
if (!mql) {
78+
mql = this._buildMQL(query);
79+
mql.addListener(onMQLEvent);
80+
this._registry.set(query, mql);
81+
}
82+
83+
if (mql.matches) {
84+
onMQLEvent(mql as unknown as MediaQueryListEvent);
85+
}
86+
});
9487
}
9588

9689
/**
9790
* Call window.matchMedia() to build a MediaQueryList; which
9891
* supports 0..n listeners for activation/deactivation
9992
*/
10093
protected _buildMQL(query: string): MediaQueryList {
101-
let canListen = isPlatformBrowser(this._platformId) &&
102-
!!(<any>window).matchMedia('all').addListener;
103-
104-
return canListen ? (<any>window).matchMedia(query) : {
105-
matches: query === 'all' || query === '',
106-
media: query,
107-
addListener: () => {
108-
},
109-
removeListener: () => {
110-
}
111-
} as unknown as MediaQueryList;
94+
return constructMql(query, isPlatformBrowser(this._platformId));
11295
}
96+
}
11397

114-
/**
115-
* For Webkit engines that only trigger the MediaQueryList Listener
116-
* when there is at least one CSS selector for the respective media query.
117-
*
118-
* @param mediaQueries
119-
* @param _document
120-
*/
121-
protected _prepareQueryCSS(mediaQueries: string[], _document: Document) {
122-
const list: string[] = mediaQueries.filter(it => !ALL_STYLES[it]);
123-
if (list.length > 0) {
124-
const query = list.join(', ');
98+
/**
99+
* Private global registry for all dynamically-created, injected style tags
100+
* @see prepare(query)
101+
*/
102+
const ALL_STYLES: {[key: string]: any} = {};
103+
104+
/**
105+
* For Webkit engines that only trigger the MediaQueryList Listener
106+
* when there is at least one CSS selector for the respective media query.
107+
*
108+
* @param mediaQueries
109+
* @param _document
110+
*/
111+
function buildQueryCss(mediaQueries: string[], _document: Document) {
112+
const list = mediaQueries.filter(it => !ALL_STYLES[it]);
113+
if (list.length > 0) {
114+
const query = list.join(', ');
125115

126-
try {
127-
let styleEl = _document.createElement('style');
116+
try {
117+
const styleEl = _document.createElement('style');
128118

129-
styleEl.setAttribute('type', 'text/css');
130-
if (!(styleEl as any).styleSheet) {
131-
let cssText = `
119+
styleEl.setAttribute('type', 'text/css');
120+
if (!(styleEl as any).styleSheet) {
121+
const cssText = `
132122
/*
133123
@angular/flex-layout - workaround for possible browser quirk with mediaQuery listeners
134124
see http://bit.ly/2sd4HMP
135125
*/
136126
@media ${query} {.fx-query-test{ }}
137127
` ;
138-
styleEl.appendChild(_document.createTextNode(cssText));
139-
}
128+
styleEl.appendChild(_document.createTextNode(cssText));
129+
}
140130

141-
_document.head!.appendChild(styleEl);
131+
_document.head!.appendChild(styleEl);
142132

143-
// Store in private global registry
144-
list.forEach(mq => ALL_STYLES[mq] = styleEl);
133+
// Store in private global registry
134+
list.forEach(mq => ALL_STYLES[mq] = styleEl);
145135

146-
} catch (e) {
147-
console.error(e);
148-
}
136+
} catch (e) {
137+
console.error(e);
149138
}
150139
}
151140
}
152141

153-
/**
154-
* Private global registry for all dynamically-created, injected style tags
155-
* @see prepare(query)
156-
*/
157-
const ALL_STYLES: {[key: string]: any} = {};
158-
159-
/**
160-
* Always convert to unique list of queries; for iteration in ::registerQuery()
161-
*/
162-
function normalizeQuery(mediaQuery: string | string[]): string[] {
163-
return (typeof mediaQuery === 'undefined') ? [] :
164-
(typeof mediaQuery === 'string') ? [mediaQuery] : unique(mediaQuery as string[]);
165-
}
142+
function constructMql(query: string, isBrowser: boolean): MediaQueryList {
143+
const canListen = isBrowser && !!(<any>window).matchMedia('all').addListener;
166144

167-
/**
168-
* Filter duplicate mediaQueries in the list
169-
*/
170-
function unique(list: string[]): string[] {
171-
let seen: {[key: string]: boolean} = {};
172-
return list.filter(item => {
173-
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
174-
});
145+
return canListen ? (<any>window).matchMedia(query) : {
146+
matches: query === 'all' || query === '',
147+
media: query,
148+
addListener: () => {
149+
},
150+
removeListener: () => {
151+
}
152+
} as unknown as MediaQueryList;
175153
}
176-

0 commit comments

Comments
 (0)