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

Commit 092aa75

Browse files
committed
feat(core): add centralized media marshal service
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 notice significant size & performance improvements. * `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` The custom breakpoints story has changed significantly. Instead of extending directives that contain the default breakpoints and writing lengthy constructors, the process has been paired 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. * 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. * 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 * `MediaMonitor` (use `MediaMarshaller`) * `ResponsiveActivation` * `BreakpointX` * `KeyOptions` * `negativeOf` * `BaseDirective` (use `BaseDirective2`) * `BaseAdapter` The minified size of the Angular Layout library has decreased *~38%* from **132KB** to **82KB**, a total savings of 50KB. After the deprecated APIs are deleted (Beta.21), the build size will be reduced yet again. It should also bring about performance improvements as a result of fewer RxJS subscriptions, memoized style lookups, and other API processing. 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
1 parent 21151b0 commit 092aa75

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2380
-2742
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Directive, Optional, Self} from '@angular/core';
2-
import {FlexDirective} from '@angular/flex-layout';
2+
import {DefaultFlexDirective} from '@angular/flex-layout';
33

44
@Directive({
55
selector: '[ngxSplitArea]',
@@ -8,5 +8,5 @@ import {FlexDirective} from '@angular/flex-layout';
88
}
99
})
1010
export class SplitAreaDirective {
11-
constructor(@Optional() @Self() public flex: FlexDirective) {}
11+
constructor(@Optional() @Self() public flex: DefaultFlexDirective) {}
1212
}

src/apps/demo-app/src/app/github-issues/split/split.directive.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class SplitDirective implements AfterContentInit, OnDestroy {
5454
const currentValue = flex.activatedValue;
5555

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

Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Directive, Optional, Self} from '@angular/core';
2-
import {FlexDirective} from '@angular/flex-layout';
2+
import {DefaultFlexDirective} from '@angular/flex-layout';
33

44
@Directive({
55
selector: '[ngxSplitArea]',
@@ -8,5 +8,5 @@ import {FlexDirective} from '@angular/flex-layout';
88
}
99
})
1010
export class SplitAreaDirective {
11-
constructor(@Optional() @Self() public flex: FlexDirective) { }
11+
constructor(@Optional() @Self() public flex: DefaultFlexDirective) { }
1212
}

