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

refactor: migrate ObservableMedia to MediaObserver #892

Merged
merged 1 commit into from
Nov 30, 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
4 changes: 2 additions & 2 deletions src/lib/core/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The `core` entrypoint contains all of the common utilities to build Layout
components. Its primary exports are the `MediaQuery` utilities (`MatchMedia`,
`ObservableMedia`) and the module that encapsulates the imports of these
`MediaObserver`) and the module that encapsulates the imports of these
providers, the `CoreModule`, and the base directive for layout
components, `BaseDirective`. These utilies can be imported separately
from the root module to take advantage of tree shaking.
Expand All @@ -22,4 +22,4 @@ export class AppModule {}
import {BaseDirective} from '@angular/flex-layout/core';

export class NewLayoutDirective extends BaseDirective {}
```
```
23 changes: 7 additions & 16 deletions src/lib/core/base/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ import {StyleBuilder} from '../style-builder/style-builder';

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

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

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

Expand All @@ -165,20 +162,19 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
* And optionally add the flow value to element's inline style.
*/
protected _getFlexFlowDirection(target: HTMLElement, addIfMissing = false): string {
let value = 'row';
let hasInlineValue = '';

if (target) {
[value, hasInlineValue] = this._styler.getFlowDirection(target);
let [value, hasInlineValue] = this._styler.getFlowDirection(target);

if (!hasInlineValue && addIfMissing) {
const style = buildLayoutCSS(value);
const elements = [target];
this._styler.applyStyleToElements(style, elements);
}

return value.trim();
}

return value.trim() || 'row';
return 'row';
}

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

/** Fast validator for presence of attribute on the host element */
protected hasKeyValue(key: string) {
return this._mqActivation!.hasKeyValue(key);
}

protected get hasInitialized() {
return this._hasInitialized;
}
Expand Down
23 changes: 12 additions & 11 deletions src/lib/core/match-media/match-media.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {BreakPoint} from '../breakpoints/break-point';
import {MockMatchMedia, MockMatchMediaProvider} from './mock/mock-match-media';
import {BreakPointRegistry} from '../breakpoints/break-point-registry';
import {MatchMedia} from './match-media';
import {ObservableMedia, ObservableMediaProvider} from '../observable-media/observable-media';
import {MediaObserver} from '../media-observer/media-observer';

