Skip to content

Commit

Permalink
feat(core): add centralized media marshal service (#900)
Browse files Browse the repository at this point in the history
### Summary
This design change marks a number of departures from the current Angular Layout configuration:

1. A number of APIs are deprecated in favor of a more polished, integrated API
2. A new semantics for creating custom breakpoint directives is introduced
3. A number of bugs caught along the way and design changes missed in past PRs are rectified

>  **PLEASE NOTE**: There will be **no end-user API changes** as a result of this PR. 

Unless custom breakpoints are configured, devs should see no change in how the library usage. These changes will deliver significant size & performance improvements.

### New APIs

* `MediaMarshaller`
	A centralized data store for elements, breakpoints, and key-value pairs. The `MediaMarshaller` responds to mediaQuery changes and triggers appropriate Layout directives. This class also introduces a way to track changes for arbitrary elements and directive types
* `BaseDirective2`
	A new directive with stripped-down functionality from the old `BaseDirective` that is designed to work in symbiosis with the `MediaMarshaller`

### Custom Breakpoints

The custom breakpoints story has changed significantly. Instead of extending directives that contain the default breakpoints and writing lengthy constructors, the process has been pared down to the following (i.e. for `fxFlexOffset`):

```ts
const inputs = ['fxFlexOffset.xss'];
const selector = `[fxFlexOffset.xss]`;

@directive({selector, inputs})
export class XssFlexOffsetDirective extends FlexOffsetDirective {
	protected inputs = inputs;
}
```

Never again will a change in the base directive constructor require an entire rewrite of custom breakpoint directives. And registering a new directive no longer brings collisions and double-registrations competing with the default directives. Everything is separated and improved.

### Features

* All of the Grid and extended Flex directives have been updated to the new standards; namely, they have been refactored to the new API with `StyleBuilder`s. The notable exception is `show-hide`, which uses a `StyleBuilder`, but does not have a cache for the results. 
> FxShowHide does not use a StyleBuilder cache as to avoid complexity for cache-shifting based on the host `display` property. If needed, an internal cache can be easily added later.

### Bug Fixes

* A number of APIs had the default values calculated in the directives instead of the `StyleBuilder`, meaning that it would be much harder to override by end-users. This has been fixed
* An issue where `fxLayoutGap` was canceling out `fxFlexOffset` has been corrected
* The `ObservableMediaProvider` has been reintroduced to allow users to continue using `ObservableMedia` while it's deprecated 

### Deprecated APIs

* `MediaMonitor` (use `MediaMarshaller`)
* `ResponsiveActivation`
* `BreakpointX`
* `KeyOptions`
* `negativeOf`
* `BaseDirective` (use `BaseDirective2`)
* `BaseAdapter`

### Build Improvements

The minified size of the Angular Layout library has decreased *~27%* from **132KB** to **97KB**, a total savings of 35KB.  After the deprecated APIs are deleted (Beta.21), the build size will be reduced by another 15KB to 82KB, representing a total decrease of *~38%*.

### Performance Improvements

This commit should also bring about performance improvements as a result of fewer RxJS subscriptions, memoized style lookups, and other API processing. 

### Stabilization 

It is our hope that along with the added `StyleBuilder` functionality, and migration away from `ObservableMedia`, this represents the end-stage towards stability for Angular Layout.

Closes #903 
Fixes #692
  • Loading branch information
CaerusKaru authored Dec 14, 2018
1 parent 21151b0 commit cd05cb4
Show file tree
Hide file tree
Showing 54 changed files with 2,386 additions and 2,742 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Directive, Optional, Self} from '@angular/core';
import {FlexDirective} from '@angular/flex-layout';
import {DefaultFlexDirective} from '@angular/flex-layout';