src/apps/universal-app/src/app/split/split.directive.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class SplitDirective implements AfterContentInit, OnDestroy {
6060
const currentValue = flex.activatedValue;
6161

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

src/lib/core/add-alias.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {extendObject} from '../utils/object-extend';
1313
* For the specified MediaChange, make sure it contains the breakpoint alias
1414
* and suffix (if available).
1515
*/
16-
export function mergeAlias(dest: MediaChange, source: BreakPoint | null) {
16+
export function mergeAlias(dest: MediaChange, source: BreakPoint | null): MediaChange {
1717
return extendObject(dest, source ? {
1818
mqAlias: source.alias,
1919
suffix: source.suffix

src/lib/core/base/base-adapter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {StyleUtils} from '../style-utils/style-utils';
1717
/**
1818
* Adapter to the BaseDirective abstract class so it can be used via composition.
1919
* @see BaseDirective
20+
* @deprecated
21+
* @deletion-target v7.0.0-beta.21
2022
*/
2123
export class BaseDirectiveAdapter extends BaseDirective {
2224

src/lib/core/base/base.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import {MediaMonitor} from '../media-monitor/media-monitor';
2323
import {MediaQuerySubscriber} from '../media-change';
2424
import {StyleBuilder} from '../style-builder/style-builder';
2525

26-
/** Abstract base class for the Layout API styling directives. */
26+
/**
27+
* Abstract base class for the Layout API styling directives.
28+
* @deprecated
29+
* @deletion-target v7.0.0-beta.21
30+
*/
2731
export abstract class BaseDirective implements OnDestroy, OnChanges {
2832

2933
/**

src/lib/core/base/base2.ts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {ElementRef, OnChanges, OnDestroy, SimpleChanges} from '@angular/core';
9+
import {Subject} from 'rxjs';
10+
11+
import {StyleDefinition, StyleUtils} from '../style-utils/style-utils';
12+
import {StyleBuilder} from '../style-builder/style-builder';
13+
import {MediaMarshaller} from '../media-marshaller/media-marshaller';
14+
import {buildLayoutCSS} from '../../utils/layout-validator';
15+
16+
export abstract class BaseDirective2 implements OnChanges, OnDestroy {
17+
18+
protected DIRECTIVE_KEY = '';
19+
protected inputs: string[] = [];
20+
protected destroySubject: Subject<void> = new Subject();
21+
22+
/** Access to host element's parent DOM node */
23+
protected get parentElement(): HTMLElement | null {
24+
return this.elementRef.nativeElement.parentElement;
25+
}
26+
27+
/** Access to the HTMLElement for the directive */
28+
protected get nativeElement(): HTMLElement {
29+
return this.elementRef.nativeElement;
30+
}
31+
32+
/** Access to the activated value for the directive */
33+
get activatedValue(): string {
34+
return this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY);
35+
}
36+
set activatedValue(value: string) {
37+
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, value,
38+
this.marshal.activatedBreakpoint);
39+
}
40+
41+
/** Cache map for style computation */
42+
protected styleCache: Map<string, StyleDefinition> = new Map();
43+
44+
protected constructor(protected elementRef: ElementRef,
45+
protected styleBuilder: StyleBuilder,
46+
protected styler: StyleUtils,
47+
protected marshal: MediaMarshaller) {
48+
}
49+
50+
/** For @Input changes */
51+
ngOnChanges(changes: SimpleChanges) {
52+
Object.keys(changes).forEach(key => {
53+
if (this.inputs.indexOf(key) !== -1) {
54+
const bp = key.split('.')[1] || '';
55+
const val = changes[key].currentValue;
56+
this.setValue(val, bp);
57+
}
58+
});
59+
}
60+
61+
ngOnDestroy(): void {
62+
this.destroySubject.next();
63+
this.destroySubject.complete();
64+
this.marshal.releaseElement(this.nativeElement);
65+
}
66+
67+
/** Add styles to the element using predefined style builder */
68+
protected addStyles(input: string, parent?: Object) {
69+
const builder = this.styleBuilder;
70+
const useCache = builder.shouldCache;
71+
72+
let genStyles: StyleDefinition | undefined = this.styleCache.get(input);
73+
74+
if (!genStyles || !useCache) {
75+
genStyles = builder.buildStyles(input, parent);
76+
if (useCache) {
77+
this.styleCache.set(input, genStyles);
78+
}
79+
}
80+
81+
this.applyStyleToElement(genStyles);
82+
builder.sideEffect(input, genStyles, parent);
83+
}
84+
85+
protected triggerUpdate() {
86+
const val = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY);
87+
this.marshal.updateElement(this.nativeElement, this.DIRECTIVE_KEY, val);
88+
}
89+
90+
/**
91+
* Determine the DOM element's Flexbox flow (flex-direction).
92+
*
93+
* Check inline style first then check computed (stylesheet) style.
94+
* And optionally add the flow value to element's inline style.
95+
*/
96+
protected getFlexFlowDirection(target: HTMLElement, addIfMissing = false): string {
97+
if (target) {
98+
const [value, hasInlineValue] = this.styler.getFlowDirection(target);
99+
100+
if (!hasInlineValue && addIfMissing) {
101+
const style = buildLayoutCSS(value);
102+
const elements = [target];
103+
this.styler.applyStyleToElements(style, elements);
104+
}
105+
106+
return value.trim();
107+
}
108+
109+
return 'row';
110+
}
111+
112+
/** Applies styles given via string pair or object map to the directive element */
113+
protected applyStyleToElement(style: StyleDefinition,
114+
value?: string | number,
115+
element: HTMLElement = this.nativeElement) {
116+
this.styler.applyStyleToElement(element, style, value);
117+
}
118+
119+
protected setValue(val: any, bp: string): void {
120+
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, val, bp);
121+
}
122+
}

src/lib/core/base/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88

99
export * from './base';
1010
export * from './base-adapter';
11+
export * from './base2';

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

+2
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ export interface BreakPoint {
1010
alias: string;
1111
suffix?: string;
1212
overlapping?: boolean;
13+
// The priority of the individual breakpoint when overlapping another breakpoint
14+
priority?: number;
1315
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,9 @@ export function mergeByAlias(defaults: BreakPoint[], custom: BreakPoint[] = []):
6464
return validateSuffixes(Object.keys(dict).map(k => dict[k]));
6565
}
6666

67+
/** HOF to sort the breakpoints by priority */
68+
export function prioritySort(a: BreakPoint, b: BreakPoint): number {
69+
const priorityA = a.priority || 0;
70+
const priorityB = b.priority || 0;
71+
return priorityB - priorityA;
72+
}

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

+26-13
Original file line numberDiff line numberDiff line change
@@ -14,63 +14,76 @@ export const RESPONSIVE_ALIASES = [
1414
export const DEFAULT_BREAKPOINTS: BreakPoint[] = [
1515
{
1616
alias: 'xs',
17-
mediaQuery: '(min-width: 0px) and (max-width: 599px)'
17+
mediaQuery: '(min-width: 0px) and (max-width: 599px)',
18+
priority: 100,
1819
},
1920
{
2021
alias: 'gt-xs',
2122
overlapping: true,
22-
mediaQuery: '(min-width: 600px)'
23+
mediaQuery: '(min-width: 600px)',
24+
priority: 7,
2325
},
2426
{
2527
alias: 'lt-sm',
2628
overlapping: true,
27-
mediaQuery: '(max-width: 599px)'
29+
mediaQuery: '(max-width: 599px)',
30+
priority: 10,
2831
},
2932
{
3033
alias: 'sm',
31-
mediaQuery: '(min-width: 600px) and (max-width: 959px)'
34+
mediaQuery: '(min-width: 600px) and (max-width: 959px)',
35+
priority: 100,
3236
},
3337
{
3438
alias: 'gt-sm',
3539
overlapping: true,
36-
mediaQuery: '(min-width: 960px)'
40+
mediaQuery: '(min-width: 960px)',
41+
priority: 8,
3742
},
3843
{
3944
alias: 'lt-md',
4045
overlapping: true,
41-
mediaQuery: '(max-width: 959px)'
46+
mediaQuery: '(max-width: 959px)',
47+
priority: 9,
4248
},
4349
{
4450
alias: 'md',
45-
mediaQuery: '(min-width: 960px) and (max-width: 1279px)'
51+
mediaQuery: '(min-width: 960px) and (max-width: 1279px)',
52+
priority: 100,
4653
},
4754
{
4855
alias: 'gt-md',
4956
overlapping: true,
50-
mediaQuery: '(min-width: 1280px)'
57+
mediaQuery: '(min-width: 1280px)',
58+
priority: 9,
5159
},
5260
{
5361
alias: 'lt-lg',
5462
overlapping: true,
55-
mediaQuery: '(max-width: 1279px)'
63+
mediaQuery: '(max-width: 1279px)',
64+
priority: 8,
5665
},
5766
{
5867
alias: 'lg',
59-
mediaQuery: '(min-width: 1280px) and (max-width: 1919px)'
68+
mediaQuery: '(min-width: 1280px) and (max-width: 1919px)',
69+
priority: 100,
6070
},
6171
{
6272
alias: 'gt-lg',
6373
overlapping: true,
64-
mediaQuery: '(min-width: 1920px)'
74+
mediaQuery: '(min-width: 1920px)',
75+
priority: 10,
6576
},
6677
{
6778
alias: 'lt-xl',
6879
overlapping: true,
69-
mediaQuery: '(max-width: 1919px)'
80+
mediaQuery: '(max-width: 1919px)',
81+
priority: 7,
7082
},
7183
{
7284
alias: 'xl',
73-
mediaQuery: '(min-width: 1920px) and (max-width: 5000px)'
85+
mediaQuery: '(min-width: 1920px) and (max-width: 5000px)',
86+
priority: 100,
7487
}
7588
];
7689

src/lib/core/breakpoints/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ export * from './data/orientation-break-points';
1212
export * from './break-point';
1313
export * from './break-point-registry';
1414
export * from './break-points-token';
15+
export {prioritySort} from './breakpoint-tools';

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

+16-13
Original file line numberDiff line numberDiff line change
@@ -79,32 +79,32 @@ export class MockMatchMedia extends MatchMedia {
7979
// Simulate activation of overlapping lt-<XXX> ranges
8080
switch (alias) {
8181
case 'lg' :
82-
this._activateByAlias('lt-xl');
82+
this._activateByAlias('lt-xl', true);
8383
break;
8484
case 'md' :
85-
this._activateByAlias('lt-xl, lt-lg');
85+
this._activateByAlias('lt-xl, lt-lg', true);
8686
break;
8787
case 'sm' :
88-
this._activateByAlias('lt-xl, lt-lg, lt-md');
88+
this._activateByAlias('lt-xl, lt-lg, lt-md', true);
8989
break;
9090
case 'xs' :
91-
this._activateByAlias('lt-xl, lt-lg, lt-md, lt-sm');
91+
this._activateByAlias('lt-xl, lt-lg, lt-md, lt-sm', true);
9292
break;
9393
}
9494

9595
// Simulate activate of overlapping gt-<xxxx> mediaQuery ranges
9696
switch (alias) {
9797
case 'xl' :
98-
this._activateByAlias('gt-lg, gt-md, gt-sm, gt-xs');
98+
this._activateByAlias('gt-lg, gt-md, gt-sm, gt-xs', true);
9999
break;
100100
case 'lg' :
101-
this._activateByAlias('gt-md, gt-sm, gt-xs');
101+
this._activateByAlias('gt-md, gt-sm, gt-xs', true);
102102
break;
103103
case 'md' :
104-
this._activateByAlias('gt-sm, gt-xs');
104+
this._activateByAlias('gt-sm, gt-xs', true);
105105
break;
106106
case 'sm' :
107-
this._activateByAlias('gt-xs');
107+
this._activateByAlias('gt-xs', true);
108108
break;
109109
}
110110
}
@@ -115,21 +115,24 @@ export class MockMatchMedia extends MatchMedia {
115115
/**
116116
*
117117
*/
118-
private _activateByAlias(aliases: string) {
118+
private _activateByAlias(aliases: string, useOverlaps = false) {
119119
const activate = (alias: string) => {
120120
const bp = this._breakpoints.findByAlias(alias);
121-
this._activateByQuery(bp ? bp.mediaQuery : alias);
121+
this._activateByQuery(bp ? bp.mediaQuery : alias, useOverlaps);
122122
};
123123
aliases.split(',').forEach(alias => activate(alias.trim()));
124124
}
125125

126126
/**
127127
*
128128
*/
129-
private _activateByQuery(mediaQuery: string) {
130-
const mql = this._registry.get(mediaQuery)!;
129+
private _activateByQuery(mediaQuery: string, useOverlaps = false) {
130+
if (useOverlaps) {
131+
this._registerMediaQuery(mediaQuery);
132+
}
133+
const mql = this._registry.get(mediaQuery);
131134
const alreadyAdded = this._actives
132-
.reduce((found, it) => (found || (mql && (it.media === mql.media))), false);
135+
.reduce((found, it) => (found || (mql ? (it.media === mql.media) : false)), false);
133136

134137
if (mql && !alreadyAdded) {
135138
this._actives.push(mql.activate());

0 commit comments

Comments
 (0)