describe('match-media', () => {
let matchMedia: MockMatchMedia;
Expand Down Expand Up @@ -111,22 +111,23 @@ describe('match-media', () => {
describe('match-media-observable', () => {
let breakPoints: BreakPointRegistry;
let matchMedia: MockMatchMedia;
let mediaQuery$: ObservableMedia;
let mediaObserver: MediaObserver;

beforeEach(() => {
// Configure testbed to prepare services
TestBed.configureTestingModule({
providers: [MockMatchMediaProvider, ObservableMediaProvider]
providers: [MockMatchMediaProvider]
});
});

// Single async inject to save references; which are used in all tests below
beforeEach(async(inject(
[ObservableMedia, MatchMedia, BreakPointRegistry],
(_media$: ObservableMedia, _matchMedia: MockMatchMedia, _breakPoints: BreakPointRegistry) => {
[MediaObserver, MatchMedia, BreakPointRegistry],
(_mediaObserver: MediaObserver, _matchMedia: MockMatchMedia,
_breakPoints: BreakPointRegistry) => {
matchMedia = _matchMedia; // inject only to manually activate mediaQuery ranges
breakPoints = _breakPoints;
mediaQuery$ = _media$;
mediaObserver = _mediaObserver;

// Quick register all breakpoint mediaQueries
breakPoints.items.forEach((bp: BreakPoint) => {
Expand All @@ -141,7 +142,7 @@ describe('match-media-observable', () => {
let current: MediaChange;
let bp = breakPoints.findByAlias('md') !;
matchMedia.activate(bp.mediaQuery);
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
current = change;
});

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

it('can observe the initial, default activation for mediaQuery == "all". ', () => {
let current: MediaChange;
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
current = change;
});

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

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

Expand All @@ -205,7 +206,7 @@ describe('match-media-observable', () => {
it('ignores mediaQuery de-activations', () => {
let activationCount = 0;
let deactivationCount = 0;
let subscription = mediaQuery$.subscribe((change: MediaChange) => {
let subscription = mediaObserver.media$.subscribe((change: MediaChange) => {
if (change.matches) {
++activationCount;
} else {
Expand Down
155 changes: 66 additions & 89 deletions src/lib/core/match-media/match-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,20 @@ import {MediaChange} from '../media-change';
*/
@Injectable({providedIn: 'root'})
export class MatchMedia {
protected _registry: Map<string, MediaQueryList>;
protected _source: BehaviorSubject<MediaChange>;
protected _observable$: Observable<MediaChange>;
protected _registry = new Map<string, MediaQueryList>();
protected _source = new BehaviorSubject<MediaChange>(new MediaChange(true));
protected _observable$ = this._source.asObservable();

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

/**
* For the specified mediaQuery?
*/
isActive(mediaQuery: string): boolean {
let mql = this._registry.get(mediaQuery);
const mql = this._registry.get(mediaQuery);
return !!mql ? mql.matches : false;
}

Expand All @@ -55,9 +52,7 @@ export class MatchMedia {
}

return this._observable$.pipe(
filter((change: MediaChange) => {
return mediaQuery ? (change.mediaQuery === mediaQuery) : true;
})
filter(change => (mediaQuery ? (change.mediaQuery === mediaQuery) : true))
);
}

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

if (list.length > 0) {
this._prepareQueryCSS(list, this._document);

list.forEach(query => {
let mql = this._registry.get(query);
let onMQLEvent = (e: MediaQueryListEvent) => {
this._zone.run(() => {
let change = new MediaChange(e.matches, query);
this._source.next(change);
});
};

if (!mql) {
mql = this._buildMQL(query);
mql.addListener(onMQLEvent);
this._registry.set(query, mql);
}

if (mql.matches) {
onMQLEvent(mql as unknown as MediaQueryListEvent);
}
});
buildQueryCss(list, this._document);
}

list.forEach(query => {
const onMQLEvent = (e: MediaQueryListEvent) => {
this._zone.run(() => this._source.next(new MediaChange(e.matches, query)));
};

let mql = this._registry.get(query);

if (!mql) {
mql = this._buildMQL(query);
mql.addListener(onMQLEvent);
this._registry.set(query, mql);
}

if (mql.matches) {
onMQLEvent(mql as unknown as MediaQueryListEvent);
}
});
}

/**
* Call window.matchMedia() to build a MediaQueryList; which
* supports 0..n listeners for activation/deactivation
*/
protected _buildMQL(query: string): MediaQueryList {
let canListen = isPlatformBrowser(this._platformId) &&
!!(<any>window).matchMedia('all').addListener;

return canListen ? (<any>window).matchMedia(query) : {
matches: query === 'all' || query === '',
media: query,
addListener: () => {
},
removeListener: () => {
}
} as unknown as MediaQueryList;
return constructMql(query, isPlatformBrowser(this._platformId));
}
}

/**
* For Webkit engines that only trigger the MediaQueryList Listener
* when there is at least one CSS selector for the respective media query.
*
* @param mediaQueries
* @param _document
*/
protected _prepareQueryCSS(mediaQueries: string[], _document: Document) {
const list: string[] = mediaQueries.filter(it => !ALL_STYLES[it]);
if (list.length > 0) {
const query = list.join(', ');
/**
* Private global registry for all dynamically-created, injected style tags
* @see prepare(query)
*/
const ALL_STYLES: {[key: string]: any} = {};

/**
* For Webkit engines that only trigger the MediaQueryList Listener
* when there is at least one CSS selector for the respective media query.
*
* @param mediaQueries
* @param _document
*/
function buildQueryCss(mediaQueries: string[], _document: Document) {
const list = mediaQueries.filter(it => !ALL_STYLES[it]);
if (list.length > 0) {
const query = list.join(', ');

try {
let styleEl = _document.createElement('style');
try {
const styleEl = _document.createElement('style');

styleEl.setAttribute('type', 'text/css');
if (!(styleEl as any).styleSheet) {
let cssText = `
styleEl.setAttribute('type', 'text/css');
if (!(styleEl as any).styleSheet) {
const cssText = `
/*
@angular/flex-layout - workaround for possible browser quirk with mediaQuery listeners
see http://bit.ly/2sd4HMP
*/
@media ${query} {.fx-query-test{ }}
` ;
styleEl.appendChild(_document.createTextNode(cssText));
}
styleEl.appendChild(_document.createTextNode(cssText));
}

_document.head!.appendChild(styleEl);
_document.head!.appendChild(styleEl);

// Store in private global registry
list.forEach(mq => ALL_STYLES[mq] = styleEl);
// Store in private global registry
list.forEach(mq => ALL_STYLES[mq] = styleEl);

} catch (e) {
console.error(e);
}
} catch (e) {
console.error(e);
}
}
}

/**
* Private global registry for all dynamically-created, injected style tags
* @see prepare(query)
*/
const ALL_STYLES: {[key: string]: any} = {};

/**
* Always convert to unique list of queries; for iteration in ::registerQuery()
*/
function normalizeQuery(mediaQuery: string | string[]): string[] {
return (typeof mediaQuery === 'undefined') ? [] :
(typeof mediaQuery === 'string') ? [mediaQuery] : unique(mediaQuery as string[]);
}
function constructMql(query: string, isBrowser: boolean): MediaQueryList {
const canListen = isBrowser && !!(<any>window).matchMedia('all').addListener;

/**
* Filter duplicate mediaQueries in the list
*/
function unique(list: string[]): string[] {
let seen: {[key: string]: boolean} = {};
return list.filter(item => {
return seen.hasOwnProperty(item) ? false : (seen[item] = true);
});
return canListen ? (<any>window).matchMedia(query) : {
matches: query === 'all' || query === '',
media: query,
addListener: () => {
},
removeListener: () => {
}
} as unknown as MediaQueryList;
}

Loading