@Directive({
selector: '[ngxSplitArea]',
Expand All @@ -8,5 +8,5 @@ import {FlexDirective} from '@angular/flex-layout';
}
})
export class SplitAreaDirective {
constructor(@Optional() @Self() public flex: FlexDirective) {}
constructor(@Optional() @Self() public flex: DefaultFlexDirective) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class SplitDirective implements AfterContentInit, OnDestroy {
const currentValue = flex.activatedValue;

// Update Flex-Layout value to build/inject new flexbox CSS
flex.activatedValue = this.calculateSize(currentValue, delta);
flex.activatedValue = `${this.calculateSize(currentValue, delta)}`;
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/apps/universal-app/src/app/split/split-area.directive.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Directive, Optional, Self} from '@angular/core';
import {FlexDirective} from '@angular/flex-layout';
import {DefaultFlexDirective} from '@angular/flex-layout';

@Directive({
selector: '[ngxSplitArea]',
Expand All @@ -8,5 +8,5 @@ import {FlexDirective} from '@angular/flex-layout';
}
})
export class SplitAreaDirective {
constructor(@Optional() @Self() public flex: FlexDirective) { }
constructor(@Optional() @Self() public flex: DefaultFlexDirective) { }
}
2 changes: 1 addition & 1 deletion src/apps/universal-app/src/app/split/split.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class SplitDirective implements AfterContentInit, OnDestroy {
const currentValue = flex.activatedValue;

// Update Flex-Layout value to build/inject new flexbox CSS
flex.activatedValue = this.calculateSize(currentValue, delta);
flex.activatedValue = `${this.calculateSize(currentValue, delta)}`;
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/core/add-alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {extendObject} from '../utils/object-extend';
* For the specified MediaChange, make sure it contains the breakpoint alias
* and suffix (if available).
*/
export function mergeAlias(dest: MediaChange, source: BreakPoint | null) {
export function mergeAlias(dest: MediaChange, source: BreakPoint | null): MediaChange {
return extendObject(dest, source ? {
mqAlias: source.alias,
suffix: source.suffix
Expand Down
2 changes: 2 additions & 0 deletions src/lib/core/base/base-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {StyleUtils} from '../style-utils/style-utils';
/**
* Adapter to the BaseDirective abstract class so it can be used via composition.
* @see BaseDirective
* @deprecated
* @deletion-target v7.0.0-beta.21
*/
export class BaseDirectiveAdapter extends BaseDirective {

Expand Down
6 changes: 5 additions & 1 deletion src/lib/core/base/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ import {MediaMonitor} from '../media-monitor/media-monitor';
import {MediaQuerySubscriber} from '../media-change';
import {StyleBuilder} from '../style-builder/style-builder';

/** Abstract base class for the Layout API styling directives. */
/**
* Abstract base class for the Layout API styling directives.
* @deprecated
* @deletion-target v7.0.0-beta.21
*/
export abstract class BaseDirective implements OnDestroy, OnChanges {

/**
Expand Down
122 changes: 122 additions & 0 deletions src/lib/core/base/base2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ElementRef, OnChanges, OnDestroy, SimpleChanges} from '@angular/core';
import {Subject} from 'rxjs';

import {StyleDefinition, StyleUtils} from '../style-utils/style-utils';
import {StyleBuilder} from '../style-builder/style-builder';
import {MediaMarshaller} from '../media-marshaller/media-marshaller';
import {buildLayoutCSS} from '../../utils/layout-validator';

export abstract class BaseDirective2 implements OnChanges, OnDestroy {

protected DIRECTIVE_KEY = '';
protected inputs: string[] = [];
protected destroySubject: Subject<void> = new Subject();

/** Access to host element's parent DOM node */
protected get parentElement(): HTMLElement | null {
return this.elementRef.nativeElement.parentElement;
}

/** Access to the HTMLElement for the directive */
protected get nativeElement(): HTMLElement {
return this.elementRef.nativeElement;
}

/** Access to the activated value for the directive */
get activatedValue(): string {
return this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY);
}
set activatedValue(value: string) {
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, value,
this.marshal.activatedBreakpoint);
}

/** Cache map for style computation */
protected styleCache: Map<string, StyleDefinition> = new Map();

protected constructor(protected elementRef: ElementRef,
protected styleBuilder: StyleBuilder,
protected styler: StyleUtils,
protected marshal: MediaMarshaller) {
}

/** For @Input changes */
ngOnChanges(changes: SimpleChanges) {
Object.keys(changes).forEach(key => {
if (this.inputs.indexOf(key) !== -1) {
const bp = key.split('.')[1] || '';
const val = changes[key].currentValue;
this.setValue(val, bp);
}
});
}

ngOnDestroy(): void {
this.destroySubject.next();
this.destroySubject.complete();
this.marshal.releaseElement(this.nativeElement);
}

/** Add styles to the element using predefined style builder */
protected addStyles(input: string, parent?: Object) {
const builder = this.styleBuilder;
const useCache = builder.shouldCache;

let genStyles: StyleDefinition | undefined = this.styleCache.get(input);

if (!genStyles || !useCache) {
genStyles = builder.buildStyles(input, parent);
if (useCache) {
this.styleCache.set(input, genStyles);
}
}

this.applyStyleToElement(genStyles);
builder.sideEffect(input, genStyles, parent);
}

protected triggerUpdate() {
const val = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY);
this.marshal.updateElement(this.nativeElement, this.DIRECTIVE_KEY, val);
}

/**
* Determine the DOM element's Flexbox flow (flex-direction).
*
* Check inline style first then check computed (stylesheet) style.
* And optionally add the flow value to element's inline style.
*/
protected getFlexFlowDirection(target: HTMLElement, addIfMissing = false): string {
if (target) {
const [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 'row';
}

/** Applies styles given via string pair or object map to the directive element */
protected applyStyleToElement(style: StyleDefinition,
value?: string | number,
element: HTMLElement = this.nativeElement) {
this.styler.applyStyleToElement(element, style, value);
}

protected setValue(val: any, bp: string): void {
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, val, bp);
}
}
1 change: 1 addition & 0 deletions src/lib/core/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export * from './base';
export * from './base-adapter';
export * from './base2';
2 changes: 2 additions & 0 deletions src/lib/core/breakpoints/break-point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export interface BreakPoint {
alias: string;
suffix?: string;
overlapping?: boolean;
// The priority of the individual breakpoint when overlapping another breakpoint
priority?: number;
}
6 changes: 6 additions & 0 deletions src/lib/core/breakpoints/breakpoint-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,9 @@ export function mergeByAlias(defaults: BreakPoint[], custom: BreakPoint[] = []):
return validateSuffixes(Object.keys(dict).map(k => dict[k]));
}

/** HOF to sort the breakpoints by priority */
export function prioritySort(a: BreakPoint, b: BreakPoint): number {
const priorityA = a.priority || 0;
const priorityB = b.priority || 0;
return priorityB - priorityA;
}
39 changes: 26 additions & 13 deletions src/lib/core/breakpoints/data/break-points.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,76 @@ export const RESPONSIVE_ALIASES = [
export const DEFAULT_BREAKPOINTS: BreakPoint[] = [
{
alias: 'xs',
mediaQuery: '(min-width: 0px) and (max-width: 599px)'
mediaQuery: '(min-width: 0px) and (max-width: 599px)',
priority: 100,
},
{
alias: 'gt-xs',
overlapping: true,
mediaQuery: '(min-width: 600px)'
mediaQuery: '(min-width: 600px)',
priority: 7,
},
{
alias: 'lt-sm',
overlapping: true,
mediaQuery: '(max-width: 599px)'
mediaQuery: '(max-width: 599px)',
priority: 10,
},
{
alias: 'sm',
mediaQuery: '(min-width: 600px) and (max-width: 959px)'
mediaQuery: '(min-width: 600px) and (max-width: 959px)',
priority: 100,
},
{
alias: 'gt-sm',
overlapping: true,
mediaQuery: '(min-width: 960px)'
mediaQuery: '(min-width: 960px)',
priority: 8,
},
{
alias: 'lt-md',
overlapping: true,
mediaQuery: '(max-width: 959px)'
mediaQuery: '(max-width: 959px)',
priority: 9,
},
{
alias: 'md',
mediaQuery: '(min-width: 960px) and (max-width: 1279px)'
mediaQuery: '(min-width: 960px) and (max-width: 1279px)',
priority: 100,
},
{
alias: 'gt-md',
overlapping: true,
mediaQuery: '(min-width: 1280px)'
mediaQuery: '(min-width: 1280px)',
priority: 9,
},
{
alias: 'lt-lg',
overlapping: true,
mediaQuery: '(max-width: 1279px)'
mediaQuery: '(max-width: 1279px)',
priority: 8,
},
{
alias: 'lg',
mediaQuery: '(min-width: 1280px) and (max-width: 1919px)'
mediaQuery: '(min-width: 1280px) and (max-width: 1919px)',
priority: 100,
},
{
alias: 'gt-lg',
overlapping: true,
mediaQuery: '(min-width: 1920px)'
mediaQuery: '(min-width: 1920px)',
priority: 10,
},
{
alias: 'lt-xl',
overlapping: true,
mediaQuery: '(max-width: 1919px)'
mediaQuery: '(max-width: 1919px)',
priority: 7,
},
{
alias: 'xl',
mediaQuery: '(min-width: 1920px) and (max-width: 5000px)'
mediaQuery: '(min-width: 1920px) and (max-width: 5000px)',
priority: 100,
}
];

1 change: 1 addition & 0 deletions src/lib/core/breakpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './data/orientation-break-points';
export * from './break-point';
export * from './break-point-registry';
export * from './break-points-token';
export {prioritySort} from './breakpoint-tools';
29 changes: 16 additions & 13 deletions src/lib/core/match-media/mock/mock-match-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,32 +79,32 @@ export class MockMatchMedia extends MatchMedia {
// Simulate activation of overlapping lt-<XXX> ranges
switch (alias) {
case 'lg' :
this._activateByAlias('lt-xl');
this._activateByAlias('lt-xl', true);
break;
case 'md' :
this._activateByAlias('lt-xl, lt-lg');
this._activateByAlias('lt-xl, lt-lg', true);
break;
case 'sm' :
this._activateByAlias('lt-xl, lt-lg, lt-md');
this._activateByAlias('lt-xl, lt-lg, lt-md', true);
break;
case 'xs' :
this._activateByAlias('lt-xl, lt-lg, lt-md, lt-sm');
this._activateByAlias('lt-xl, lt-lg, lt-md, lt-sm', true);
break;
}

// Simulate activate of overlapping gt-<xxxx> mediaQuery ranges
switch (alias) {
case 'xl' :
this._activateByAlias('gt-lg, gt-md, gt-sm, gt-xs');
this._activateByAlias('gt-lg, gt-md, gt-sm, gt-xs', true);
break;
case 'lg' :
this._activateByAlias('gt-md, gt-sm, gt-xs');
this._activateByAlias('gt-md, gt-sm, gt-xs', true);
break;
case 'md' :
this._activateByAlias('gt-sm, gt-xs');
this._activateByAlias('gt-sm, gt-xs', true);
break;
case 'sm' :
this._activateByAlias('gt-xs');
this._activateByAlias('gt-xs', true);
break;
}
}
Expand All @@ -115,21 +115,24 @@ export class MockMatchMedia extends MatchMedia {
/**
*
*/
private _activateByAlias(aliases: string) {
private _activateByAlias(aliases: string, useOverlaps = false) {
const activate = (alias: string) => {
const bp = this._breakpoints.findByAlias(alias);
this._activateByQuery(bp ? bp.mediaQuery : alias);
this._activateByQuery(bp ? bp.mediaQuery : alias, useOverlaps);
};
aliases.split(',').forEach(alias => activate(alias.trim()));
}

/**
*
*/
private _activateByQuery(mediaQuery: string) {
const mql = this._registry.get(mediaQuery)!;
private _activateByQuery(mediaQuery: string, useOverlaps = false) {
if (useOverlaps) {
this._registerMediaQuery(mediaQuery);
}
const mql = this._registry.get(mediaQuery);
const alreadyAdded = this._actives
.reduce((found, it) => (found || (mql && (it.media === mql.media))), false);
.reduce((found, it) => (found || (mql ? (it.media === mql.media) : false)), false);

if (mql && !alreadyAdded) {
this._actives.push(mql.activate());
Expand Down
Loading

0 comments on commit cd05cb4

Please sign in to comment.