From 6db5c650044d9933e171f3b9f42c5cf7db51415c Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Mon, 26 Nov 2018 17:30:07 -0600 Subject: [PATCH] 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 --- .../split/split-area.directive.ts | 4 +- .../github-issues/split/split.directive.ts | 2 +- .../src/app/split/split-area.directive.ts | 4 +- .../src/app/split/split.directive.ts | 2 +- src/lib/core/add-alias.ts | 2 +- src/lib/core/base/base-adapter.ts | 2 + src/lib/core/base/base.ts | 6 +- src/lib/core/base/base2.ts | 122 +++++ src/lib/core/base/index.ts | 1 + src/lib/core/breakpoints/break-point.ts | 2 + src/lib/core/breakpoints/breakpoint-tools.ts | 6 + src/lib/core/breakpoints/data/break-points.ts | 39 +- src/lib/core/breakpoints/index.ts | 1 + .../core/match-media/mock/mock-match-media.ts | 29 +- .../media-marshaller/media-marshaller.spec.ts | 130 ++++++ .../core/media-marshaller/media-marshaller.ts | 227 +++++++++ src/lib/core/media-monitor/media-monitor.ts | 2 + src/lib/core/module.ts | 3 +- src/lib/core/public-api.ts | 1 + .../responsive-activation.ts | 10 + src/lib/extended/class/class.spec.ts | 4 +- src/lib/extended/class/class.ts | 161 ++----- src/lib/extended/img-src/img-src.ts | 170 +++---- src/lib/extended/module.ts | 16 +- src/lib/extended/show-hide/hide.spec.ts | 4 +- src/lib/extended/show-hide/show-hide.ts | 265 +++++------ src/lib/extended/show-hide/show.spec.ts | 55 +-- src/lib/extended/style/style.spec.ts | 6 +- src/lib/extended/style/style.ts | 200 +++----- src/lib/flex/flex-align/flex-align.ts | 123 ++--- src/lib/flex/flex-fill/flex-fill.ts | 23 +- src/lib/flex/flex-offset/flex-offset.spec.ts | 1 + src/lib/flex/flex-offset/flex-offset.ts | 181 ++----- src/lib/flex/flex-order/flex-order.ts | 120 ++--- src/lib/flex/flex/flex.spec.ts | 88 +++- src/lib/flex/flex/flex.ts | 207 ++++---- src/lib/flex/layout-align/layout-align.ts | 164 +++---- src/lib/flex/layout-gap/layout-gap.ts | 237 +++++----- src/lib/flex/layout/layout.ts | 154 ++---- src/lib/flex/module.ts | 28 +- src/lib/grid/align-columns/align-columns.ts | 252 +++++----- src/lib/grid/align-rows/align-rows.ts | 215 ++++----- src/lib/grid/area/area.ts | 128 ++--- src/lib/grid/areas/areas.ts | 142 +++--- src/lib/grid/auto/auto.ts | 154 +++--- src/lib/grid/column/column.ts | 128 ++--- src/lib/grid/columns/columns.ts | 164 +++---- src/lib/grid/gap/gap.ts | 149 +++--- src/lib/grid/grid-align/grid-align.ts | 210 ++++----- src/lib/grid/module.ts | 44 +- src/lib/grid/row/row.ts | 128 ++--- src/lib/grid/rows/rows.ts | 164 +++---- src/lib/server/server-provider.ts | 6 +- yarn.lock | 442 ++++++++---------- 54 files changed, 2386 insertions(+), 2742 deletions(-) create mode 100644 src/lib/core/base/base2.ts create mode 100644 src/lib/core/media-marshaller/media-marshaller.spec.ts create mode 100644 src/lib/core/media-marshaller/media-marshaller.ts diff --git a/src/apps/demo-app/src/app/github-issues/split/split-area.directive.ts b/src/apps/demo-app/src/app/github-issues/split/split-area.directive.ts index 5e9610e3e..9d12464d2 100644 --- a/src/apps/demo-app/src/app/github-issues/split/split-area.directive.ts +++ b/src/apps/demo-app/src/app/github-issues/split/split-area.directive.ts @@ -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]', @@ -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) {} } diff --git a/src/apps/demo-app/src/app/github-issues/split/split.directive.ts b/src/apps/demo-app/src/app/github-issues/split/split.directive.ts index 7b2145923..836933ce9 100644 --- a/src/apps/demo-app/src/app/github-issues/split/split.directive.ts +++ b/src/apps/demo-app/src/app/github-issues/split/split.directive.ts @@ -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)}`; }); } diff --git a/src/apps/universal-app/src/app/split/split-area.directive.ts b/src/apps/universal-app/src/app/split/split-area.directive.ts index 7a4e015b1..9cbb034db 100644 --- a/src/apps/universal-app/src/app/split/split-area.directive.ts +++ b/src/apps/universal-app/src/app/split/split-area.directive.ts @@ -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]', @@ -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) { } } diff --git a/src/apps/universal-app/src/app/split/split.directive.ts b/src/apps/universal-app/src/app/split/split.directive.ts index e01f70854..55d492995 100644 --- a/src/apps/universal-app/src/app/split/split.directive.ts +++ b/src/apps/universal-app/src/app/split/split.directive.ts @@ -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)}`; }); } diff --git a/src/lib/core/add-alias.ts b/src/lib/core/add-alias.ts index ae9206fa0..e419198e7 100644 --- a/src/lib/core/add-alias.ts +++ b/src/lib/core/add-alias.ts @@ -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 diff --git a/src/lib/core/base/base-adapter.ts b/src/lib/core/base/base-adapter.ts index 5451b7fa9..cf3d16d98 100644 --- a/src/lib/core/base/base-adapter.ts +++ b/src/lib/core/base/base-adapter.ts @@ -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 { diff --git a/src/lib/core/base/base.ts b/src/lib/core/base/base.ts index 14249170b..45cd47d79 100644 --- a/src/lib/core/base/base.ts +++ b/src/lib/core/base/base.ts @@ -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 { /** diff --git a/src/lib/core/base/base2.ts b/src/lib/core/base/base2.ts new file mode 100644 index 000000000..309c148d7 --- /dev/null +++ b/src/lib/core/base/base2.ts @@ -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 = 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 = 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); + } +} diff --git a/src/lib/core/base/index.ts b/src/lib/core/base/index.ts index 855382060..dd0a27eb6 100644 --- a/src/lib/core/base/index.ts +++ b/src/lib/core/base/index.ts @@ -8,3 +8,4 @@ export * from './base'; export * from './base-adapter'; +export * from './base2'; diff --git a/src/lib/core/breakpoints/break-point.ts b/src/lib/core/breakpoints/break-point.ts index 3e4cc9bb1..e3d07dd71 100644 --- a/src/lib/core/breakpoints/break-point.ts +++ b/src/lib/core/breakpoints/break-point.ts @@ -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; } diff --git a/src/lib/core/breakpoints/breakpoint-tools.ts b/src/lib/core/breakpoints/breakpoint-tools.ts index 6a9a9aa5c..73df2fb30 100644 --- a/src/lib/core/breakpoints/breakpoint-tools.ts +++ b/src/lib/core/breakpoints/breakpoint-tools.ts @@ -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; +} diff --git a/src/lib/core/breakpoints/data/break-points.ts b/src/lib/core/breakpoints/data/break-points.ts index cc149df33..c83f6f4a5 100644 --- a/src/lib/core/breakpoints/data/break-points.ts +++ b/src/lib/core/breakpoints/data/break-points.ts @@ -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, } ]; diff --git a/src/lib/core/breakpoints/index.ts b/src/lib/core/breakpoints/index.ts index 18c379038..6bf81c5b0 100644 --- a/src/lib/core/breakpoints/index.ts +++ b/src/lib/core/breakpoints/index.ts @@ -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'; diff --git a/src/lib/core/match-media/mock/mock-match-media.ts b/src/lib/core/match-media/mock/mock-match-media.ts index 838fc789e..8b61dca9d 100644 --- a/src/lib/core/match-media/mock/mock-match-media.ts +++ b/src/lib/core/match-media/mock/mock-match-media.ts @@ -79,32 +79,32 @@ export class MockMatchMedia extends MatchMedia { // Simulate activation of overlapping lt- 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- 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; } } @@ -115,10 +115,10 @@ 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())); } @@ -126,10 +126,13 @@ export class MockMatchMedia extends MatchMedia { /** * */ - 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()); diff --git a/src/lib/core/media-marshaller/media-marshaller.spec.ts b/src/lib/core/media-marshaller/media-marshaller.spec.ts new file mode 100644 index 000000000..5fac1d60a --- /dev/null +++ b/src/lib/core/media-marshaller/media-marshaller.spec.ts @@ -0,0 +1,130 @@ +/** + * @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 {async, fakeAsync, inject, TestBed} from '@angular/core/testing'; +import {Subject} from 'rxjs'; + +import {MockMatchMedia, MockMatchMediaProvider} from '../match-media/mock/mock-match-media'; +import {MatchMedia} from '../match-media/match-media'; +import {MediaMarshaller} from './media-marshaller'; + +describe('media-marshaller', () => { + let matchMedia: MockMatchMedia; + let mediaMarshaller: MediaMarshaller; + + beforeEach(() => { + // Configure testbed to prepare services + TestBed.configureTestingModule({ + providers: [MockMatchMediaProvider] + }); + spyOn(MediaMarshaller.prototype, 'activate').and.callThrough(); + }); + + // Single async inject to save references; which are used in all tests below + beforeEach(async(inject([MatchMedia, MediaMarshaller], + (service: MockMatchMedia, marshal: MediaMarshaller) => { + matchMedia = service; // inject only to manually activate mediaQuery ranges + mediaMarshaller = marshal; + }))); + afterEach(() => { + matchMedia.clearAll(); + }); + + it('activates when match-media activates', () => { + matchMedia.activate('xs'); + expect(mediaMarshaller.activate).toHaveBeenCalled(); + }); + + it('should set correct activated breakpoint', () => { + matchMedia.activate('lg'); + expect(mediaMarshaller.activatedBreakpoint).toBe('lg'); + + matchMedia.activate('gt-md'); + expect(mediaMarshaller.activatedBreakpoint).toBe('gt-md'); + }); + + it('should init', () => { + let triggered = false; + const builder = () => { + triggered = true; + }; + mediaMarshaller.init(fakeElement, fakeKey, builder); + mediaMarshaller.setValue(fakeElement, fakeKey, 0, 'xs'); + triggered = false; + matchMedia.activate('xs'); + expect(triggered).toBeTruthy(); + }); + + it('should init with observables', () => { + let triggered = false; + const subject: Subject = new Subject(); + const obs = subject.asObservable(); + const builder = () => { + triggered = true; + }; + mediaMarshaller.init(fakeElement, fakeKey, builder, [obs]); + subject.next(); + expect(triggered).toBeTruthy(); + }); + + it('should updateStyles', () => { + let triggered = false; + const builder = () => { + triggered = true; + }; + mediaMarshaller.init(fakeElement, fakeKey, builder); + mediaMarshaller.setValue(fakeElement, fakeKey, 0, ''); + triggered = false; + mediaMarshaller.updateStyles(); + expect(triggered).toBeTruthy(); + }); + + it('should updateElement', () => { + let triggered = false; + const builder = () => { + triggered = true; + }; + mediaMarshaller.init(fakeElement, fakeKey, builder); + mediaMarshaller.updateElement(fakeElement, fakeKey, 0); + expect(triggered).toBeTruthy(); + }); + + it('should get the right value', () => { + const builder = () => {}; + mediaMarshaller.init(fakeElement, fakeKey, builder); + mediaMarshaller.setValue(fakeElement, fakeKey, 0, ''); + const value = mediaMarshaller.getValue(fakeElement, fakeKey); + expect(value).toEqual(0); + }); + + it('should track changes', fakeAsync(() => { + const builder = () => {}; + let triggered = false; + mediaMarshaller.init(fakeElement, fakeKey, builder); + mediaMarshaller.trackValue(fakeElement, fakeKey).subscribe(() => { + triggered = true; + }); + mediaMarshaller.setValue(fakeElement, fakeKey, 0, ''); + expect(triggered).toBeTruthy(); + })); + + it('should release', () => { + let triggered = false; + const subject: Subject = new Subject(); + const obs = subject.asObservable(); + const builder = () => { + triggered = true; + }; + mediaMarshaller.init(fakeElement, fakeKey, builder, [obs]); + mediaMarshaller.releaseElement(fakeElement); + subject.next(); + expect(triggered).toBeFalsy(); + }); +}); + +const fakeElement = {} as HTMLElement; +const fakeKey = 'FAKE_KEY'; diff --git a/src/lib/core/media-marshaller/media-marshaller.ts b/src/lib/core/media-marshaller/media-marshaller.ts new file mode 100644 index 000000000..644abe7f4 --- /dev/null +++ b/src/lib/core/media-marshaller/media-marshaller.ts @@ -0,0 +1,227 @@ +/** + * @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 {Injectable} from '@angular/core'; +import {merge, Observable, Subject, Subscription} from 'rxjs'; +import {filter} from 'rxjs/operators'; + +import {BreakPoint} from '../breakpoints/break-point'; +import {prioritySort} from '../breakpoints/breakpoint-tools'; +import {BreakPointRegistry} from '../breakpoints/break-point-registry'; +import {MatchMedia} from '../match-media/match-media'; +import {MediaChange} from '../media-change'; + +type Builder = Function; +type ValueMap = Map; +type BreakpointMap = Map; +type ElementMap = Map; +type SubscriptionMap = Map; +type WatcherMap = WeakMap; +type BuilderMap = WeakMap>; + +export interface ElementMatcher { + element: HTMLElement; + key: string; + value: any; +} + +/** + * MediaMarshaller - register responsive values from directives and + * trigger them based on media query events + */ +@Injectable({providedIn: 'root'}) +export class MediaMarshaller { + private activatedBreakpoints: BreakPoint[] = []; + private elementMap: ElementMap = new Map(); + private watcherMap: WatcherMap = new WeakMap(); + private builderMap: BuilderMap = new WeakMap(); + private subject: Subject = new Subject(); + + get activatedBreakpoint(): string { + return this.activatedBreakpoints[0] ? this.activatedBreakpoints[0].alias : ''; + } + + constructor(protected matchMedia: MatchMedia, + protected breakpoints: BreakPointRegistry) { + this.matchMedia.observe().subscribe(this.activate.bind(this)); + } + + /** + * activate or deactivate a given breakpoint + * @param mc + */ + activate(mc: MediaChange) { + const bp: BreakPoint | null = this.findByQuery(mc.mediaQuery); + if (mc.matches && bp) { + this.activatedBreakpoints.push(bp); + this.activatedBreakpoints.sort(prioritySort); + } else if (!mc.matches && bp) { + // Remove the breakpoint when it's deactivated + this.activatedBreakpoints.splice(this.activatedBreakpoints.indexOf(bp), 1); + } + this.updateStyles(); + } + + /** + * initialize the marshaller with necessary elements for delegation on an element + * @param element + * @param key + * @param builder optional so that custom bp directives don't have to re-provide this + * @param observables + */ + init(element: HTMLElement, + key: string, + builder?: Builder, + observables: Observable[] = []): void { + if (builder) { + let builders = this.builderMap.get(element); + if (!builders) { + builders = new Map(); + this.builderMap.set(element, builders); + } + builders.set(key, builder); + } + if (observables) { + let watchers = this.watcherMap.get(element); + if (!watchers) { + watchers = new Map(); + this.watcherMap.set(element, watchers); + } + const subscription = watchers.get(key); + if (!subscription) { + const newSubscription = merge(...observables).subscribe(() => { + const currentValue = this.getValue(element, key); + this.updateElement(element, key, currentValue); + }); + watchers.set(key, newSubscription); + } + } + } + + /** + * get the value for an element and key and optionally a given breakpoint + * @param element + * @param key + * @param bp + */ + getValue(element: HTMLElement, key: string, bp?: string): any { + const bpMap = this.elementMap.get(element); + if (bpMap) { + const values = bp !== undefined ? bpMap.get(bp) : this.getFallback(bpMap); + if (values) { + const value = values.get(key); + return value !== undefined ? value : ''; + } + } + return ''; + } + + /** + * whether the element has values for a given key + * @param element + * @param key + */ + hasValue(element: HTMLElement, key: string): boolean { + const bpMap = this.elementMap.get(element); + if (bpMap) { + const values = this.getFallback(bpMap); + if (values) { + return values.get(key) !== undefined || false; + } + } + return false; + } + + /** + * Set the value for an input on a directive + * @param element the element in question + * @param key the type of the directive (e.g. flex, layout-gap, etc) + * @param bp the breakpoint suffix (empty string = default) + * @param val the value for the breakpoint + */ + setValue(element: HTMLElement, key: string, val: any, bp: string): void { + let bpMap: BreakpointMap | undefined = this.elementMap.get(element); + if (!bpMap) { + bpMap = new Map().set(bp, new Map().set(key, val)); + this.elementMap.set(element, bpMap); + } else { + const values = (bpMap.get(bp) || new Map()).set(key, val); + bpMap.set(bp, values); + this.elementMap.set(element, bpMap); + } + this.updateElement(element, key, this.getValue(element, key)); + } + + trackValue(element: HTMLElement, key: string): Observable { + return this.subject.asObservable() + .pipe(filter(v => v.element === element && v.key === key)); + } + + /** update all styles for all elements on the current breakpoint */ + updateStyles(): void { + this.elementMap.forEach((bpMap, el) => { + const valueMap = this.getFallback(bpMap); + if (valueMap) { + valueMap.forEach((v, k) => this.updateElement(el, k, v)); + } + }); + } + + /** + * update a given element with the activated values for a given key + * @param element + * @param key + * @param value + */ + updateElement(element: HTMLElement, key: string, value: any): void { + const builders = this.builderMap.get(element); + if (builders) { + const builder: Builder | undefined = builders.get(key); + if (builder) { + builder(value); + this.subject.next({element, key, value}); + } + } + } + + /** + * release all references to a given element + * @param element + */ + releaseElement(element: HTMLElement): void { + const watcherMap = this.watcherMap.get(element); + if (watcherMap) { + watcherMap.forEach(s => s.unsubscribe()); + this.watcherMap.delete(element); + } + const elementMap = this.elementMap.get(element); + if (elementMap) { + elementMap.forEach((_, s) => elementMap.delete(s)); + this.elementMap.delete(element); + } + } + + /** Breakpoint locator by mediaQuery */ + private findByQuery(query: string) { + return this.breakpoints.findByQuery(query); + } + + /** + * get the fallback breakpoint for a given element, starting with the current breakpoint + * @param bpMap + */ + private getFallback(bpMap: BreakpointMap): ValueMap | undefined { + for (let i = 0; i < this.activatedBreakpoints.length; i++) { + const activatedBp = this.activatedBreakpoints[i]; + const valueMap = bpMap.get(activatedBp.alias); + if (valueMap) { + return valueMap; + } + } + return bpMap.get(''); + } +} diff --git a/src/lib/core/media-monitor/media-monitor.ts b/src/lib/core/media-monitor/media-monitor.ts index 8454029cb..ceb3d9097 100644 --- a/src/lib/core/media-monitor/media-monitor.ts +++ b/src/lib/core/media-monitor/media-monitor.ts @@ -28,6 +28,8 @@ import {mergeAlias} from '../add-alias'; * - injects alias information into each raw MediaChange event * - provides accessor to the currently active BreakPoint * - publish list of overlapping BreakPoint(s); used by ResponsiveActivation + * @deprecated + * @deletion-target v7.0.0-beta.21 */ @Injectable({providedIn: 'root'}) export class MediaMonitor { diff --git a/src/lib/core/module.ts b/src/lib/core/module.ts index 5f89388b3..576f7438f 100644 --- a/src/lib/core/module.ts +++ b/src/lib/core/module.ts @@ -8,6 +8,7 @@ import {NgModule} from '@angular/core'; import {BROWSER_PROVIDER} from './browser-provider'; +import {ObservableMediaProvider} from './observable-media/observable-media'; /** * ***************************************************************** @@ -16,7 +17,7 @@ import {BROWSER_PROVIDER} from './browser-provider'; */ @NgModule({ - providers: [BROWSER_PROVIDER] + providers: [BROWSER_PROVIDER, ObservableMediaProvider] }) export class CoreModule { } diff --git a/src/lib/core/public-api.ts b/src/lib/core/public-api.ts index 8c23ddfb0..9faead727 100644 --- a/src/lib/core/public-api.ts +++ b/src/lib/core/public-api.ts @@ -23,3 +23,4 @@ export * from './responsive-activation/responsive-activation'; export * from './style-utils/style-utils'; export * from './style-builder/style-builder'; export * from './basis-validator/basis-validator'; +export * from './media-marshaller/media-marshaller'; diff --git a/src/lib/core/responsive-activation/responsive-activation.ts b/src/lib/core/responsive-activation/responsive-activation.ts index f09c7c9f8..9ff38b241 100644 --- a/src/lib/core/responsive-activation/responsive-activation.ts +++ b/src/lib/core/responsive-activation/responsive-activation.ts @@ -13,11 +13,19 @@ import {BreakPoint} from '../breakpoints/break-point'; import {MediaMonitor} from '../media-monitor/media-monitor'; import {extendObject} from '../../utils/object-extend'; +/** + * @deprecated + * @deletion-target v7.0.0-beta.21 + */ export interface BreakPointX extends BreakPoint { key: string; baseKey: string; } +/** + * @deprecated + * @deletion-target v7.0.0-beta.21 + */ export class KeyOptions { constructor(public baseKey: string, public defaultValue: string|number|boolean, @@ -36,6 +44,8 @@ export class KeyOptions { * MediaQueryServices. * * NOTE: these interceptions enables the logic in the fx API directives to remain terse and clean. + * @deprecated + * @deletion-target v7.0.0-beta.21 */ export class ResponsiveActivation { private _activatedInputKey: string = ''; diff --git a/src/lib/extended/class/class.spec.ts b/src/lib/extended/class/class.spec.ts index aba666397..21d3c7f3d 100644 --- a/src/lib/extended/class/class.spec.ts +++ b/src/lib/extended/class/class.spec.ts @@ -18,7 +18,7 @@ import { import {customMatchers, expect} from '../../utils/testing/custom-matchers'; import {makeCreateTestComponent, expectNativeEl, queryFor} from '../../utils/testing/helpers'; -import {ClassDirective} from './class'; +import {DefaultClassDirective} from './class'; describe('class directive', () => { @@ -44,7 +44,7 @@ describe('class directive', () => { CommonModule, CoreModule ], - declarations: [TestClassComponent, ClassDirective], + declarations: [TestClassComponent, DefaultClassDirective], providers: [MockMatchMediaProvider] }); }); diff --git a/src/lib/extended/class/class.ts b/src/lib/extended/class/class.ts index c41e9e68e..bcb740065 100644 --- a/src/lib/extended/class/class.ts +++ b/src/lib/extended/class/class.ts @@ -12,155 +12,80 @@ import { Input, IterableDiffers, KeyValueDiffers, - OnChanges, - OnDestroy, Optional, Renderer2, - SimpleChanges, Self, - OnInit, } from '@angular/core'; import {NgClass} from '@angular/common'; -import { - BaseDirective, - BaseDirectiveAdapter, - MediaChange, - MediaMonitor, - StyleUtils, -} from '@angular/flex-layout/core'; - +import {BaseDirective2, StyleUtils, MediaMarshaller} from '@angular/flex-layout/core'; -/** NgClass allowed inputs **/ -export type NgClassType = string | string[] | Set | {[klass: string]: any}; +export class ClassDirective extends BaseDirective2 implements DoCheck { -/** - * Directive to add responsive support for ngClass. - * This maintains the core functionality of 'ngClass' and adds responsive API - * Note: this class is a no-op when rendered on the server - */ -@Directive({ - selector: ` - [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], - [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], - [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg] - ` -}) -export class ClassDirective extends BaseDirective - implements DoCheck, OnChanges, OnDestroy, OnInit { - - /** - * Intercept ngClass assignments so we cache the default classes - * which are merged with activated styles or used as fallbacks. - * Note: Base ngClass values are applied during ngDoCheck() - */ - @Input('ngClass') - set ngClassBase(val: NgClassType) { - const key = 'ngClass'; - this._base.cacheInput(key, val, true); - this._ngClassInstance.ngClass = this._base.queryInput(key); - } + protected DIRECTIVE_KEY = 'ngClass'; /** * Capture class assignments so we cache the default classes * which are merged with activated styles and used as fallbacks. */ @Input('class') - set klazz(val: string) { - const key = 'class'; - this._base.cacheInput(key, val); - this._ngClassInstance.klass = val; + set klass(val: string) { + this.ngClassInstance.klass = val; + this.setValue(val, ''); } - /* tslint:disable */ - @Input('ngClass.xs') set ngClassXs(val: NgClassType) { this._base.cacheInput('ngClassXs', val, true); } - @Input('ngClass.sm') set ngClassSm(val: NgClassType) { this._base.cacheInput('ngClassSm', val, true); } - @Input('ngClass.md') set ngClassMd(val: NgClassType) { this._base.cacheInput('ngClassMd', val, true); } - @Input('ngClass.lg') set ngClassLg(val: NgClassType) { this._base.cacheInput('ngClassLg', val, true); } - @Input('ngClass.xl') set ngClassXl(val: NgClassType) { this._base.cacheInput('ngClassXl', val, true); } - - @Input('ngClass.lt-sm') set ngClassLtSm(val: NgClassType) { this._base.cacheInput('ngClassLtSm', val, true); } - @Input('ngClass.lt-md') set ngClassLtMd(val: NgClassType) { this._base.cacheInput('ngClassLtMd', val, true); } - @Input('ngClass.lt-lg') set ngClassLtLg(val: NgClassType) { this._base.cacheInput('ngClassLtLg', val, true); } - @Input('ngClass.lt-xl') set ngClassLtXl(val: NgClassType) { this._base.cacheInput('ngClassLtXl', val, true); } - - @Input('ngClass.gt-xs') set ngClassGtXs(val: NgClassType) { this._base.cacheInput('ngClassGtXs', val, true); } - @Input('ngClass.gt-sm') set ngClassGtSm(val: NgClassType) { this._base.cacheInput('ngClassGtSm', val, true); } - @Input('ngClass.gt-md') set ngClassGtMd(val: NgClassType) { this._base.cacheInput('ngClassGtMd', val, true); } - @Input('ngClass.gt-lg') set ngClassGtLg(val: NgClassType) { this._base.cacheInput('ngClassGtLg', val, true); } - - /* tslint:enable */ - constructor(protected monitor: MediaMonitor, - protected _iterableDiffers: IterableDiffers, - protected _keyValueDiffers: KeyValueDiffers, - protected _ngEl: ElementRef, - protected _renderer: Renderer2, - @Optional() @Self() private readonly _ngClassInstance: NgClass, - protected _styler: StyleUtils) { - super(monitor, _ngEl, _styler); - this._base = new BaseDirectiveAdapter( - 'ngClass', - this.monitor, - this._ngEl, - this._styler - ); - if (!this._ngClassInstance) { + constructor(protected elementRef: ElementRef, + protected styler: StyleUtils, + protected marshal: MediaMarshaller, + protected iterableDiffers: IterableDiffers, + protected keyValueDiffers: KeyValueDiffers, + protected renderer: Renderer2, + @Optional() @Self() protected readonly ngClassInstance: NgClass) { + super(elementRef, null!, styler, marshal); + if (!this.ngClassInstance) { // Create an instance NgClass Directive instance only if `ngClass=""` has NOT been defined on // the same host element; since the responsive variations may be defined... - this._ngClassInstance = new NgClass( - this._iterableDiffers, this._keyValueDiffers, this._ngEl, this._renderer + this.ngClassInstance = new NgClass( + this.iterableDiffers, this.keyValueDiffers, this.elementRef, this.renderer ); } + this.marshal.init(this.nativeElement, this.DIRECTIVE_KEY, this.updateWithValue.bind(this)); + } + + protected updateWithValue(value: any) { + this.ngClassInstance.ngClass = value; + this.ngClassInstance.ngDoCheck(); } // ****************************************************************** // Lifecycle Hooks // ****************************************************************** - /** - * For @Input changes on the current mq activation property - */ - ngOnChanges(changes: SimpleChanges) { - if (this._base.activeKey in changes) { - this._ngClassInstance.ngClass = this._base.mqActivation.activatedInput || ''; - } - } - - ngOnInit() { - this._configureMQListener(); - } - /** * For ChangeDetectionStrategy.onPush and ngOnChanges() updates */ ngDoCheck() { - this._ngClassInstance.ngDoCheck(); + this.ngClassInstance.ngDoCheck(); } +} - ngOnDestroy() { - this._base.ngOnDestroy(); - } +const inputs = [ + 'ngClass', 'ngClass.xs', 'ngClass.sm', 'ngClass.md', 'ngClass.lg', 'ngClass.xl', + 'ngClass.lt-sm', 'ngClass.lt-md', 'ngClass.lt-lg', 'ngClass.lt-xl', + 'ngClass.gt-xs', 'ngClass.gt-sm', 'ngClass.gt-md', 'ngClass.gt-lg' +]; - // ****************************************************************** - // Internal Methods - // ****************************************************************** +const selector = ` + [ngClass], [ngClass.xs], [ngClass.sm], [ngClass.md], [ngClass.lg], [ngClass.xl], + [ngClass.lt-sm], [ngClass.lt-md], [ngClass.lt-lg], [ngClass.lt-xl], + [ngClass.gt-xs], [ngClass.gt-sm], [ngClass.gt-md], [ngClass.gt-lg] +`; - /** - * Build an mqActivation object that bridges mql change events to onMediaQueryChange handlers - * NOTE: We delegate subsequent activity to the NgClass logic - * Identify the activated input value and update the ngClass iterables... - * Use ngDoCheck() to actually apply the values to the element - */ - protected _configureMQListener(baseKey = 'ngClass') { - const fallbackValue = this._base.queryInput(baseKey); - this._base.listenForMediaQueryChanges(baseKey, fallbackValue, (changes: MediaChange) => { - this._ngClassInstance.ngClass = changes.value || ''; - this._ngClassInstance.ngDoCheck(); - }); - } - - /** - * Special adapter to cross-cut responsive behaviors and capture mediaQuery changes - * Delegate value changes to the internal `_ngClassInstance` for processing - */ - protected _base: BaseDirectiveAdapter; // used for `ngClass.xxx` selectors +/** + * Directive to add responsive support for ngClass. + * This maintains the core functionality of 'ngClass' and adds responsive API + * Note: this class is a no-op when rendered on the server + */ +@Directive({selector, inputs}) +export class DefaultClassDirective extends ClassDirective { + protected inputs = inputs; } diff --git a/src/lib/extended/img-src/img-src.ts b/src/lib/extended/img-src/img-src.ts index 0050bec66..a6d19de03 100644 --- a/src/lib/extended/img-src/img-src.ts +++ b/src/lib/extended/img-src/img-src.ts @@ -5,96 +5,46 @@ * 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 { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - Inject, - Optional, - PLATFORM_ID, -} from '@angular/core'; +import {Directive, ElementRef, Inject, PLATFORM_ID, Injectable, Input} from '@angular/core'; import {isPlatformServer} from '@angular/common'; import { - BaseDirective, - MediaMonitor, + MediaMarshaller, + BaseDirective2, SERVER_TOKEN, + StyleBuilder, + StyleDefinition, StyleUtils, } from '@angular/flex-layout/core'; - -/** - * This directive provides a responsive API for the HTML 'src' attribute - * and will update the img.src property upon each responsive activation. - * - * e.g. - * - * - * @see https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-src/ - */ -@Directive({ - selector: ` - img[src.xs], img[src.sm], img[src.md], img[src.lg], img[src.xl], - img[src.lt-sm], img[src.lt-md], img[src.lt-lg], img[src.lt-xl], - img[src.gt-xs], img[src.gt-sm], img[src.gt-md], img[src.gt-lg] -` -}) -export class ImgSrcDirective extends BaseDirective implements OnInit, OnChanges { - - /* tslint:disable */ - @Input('src') set srcBase(val: string) { this.cacheDefaultSrc(val); } - - @Input('src.xs') set srcXs(val: string) { this._cacheInput('srcXs', val); } - @Input('src.sm') set srcSm(val: string) { this._cacheInput('srcSm', val); } - @Input('src.md') set srcMd(val: string) { this._cacheInput('srcMd', val); } - @Input('src.lg') set srcLg(val: string) { this._cacheInput('srcLg', val); } - @Input('src.xl') set srcXl(val: string) { this._cacheInput('srcXl', val); } - - @Input('src.lt-sm') set srcLtSm(val: string) { this._cacheInput('srcLtSm', val); } - @Input('src.lt-md') set srcLtMd(val: string) { this._cacheInput('srcLtMd', val); } - @Input('src.lt-lg') set srcLtLg(val: string) { this._cacheInput('srcLtLg', val); } - @Input('src.lt-xl') set srcLtXl(val: string) { this._cacheInput('srcLtXl', val); } - - @Input('src.gt-xs') set srcGtXs(val: string) { this._cacheInput('srcGtXs', val); } - @Input('src.gt-sm') set srcGtSm(val: string) { this._cacheInput('srcGtSm', val); } - @Input('src.gt-md') set srcGtMd(val: string) { this._cacheInput('srcGtMd', val); } - @Input('src.gt-lg') set srcGtLg(val: string) { this._cacheInput('srcGtLg', val); } - /* tslint:enable */ - - constructor(protected _elRef: ElementRef, - protected _monitor: MediaMonitor, - protected _styler: StyleUtils, - @Inject(PLATFORM_ID) protected _platformId: Object, - @Optional() @Inject(SERVER_TOKEN) protected _serverModuleLoaded: boolean) { - super(_monitor, _elRef, _styler); - this._cacheInput('src', _elRef.nativeElement.getAttribute('src') || ''); - if (isPlatformServer(this._platformId) && this._serverModuleLoaded) { - this.nativeElement.setAttribute('src', ''); - } +@Injectable({providedIn: 'root'}) +export class ImgSrcStyleBuilder extends StyleBuilder { + buildStyles(url: string) { + return {'content': url ? `url(${url})` : ''}; } +} - /** - * Listen for responsive changes to update the img.src attribute - */ - ngOnInit() { - super.ngOnInit(); +export class ImgSrcDirective extends BaseDirective2 { + protected DIRECTIVE_KEY = 'img-src'; + protected defaultSrc = ''; - if (this.hasResponsiveKeys) { - // Listen for responsive changes - this._listenForMediaQueryChanges('src', this.defaultSrc, () => { - this._updateSrcFor(); - }); - } - this._updateSrcFor(); + @Input('src') + set src(val: string) { + this.defaultSrc = val; + this.setValue('', this.defaultSrc); } - /** - * Update the 'src' property of the host element - */ - ngOnChanges() { - if (this.hasInitialized) { - this._updateSrcFor(); + constructor(protected elementRef: ElementRef, + protected styleBuilder: ImgSrcStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller, + @Inject(PLATFORM_ID) protected platformId: Object, + @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.updateSrcFor.bind(this)); + this.setValue('', this.nativeElement.getAttribute('src') || ''); + if (isPlatformServer(this.platformId) && this.serverModuleLoaded) { + this.nativeElement.setAttribute('src', ''); } } @@ -106,40 +56,42 @@ export class ImgSrcDirective extends BaseDirective implements OnInit, OnChanges * Do nothing to standard `` usages, only when responsive * keys are present do we actually call `setAttribute()` */ - protected _updateSrcFor() { - if (this.hasResponsiveKeys) { - let url = this.activatedValue || this.defaultSrc; - if (isPlatformServer(this._platformId) && this._serverModuleLoaded) { - this._styler.applyStyleToElement(this.nativeElement, {'content': url ? `url(${url})` : ''}); - } else { - this.nativeElement.setAttribute('src', String(url)); - } + protected updateSrcFor() { + let url = this.activatedValue || this.defaultSrc; + if (isPlatformServer(this.platformId) && this.serverModuleLoaded) { + this.addStyles(url); + } else { + this.nativeElement.setAttribute('src', String(url)); } } - /** - * Cache initial value of 'src', this will be used as fallback when breakpoint - * activations change. - * NOTE: The default 'src' property is not bound using @Input(), so perform - * a post-ngOnInit() lookup of the default src value (if any). - */ - protected cacheDefaultSrc(value?: string) { - this._cacheInput('src', value || ''); - } + protected styleCache = imgSrcCache; +} - /** - * Empty values are maintained, undefined values are exposed as '' - */ - protected get defaultSrc(): string { - return this._queryInput('src') || ''; - } +const imgSrcCache: Map = new Map(); - /** - * Does the have 1 or more src. responsive inputs - * defined... these will be mapped to activated breakpoints. - */ - protected get hasResponsiveKeys() { - return Object.keys(this._inputMap).length > 1; - } +const inputs = [ + 'src.xs', 'src.sm', 'src.md', 'src.lg', 'src.xl', + 'src.lt-sm', 'src.lt-md', 'src.lt-lg', 'src.lt-xl', + 'src.gt-xs', 'src.gt-sm', 'src.gt-md', 'src.gt-lg' +]; +const selector = ` + img[src.xs], img[src.sm], img[src.md], img[src.lg], img[src.xl], + img[src.lt-sm], img[src.lt-md], img[src.lt-lg], img[src.lt-xl], + img[src.gt-xs], img[src.gt-sm], img[src.gt-md], img[src.gt-lg] +`; + +/** + * This directive provides a responsive API for the HTML 'src' attribute + * and will update the img.src property upon each responsive activation. + * + * e.g. + * + * + * @see https://css-tricks.com/responsive-images-youre-just-changing-resolutions-use-src/ + */ +@Directive({selector, inputs}) +export class DefaultImgSrcDirective extends ImgSrcDirective { + protected inputs = inputs; } diff --git a/src/lib/extended/module.ts b/src/lib/extended/module.ts index e38773cbb..5f0e77ae2 100644 --- a/src/lib/extended/module.ts +++ b/src/lib/extended/module.ts @@ -8,17 +8,17 @@ import {NgModule} from '@angular/core'; import {CoreModule} from '@angular/flex-layout/core'; -import {ImgSrcDirective} from './img-src/img-src'; -import {ClassDirective} from './class/class'; -import {ShowHideDirective} from './show-hide/show-hide'; -import {StyleDirective} from './style/style'; +import {DefaultImgSrcDirective} from './img-src/img-src'; +import {DefaultClassDirective} from './class/class'; +import {DefaultShowHideDirective} from './show-hide/show-hide'; +import {DefaultStyleDirective} from './style/style'; const ALL_DIRECTIVES = [ - ShowHideDirective, - ClassDirective, - StyleDirective, - ImgSrcDirective + DefaultShowHideDirective, + DefaultClassDirective, + DefaultStyleDirective, + DefaultImgSrcDirective ]; /** diff --git a/src/lib/extended/show-hide/hide.spec.ts b/src/lib/extended/show-hide/hide.spec.ts index f89b8f6ea..898a3fcb5 100644 --- a/src/lib/extended/show-hide/hide.spec.ts +++ b/src/lib/extended/show-hide/hide.spec.ts @@ -22,7 +22,7 @@ import {customMatchers, expect, NgMatchers} from '../../utils/testing/custom-mat import { makeCreateTestComponent, expectNativeEl, queryFor } from '../../utils/testing/helpers'; -import {ShowHideDirective} from './show-hide'; +import {DefaultShowHideDirective} from './show-hide'; describe('hide directive', () => { @@ -60,7 +60,7 @@ describe('hide directive', () => { // Configure testbed to prepare services TestBed.configureTestingModule({ imports: [CommonModule, CoreModule], - declarations: [TestHideComponent, ShowHideDirective], + declarations: [TestHideComponent, DefaultShowHideDirective], providers: [ MockMatchMediaProvider, {provide: SERVER_TOKEN, useValue: true}, diff --git a/src/lib/extended/show-hide/show-hide.ts b/src/lib/extended/show-hide/show-hide.ts index 97cf05d13..968077b97 100644 --- a/src/lib/extended/show-hide/show-hide.ts +++ b/src/lib/extended/show-hide/show-hide.ts @@ -8,135 +8,104 @@ import { Directive, ElementRef, - Input, - OnInit, OnChanges, - OnDestroy, SimpleChanges, - Self, Optional, Inject, PLATFORM_ID, - ViewChild, + Injectable, AfterViewInit, } from '@angular/core'; import {isPlatformServer} from '@angular/common'; import { - BaseDirective, + BaseDirective2, LAYOUT_CONFIG, LayoutConfigOptions, - MediaChange, - MediaMonitor, + MediaMarshaller, SERVER_TOKEN, StyleUtils, + StyleBuilder, } from '@angular/flex-layout/core'; -import {FlexDirective, LayoutDirective} from '@angular/flex-layout/flex'; -import {Subscription} from 'rxjs'; - -const FALSY = ['false', false, 0]; +import {coerceBooleanProperty} from '@angular/cdk/coercion'; +import {takeUntil} from 'rxjs/operators'; /** * For fxHide selectors, we invert the 'value' * and assign to the equivalent fxShow selector cache * - When 'hide' === '' === true, do NOT show the element * - When 'hide' === false or 0... we WILL show the element + * @deprecated + * @deletion-target v7.0.0-beta.21 */ export function negativeOf(hide: any) { return (hide === '') ? false : ((hide === 'false') || (hide === 0)) ? true : !hide; } -/** - * 'show' Layout API directive - * - */ -@Directive({ - selector: ` - [fxShow], - [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], - [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], - [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], - [fxHide], - [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], - [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], - [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg] -` -}) -export class ShowHideDirective extends BaseDirective - implements OnInit, OnChanges, OnDestroy, AfterViewInit { +export interface ShowHideParent { + display: string; +} - /** - * Subscription to the parent flex container's layout changes. - * Stored so we can unsubscribe when this directive is destroyed. - */ - protected _layoutWatcher?: Subscription; +@Injectable({providedIn: 'root'}) +export class ShowHideStyleBuilder extends StyleBuilder { + buildStyles(show: string, parent: ShowHideParent) { + const shouldShow = show === 'true'; + return {'display': shouldShow ? parent.display : 'none'}; + } +} + +export class ShowHideDirective extends BaseDirective2 implements AfterViewInit, OnChanges { + protected DIRECTIVE_KEY = 'show-hide'; /** Original dom Elements CSS display style */ - protected _display: string = ''; - - /* tslint:disable */ - @Input('fxShow') set show(val: string) { this._cacheInput('show', val); } - @Input('fxShow.xs') set showXs(val: string) {this._cacheInput('showXs', val);} - @Input('fxShow.sm') set showSm(val: string) {this._cacheInput('showSm', val); }; - @Input('fxShow.md') set showMd(val: string) {this._cacheInput('showMd', val); }; - @Input('fxShow.lg') set showLg(val: string) {this._cacheInput('showLg', val); }; - @Input('fxShow.xl') set showXl(val: string) {this._cacheInput('showXl', val); }; - - @Input('fxShow.lt-sm') set showLtSm(val: string) { this._cacheInput('showLtSm', val); }; - @Input('fxShow.lt-md') set showLtMd(val: string) { this._cacheInput('showLtMd', val); }; - @Input('fxShow.lt-lg') set showLtLg(val: string) { this._cacheInput('showLtLg', val); }; - @Input('fxShow.lt-xl') set showLtXl(val: string) { this._cacheInput('showLtXl', val); }; - - @Input('fxShow.gt-xs') set showGtXs(val: string) {this._cacheInput('showGtXs', val); }; - @Input('fxShow.gt-sm') set showGtSm(val: string) {this._cacheInput('showGtSm', val); }; - @Input('fxShow.gt-md') set showGtMd(val: string) {this._cacheInput('showGtMd', val); }; - @Input('fxShow.gt-lg') set showGtLg(val: string) {this._cacheInput('showGtLg', val); }; - - @Input('fxHide') set hide(val: string) {this._cacheInput('show', negativeOf(val));} - @Input('fxHide.xs') set hideXs(val: string) {this._cacheInput('showXs', negativeOf(val));} - @Input('fxHide.sm') set hideSm(val: string) {this._cacheInput('showSm', negativeOf(val)); }; - @Input('fxHide.md') set hideMd(val: string) {this._cacheInput('showMd', negativeOf(val)); }; - @Input('fxHide.lg') set hideLg(val: string) {this._cacheInput('showLg', negativeOf(val)); }; - @Input('fxHide.xl') set hideXl(val: string) {this._cacheInput('showXl', negativeOf(val)); }; - - @Input('fxHide.lt-sm') set hideLtSm(val: string) { this._cacheInput('showLtSm', negativeOf(val)); }; - @Input('fxHide.lt-md') set hideLtMd(val: string) { this._cacheInput('showLtMd', negativeOf(val)); }; - @Input('fxHide.lt-lg') set hideLtLg(val: string) { this._cacheInput('showLtLg', negativeOf(val)); }; - @Input('fxHide.lt-xl') set hideLtXl(val: string) { this._cacheInput('showLtXl', negativeOf(val)); }; - - @Input('fxHide.gt-xs') set hideGtXs(val: string) {this._cacheInput('showGtXs', negativeOf(val)); }; - @Input('fxHide.gt-sm') set hideGtSm(val: string) {this._cacheInput('showGtSm', negativeOf(val)); }; - @Input('fxHide.gt-md') set hideGtMd(val: string) {this._cacheInput('showGtMd', negativeOf(val)); }; - @Input('fxHide.gt-lg') set hideGtLg(val: string) {this._cacheInput('showGtLg', negativeOf(val)); }; - /* tslint:enable */ - - @ViewChild(FlexDirective) protected _flexChild: FlexDirective | null = null; - - constructor(monitor: MediaMonitor, - @Optional() @Self() protected layout: LayoutDirective, - protected elRef: ElementRef, - protected styleUtils: StyleUtils, + protected display: string = ''; + protected hasLayout = false; + protected hasFlexChild = false; + + constructor(protected elementRef: ElementRef, + protected styleBuilder: ShowHideStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller, + @Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions, @Inject(PLATFORM_ID) protected platformId: Object, - @Optional() @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean, - @Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions) { - - super(monitor, elRef, styleUtils); + @Optional() @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean) { + super(elementRef, styleBuilder, styler, marshal); } // ********************************************* // Lifecycle Methods // ********************************************* - /** - * Override accessor to the current HTMLElement's `display` style - * Note: Show/Hide will not change the display to 'flex' but will set it to 'block' - * unless it was already explicitly specified inline or in a CSS stylesheet. - */ - protected _getDisplayStyle(): string { - return (this.layout || (this._flexChild && this.layoutConfig.addFlexToParent)) ? - 'flex' : super._getDisplayStyle(); - } + ngAfterViewInit() { + this.hasLayout = this.marshal.hasValue(this.nativeElement, 'layout'); + this.marshal.trackValue(this.nativeElement, 'layout') + .pipe(takeUntil(this.destroySubject)) + .subscribe(this.updateWithValue.bind(this)); + + const children = Array.from(this.nativeElement.children); + for (let i = 0; i < children.length; i++) { + if (this.marshal.hasValue(children[i] as HTMLElement, 'flex')) { + this.hasFlexChild = true; + break; + } + } + if (DISPLAY_MAP.has(this.nativeElement)) { + this.display = DISPLAY_MAP.get(this.nativeElement)!; + } else { + this.display = this.getDisplayStyle(); + DISPLAY_MAP.set(this.nativeElement, this.display); + } + + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); + // set the default to show unless explicitly overridden + const defaultValue = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY, ''); + if (defaultValue === undefined || defaultValue === '') { + this.setValue(true, ''); + } + this.updateWithValue(this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY)); + } /** * On changes to any @Input properties... @@ -144,76 +113,76 @@ export class ShowHideDirective extends BaseDirective * Then conditionally override with the mq-activated Input's current value */ ngOnChanges(changes: SimpleChanges) { - if (this.hasInitialized && (changes['show'] != null || this._mqActivation)) { - this._updateWithValue(); - } - } - - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - } - - ngAfterViewInit() { - if (DISPLAY_MAP.has(this.nativeElement)) { - this._display = DISPLAY_MAP.get(this.nativeElement)!; - } else { - this._display = this._getDisplayStyle(); - DISPLAY_MAP.set(this.nativeElement, this._display); - } - if (this.layout) { - /** - * The Layout can set the display:flex (and incorrectly affect the Hide/Show directives. - * Whenever Layout [on the same element] resets its CSS, then update the Hide/Show CSS - */ - this._layoutWatcher = this.layout.layout$.subscribe(() => this._updateWithValue()); - } - let value = this._getDefaultVal('show', true); - // Build _mqActivation controller - this._listenForMediaQueryChanges('show', value, (changes: MediaChange) => { - this._updateWithValue(changes.value); + Object.keys(changes).forEach(key => { + if (this.inputs.indexOf(key) !== -1) { + const inputKey = key.split('.'); + const bp = inputKey[1] || ''; + const inputValue = changes[key].currentValue; + let shouldShow = inputValue !== '' ? + inputValue !== 0 ? coerceBooleanProperty(inputValue) : false + : true; + if (inputKey[0] === 'fxHide') { + shouldShow = !shouldShow; + } + this.setValue(shouldShow, bp); + } }); - this._updateWithValue(); - } - - ngOnDestroy() { - super.ngOnDestroy(); - if (this._layoutWatcher) { - this._layoutWatcher.unsubscribe(); - } } // ********************************************* // Protected methods // ********************************************* + /** + * Override accessor to the current HTMLElement's `display` style + * Note: Show/Hide will not change the display to 'flex' but will set it to 'block' + * unless it was already explicitly specified inline or in a CSS stylesheet. + */ + protected getDisplayStyle(): string { + return (this.hasLayout || (this.hasFlexChild && this.layoutConfig.addFlexToParent)) ? + 'flex' : this.styler.lookupStyle(this.nativeElement, 'display', true); + } + /** Validate the visibility value and then update the host's inline display style */ - protected _updateWithValue(value?: string|number|boolean) { - value = value || this._getDefaultVal('show', true); - if (this._mqActivation) { - value = this._mqActivation.activatedInput; + protected updateWithValue(value: boolean|string = true) { + if (value === '') { + return; } - - let shouldShow = this._validateTruthy(value); - this._applyStyleToElement(this._buildCSS(shouldShow)); + this.addStyles(value ? 'true' : 'false', {display: this.display}); if (isPlatformServer(this.platformId) && this.serverModuleLoaded) { this.nativeElement.style.setProperty('display', ''); } } +} +const DISPLAY_MAP: WeakMap = new WeakMap(); - /** Build the CSS that should be assigned to the element instance */ - protected _buildCSS(show: boolean) { - return {'display': show ? this._display : 'none'}; - } +const inputs = [ + 'fxShow', + 'fxShow.xs', 'fxShow.sm', 'fxShow.md', 'fxShow.lg', 'fxShow.xl', + 'fxShow.lt-sm', 'fxShow.lt-md', 'fxShow.lt-lg', 'fxShow.lt-xl', + 'fxShow.gt-xs', 'fxShow.gt-sm', 'fxShow.gt-md', 'fxShow.gt-lg', + 'fxHide', + 'fxHide.xs', 'fxHide.sm', 'fxHide.md', 'fxHide.lg', 'fxHide.xl', + 'fxHide.lt-sm', 'fxHide.lt-md', 'fxHide.lt-lg', 'fxHide.lt-xl', + 'fxHide.gt-xs', 'fxHide.gt-sm', 'fxHide.gt-md', 'fxHide.gt-lg' +]; + +const selector = ` + [fxShow], + [fxShow.xs], [fxShow.sm], [fxShow.md], [fxShow.lg], [fxShow.xl], + [fxShow.lt-sm], [fxShow.lt-md], [fxShow.lt-lg], [fxShow.lt-xl], + [fxShow.gt-xs], [fxShow.gt-sm], [fxShow.gt-md], [fxShow.gt-lg], + [fxHide], + [fxHide.xs], [fxHide.sm], [fxHide.md], [fxHide.lg], [fxHide.xl], + [fxHide.lt-sm], [fxHide.lt-md], [fxHide.lt-lg], [fxHide.lt-xl], + [fxHide.gt-xs], [fxHide.gt-sm], [fxHide.gt-md], [fxHide.gt-lg] +`; - /** Validate the to be not FALSY */ - _validateTruthy(show: string | number | boolean = '') { - return (FALSY.indexOf(show) === -1); - } +/** + * 'show' Layout API directive + */ +@Directive({selector, inputs}) +export class DefaultShowHideDirective extends ShowHideDirective { + protected inputs = inputs; } - -const DISPLAY_MAP: WeakMap = new WeakMap(); diff --git a/src/lib/extended/show-hide/show.spec.ts b/src/lib/extended/show-hide/show.spec.ts index d529d2d27..152293258 100644 --- a/src/lib/extended/show-hide/show.spec.ts +++ b/src/lib/extended/show-hide/show.spec.ts @@ -5,24 +5,11 @@ * 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 { - Component, - Directive, - ElementRef, - Inject, - Input, - OnInit, - Optional, - PLATFORM_ID, - Self, -} from '@angular/core'; +import {Component, Directive, OnInit, PLATFORM_ID} from '@angular/core'; import {CommonModule, isPlatformBrowser} from '@angular/common'; import {ComponentFixture, TestBed, inject, async} from '@angular/core/testing'; import { - LAYOUT_CONFIG, - LayoutConfigOptions, MatchMedia, - MediaMonitor, MockMatchMedia, MockMatchMediaProvider, MediaObserver, @@ -42,8 +29,7 @@ import {MatFormFieldModule} from '@angular/material/form-field'; import {FormsModule} from '@angular/forms'; import {MatSelectModule} from '@angular/material/select'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {negativeOf, ShowHideDirective} from './show-hide'; -import {LayoutDirective} from '@angular/flex-layout/flex'; +import {ShowHideDirective} from './show-hide'; describe('show directive', () => { let fixture: ComponentFixture; @@ -268,25 +254,25 @@ describe('show directive', () => { `); - let selector = '[el]'; + const elSelector = '[el]'; matchMedia.useOverlaps = true; fixture.detectChanges(); // NOTE: platform-server can't compute display for unknown elements if (isPlatformBrowser(platformId)) { - expectEl(queryFor(fixture, selector)[0]).toHaveStyle({ + expectEl(queryFor(fixture, elSelector)[0]).toHaveCSS({ 'display': 'inline' }, styler); } else { - expectEl(queryFor(fixture, selector)[0]).not.toHaveStyle({ + expectEl(queryFor(fixture, elSelector)[0]).not.toHaveStyle({ 'display': '*' }, styler); } matchMedia.activate('xs'); fixture.detectChanges(); - expectEl(queryFor(fixture, selector)[0]).toHaveStyle({ + expectEl(queryFor(fixture, elSelector)[0]).toHaveStyle({ 'display': 'none' }, styler); @@ -294,11 +280,11 @@ describe('show directive', () => { fixture.detectChanges(); // NOTE: platform-server can't compute display for unknown elements if (isPlatformBrowser(platformId)) { - expectEl(queryFor(fixture, selector)[0]).toHaveStyle({ + expectEl(queryFor(fixture, elSelector)[0]).toHaveCSS({ 'display': 'inline' }, styler); } else { - expectEl(queryFor(fixture, selector)[0]).not.toHaveStyle({ + expectEl(queryFor(fixture, elSelector)[0]).not.toHaveStyle({ 'display': '*' }, styler); } @@ -348,26 +334,13 @@ describe('show directive', () => { }); -@Directive({ - selector: `[fxShow.sm-md], [fxHide.sm-md]` -}) -class FxShowHideDirective extends ShowHideDirective { - constructor(monitor: MediaMonitor, - @Optional() @Self() protected layout: LayoutDirective, - protected elRef: ElementRef, - protected styleUtils: StyleUtils, - @Inject(PLATFORM_ID) protected platformId: Object, - @Optional() @Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean, - @Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions) { - super(monitor, layout, elRef, styleUtils, platformId, serverModuleLoaded, layoutConfig); - } +const inputs = ['fxShow.sm-md', 'fxHide.sm-md']; +const selector = `[fxShow.sm-md], [fxHide.sm-md]`; - @Input('fxShow.sm-md') set showSmMd(val: string) { - this._cacheInput('showSmMd', val); - } - @Input('fxHide.sm-md') set hideSmMd(val: string) { - this._cacheInput('showSmMd', negativeOf(val)); - } +// Used to test custom breakpoint functionality +@Directive({inputs, selector}) +class FxShowHideDirective extends ShowHideDirective { + protected inputs = inputs; } diff --git a/src/lib/extended/style/style.spec.ts b/src/lib/extended/style/style.spec.ts index f8fe88069..f25aed40e 100644 --- a/src/lib/extended/style/style.spec.ts +++ b/src/lib/extended/style/style.spec.ts @@ -15,9 +15,9 @@ import { MockMatchMediaProvider, StyleUtils, } from '@angular/flex-layout/core'; -import {LayoutDirective} from '@angular/flex-layout/flex'; +import {DefaultLayoutDirective} from '@angular/flex-layout/flex'; -import {StyleDirective} from './style'; +import {DefaultStyleDirective} from './style'; import {customMatchers} from '../../utils/testing/custom-matchers'; import { makeCreateTestComponent, expectNativeEl @@ -42,7 +42,7 @@ describe('style directive', () => { // Configure testbed to prepare services TestBed.configureTestingModule({ imports: [CommonModule, CoreModule], - declarations: [TestStyleComponent, LayoutDirective, StyleDirective], + declarations: [TestStyleComponent, DefaultLayoutDirective, DefaultStyleDirective], providers: [MockMatchMediaProvider] }); }); diff --git a/src/lib/extended/style/style.ts b/src/lib/extended/style/style.ts index 12136729e..98e758566 100644 --- a/src/lib/extended/style/style.ts +++ b/src/lib/extended/style/style.ts @@ -9,28 +9,16 @@ import { Directive, DoCheck, ElementRef, - Input, KeyValueDiffers, - OnDestroy, - OnChanges, Optional, Renderer2, SecurityContext, Self, - SimpleChanges, - OnInit, } from '@angular/core'; import {NgStyle} from '@angular/common'; import {DomSanitizer} from '@angular/platform-browser'; -import { - BaseDirective, - BaseDirectiveAdapter, - MediaChange, - MediaMonitor, - StyleUtils, -} from '@angular/flex-layout/core'; +import {BaseDirective2, StyleUtils, MediaMarshaller} from '@angular/flex-layout/core'; -import {extendObject} from '../../utils/object-extend'; import { NgStyleRawList, NgStyleType, @@ -44,130 +32,33 @@ import { keyValuesToMap, } from './style-transforms'; -/** - * Directive to add responsive support for ngStyle. - * - */ -@Directive({ - selector: ` - [ngStyle.xs], [ngStyle.sm], [ngStyle.md], [ngStyle.lg], [ngStyle.xl], - [ngStyle.lt-sm], [ngStyle.lt-md], [ngStyle.lt-lg], [ngStyle.lt-xl], - [ngStyle.gt-xs], [ngStyle.gt-sm], [ngStyle.gt-md], [ngStyle.gt-lg] - ` -}) -export class StyleDirective extends BaseDirective implements DoCheck, OnChanges, OnDestroy, OnInit { - - /** - * Intercept ngStyle assignments so we cache the default styles - * which are merged with activated styles or used as fallbacks. - */ - @Input('ngStyle') - set ngStyleBase(val: NgStyleType) { - const key = 'ngStyle'; - this._base.cacheInput(key, val, true); // convert val to hashmap - this._ngStyleInstance.ngStyle = this._base.queryInput(key); - } - - /* tslint:disable */ - @Input('ngStyle.xs') set ngStyleXs(val: NgStyleType) { this._base.cacheInput('ngStyleXs', val, true); } - @Input('ngStyle.sm') set ngStyleSm(val: NgStyleType) { this._base.cacheInput('ngStyleSm', val, true); }; - @Input('ngStyle.md') set ngStyleMd(val: NgStyleType) { this._base.cacheInput('ngStyleMd', val, true); }; - @Input('ngStyle.lg') set ngStyleLg(val: NgStyleType) { this._base.cacheInput('ngStyleLg', val, true); }; - @Input('ngStyle.xl') set ngStyleXl(val: NgStyleType) { this._base.cacheInput('ngStyleXl', val, true); }; - - @Input('ngStyle.lt-sm') set ngStyleLtSm(val: NgStyleType) { this._base.cacheInput('ngStyleLtSm', val, true); }; - @Input('ngStyle.lt-md') set ngStyleLtMd(val: NgStyleType) { this._base.cacheInput('ngStyleLtMd', val, true); }; - @Input('ngStyle.lt-lg') set ngStyleLtLg(val: NgStyleType) { this._base.cacheInput('ngStyleLtLg', val, true); }; - @Input('ngStyle.lt-xl') set ngStyleLtXl(val: NgStyleType) { this._base.cacheInput('ngStyleLtXl', val, true); }; +export class StyleDirective extends BaseDirective2 implements DoCheck { - @Input('ngStyle.gt-xs') set ngStyleGtXs(val: NgStyleType) { this._base.cacheInput('ngStyleGtXs', val, true); }; - @Input('ngStyle.gt-sm') set ngStyleGtSm(val: NgStyleType) { this._base.cacheInput('ngStyleGtSm', val, true); } ; - @Input('ngStyle.gt-md') set ngStyleGtMd(val: NgStyleType) { this._base.cacheInput('ngStyleGtMd', val, true); }; - @Input('ngStyle.gt-lg') set ngStyleGtLg(val: NgStyleType) { this._base.cacheInput('ngStyleGtLg', val, true); }; - /* tslint:enable */ + protected DIRECTIVE_KEY = 'ngStyle'; - /** - * Constructor for the ngStyle subclass; which adds selectors and - * a MediaQuery Activation Adapter - */ - constructor(private monitor: MediaMonitor, - protected _sanitizer: DomSanitizer, - protected _ngEl: ElementRef, - protected _renderer: Renderer2, - protected _differs: KeyValueDiffers, - @Optional() @Self() private readonly _ngStyleInstance: NgStyle, - protected _styler: StyleUtils) { - super(monitor, _ngEl, _styler); - this._base = new BaseDirectiveAdapter( - 'ngStyle', - this.monitor, - this._ngEl, - this._styler - ); - if (!this._ngStyleInstance) { + constructor(protected elementRef: ElementRef, + protected styler: StyleUtils, + protected marshal: MediaMarshaller, + protected keyValueDiffers: KeyValueDiffers, + protected renderer: Renderer2, + protected sanitizer: DomSanitizer, + @Optional() @Self() private readonly ngStyleInstance: NgStyle) { + super(elementRef, null!, styler, marshal); + if (!this.ngStyleInstance) { // Create an instance NgClass Directive instance only if `ngClass=""` has NOT been // defined on the same host element; since the responsive variations may be defined... - this._ngStyleInstance = new NgStyle(this._differs, this._ngEl, this._renderer); + this.ngStyleInstance = new NgStyle(this.keyValueDiffers, this.elementRef, this.renderer); } - - this._buildCacheInterceptor(); - this._fallbackToStyle(); - } - - // ****************************************************************** - // Lifecycle Hooks - // ****************************************************************** - - /** For @Input changes on the current mq activation property */ - ngOnChanges(changes: SimpleChanges) { - if (this._base.activeKey in changes) { - this._ngStyleInstance.ngStyle = this._base.mqActivation.activatedInput || ''; - } - } - - ngOnInit() { - this._configureMQListener(); - } - - /** For ChangeDetectionStrategy.onPush and ngOnChanges() updates */ - ngDoCheck() { - this._ngStyleInstance.ngDoCheck(); + this.marshal.init(this.nativeElement, this.DIRECTIVE_KEY, this.updateWithValue.bind(this)); + this.setValue(this.nativeElement.getAttribute('style') || '', ''); } - ngOnDestroy() { - this._base.ngOnDestroy(); - } - - // ****************************************************************** - // Internal Methods - // ****************************************************************** - - /** - * Build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - protected _configureMQListener(baseKey = 'ngStyle') { - const fallbackValue = this._base.queryInput(baseKey); - this._base.listenForMediaQueryChanges(baseKey, fallbackValue, (changes: MediaChange) => { - this._ngStyleInstance.ngStyle = changes.value || ''; - this._ngStyleInstance.ngDoCheck(); - }); - } - - // ************************************************************************ - // Private Internal Methods - // ************************************************************************ - - /** Build intercept to convert raw strings to ngStyleMap */ - protected _buildCacheInterceptor() { - let cacheInput = this._base.cacheInput.bind(this._base); - this._base.cacheInput = (key?: string, source?: any, cacheRaw = false, merge = true) => { - let styles = this._buildStyleMap(source); - if (merge) { - styles = extendObject({}, this._base.inputMap['ngStyle'], styles); - } - cacheInput(key, styles, cacheRaw); - }; + protected updateWithValue(value: any) { + const styles = this.buildStyleMap(value); + const defaultStyles = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY, ''); + const fallback = this.buildStyleMap(defaultStyles); + this.ngStyleInstance.ngStyle = {...fallback, ...styles}; + this.ngStyleInstance.ngDoCheck(); } /** @@ -176,11 +67,10 @@ export class StyleDirective extends BaseDirective implements DoCheck, OnChanges, * Comma-delimiters are not supported due to complexities of * possible style values such as `rgba(x,x,x,x)` and others */ - protected _buildStyleMap(styles: NgStyleType) { - let sanitizer: NgStyleSanitizer = (val: any) => { - // Always safe-guard (aka sanitize) style property values - return this._sanitizer.sanitize(SecurityContext.STYLE, val) || ''; - }; + protected buildStyleMap(styles: NgStyleType): NgStyleMap { + // Always safe-guard (aka sanitize) style property values + const sanitizer: NgStyleSanitizer = (val: any) => + this.sanitizer.sanitize(SecurityContext.STYLE, val) || ''; if (styles) { switch (getType(styles)) { case 'string': return buildMapFromList(buildRawList(styles), @@ -191,22 +81,40 @@ export class StyleDirective extends BaseDirective implements DoCheck, OnChanges, } } - return styles; + return {}; } - /** Initial lookup of raw 'class' value (if any) */ - protected _fallbackToStyle() { - if (!this._base.queryInput('ngStyle')) { - this.ngStyleBase = this._getAttributeValue('style') || ''; - } + // ****************************************************************** + // Lifecycle Hooks + // ****************************************************************** + + /** For ChangeDetectionStrategy.onPush and ngOnChanges() updates */ + ngDoCheck() { + this.ngStyleInstance.ngDoCheck(); } +} - /** - * Special adapter to cross-cut responsive behaviors - * into the StyleDirective - */ - protected _base: BaseDirectiveAdapter; +const inputs = [ + 'ngStyle', + 'ngStyle.xs', 'ngStyle.sm', 'ngStyle.md', 'ngStyle.lg', 'ngStyle.xl', + 'ngStyle.lt-sm', 'ngStyle.lt-md', 'ngStyle.lt-lg', 'ngStyle.lt-xl', + 'ngStyle.gt-xs', 'ngStyle.gt-sm', 'ngStyle.gt-md', 'ngStyle.gt-lg' +]; + +const selector = ` + [ngStyle], + [ngStyle.xs], [ngStyle.sm], [ngStyle.md], [ngStyle.lg], [ngStyle.xl], + [ngStyle.lt-sm], [ngStyle.lt-md], [ngStyle.lt-lg], [ngStyle.lt-xl], + [ngStyle.gt-xs], [ngStyle.gt-sm], [ngStyle.gt-md], [ngStyle.gt-lg] +`; +/** + * Directive to add responsive support for ngStyle. + * + */ +@Directive({selector, inputs}) +export class DefaultStyleDirective extends StyleDirective implements DoCheck { + protected inputs = inputs; } /** Build a styles map from a list of styles, while sanitizing bad values first */ diff --git a/src/lib/flex/flex-align/flex-align.ts b/src/lib/flex/flex-align/flex-align.ts index a41865aa2..0050d92e0 100644 --- a/src/lib/flex/flex-align/flex-align.ts +++ b/src/lib/flex/flex-align/flex-align.ts @@ -5,20 +5,10 @@ * 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 {Directive, ElementRef, Injectable, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, - Injectable, -} from '@angular/core'; -import { - BaseDirective, - MediaChange, - MediaMonitor, + MediaMarshaller, + BaseDirective2, StyleBuilder, StyleDefinition, StyleUtils, @@ -27,6 +17,7 @@ import { @Injectable({providedIn: 'root'}) export class FlexAlignStyleBuilder extends StyleBuilder { buildStyles(input: string) { + input = input || 'stretch'; const styles: StyleDefinition = {}; // Cross-axis @@ -46,87 +37,45 @@ export class FlexAlignStyleBuilder extends StyleBuilder { } } +const inputs = [ + 'fxFlexAlign', 'fxFlexAlign.xs', 'fxFlexAlign.sm', 'fxFlexAlign.md', + 'fxFlexAlign.lg', 'fxFlexAlign.xl', 'fxFlexAlign.lt-sm', 'fxFlexAlign.lt-md', + 'fxFlexAlign.lt-lg', 'fxFlexAlign.lt-xl', 'fxFlexAlign.gt-xs', 'fxFlexAlign.gt-sm', + 'fxFlexAlign.gt-md', 'fxFlexAlign.gt-lg' +]; +const selector = ` + [fxFlexAlign], [fxFlexAlign.xs], [fxFlexAlign.sm], [fxFlexAlign.md], + [fxFlexAlign.lg], [fxFlexAlign.xl], [fxFlexAlign.lt-sm], [fxFlexAlign.lt-md], + [fxFlexAlign.lt-lg], [fxFlexAlign.lt-xl], [fxFlexAlign.gt-xs], [fxFlexAlign.gt-sm], + [fxFlexAlign.gt-md], [fxFlexAlign.gt-lg] +`; + /** * 'flex-align' flexbox styling directive * Allows element-specific overrides for cross-axis alignments in a layout container * @see https://css-tricks.com/almanac/properties/a/align-self/ */ -@Directive({ - selector: ` - [fxFlexAlign], - [fxFlexAlign.xs], [fxFlexAlign.sm], [fxFlexAlign.md], [fxFlexAlign.lg], [fxFlexAlign.xl], - [fxFlexAlign.lt-sm], [fxFlexAlign.lt-md], [fxFlexAlign.lt-lg], [fxFlexAlign.lt-xl], - [fxFlexAlign.gt-xs], [fxFlexAlign.gt-sm], [fxFlexAlign.gt-md], [fxFlexAlign.gt-lg] -` -}) -export class FlexAlignDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('fxFlexAlign') set align(val: string) { this._cacheInput('align', val); }; - @Input('fxFlexAlign.xs') set alignXs(val: string) { this._cacheInput('alignXs', val); }; - @Input('fxFlexAlign.sm') set alignSm(val: string) { this._cacheInput('alignSm', val); }; - @Input('fxFlexAlign.md') set alignMd(val: string) { this._cacheInput('alignMd', val); }; - @Input('fxFlexAlign.lg') set alignLg(val: string) { this._cacheInput('alignLg', val); }; - @Input('fxFlexAlign.xl') set alignXl(val: string) { this._cacheInput('alignXl', val); }; - - @Input('fxFlexAlign.lt-sm') set alignLtSm(val: string) { this._cacheInput('alignLtSm', val); }; - @Input('fxFlexAlign.lt-md') set alignLtMd(val: string) { this._cacheInput('alignLtMd', val); }; - @Input('fxFlexAlign.lt-lg') set alignLtLg(val: string) { this._cacheInput('alignLtLg', val); }; - @Input('fxFlexAlign.lt-xl') set alignLtXl(val: string) { this._cacheInput('alignLtXl', val); }; - - @Input('fxFlexAlign.gt-xs') set alignGtXs(val: string) { this._cacheInput('alignGtXs', val); }; - @Input('fxFlexAlign.gt-sm') set alignGtSm(val: string) { this._cacheInput('alignGtSm', val); }; - @Input('fxFlexAlign.gt-md') set alignGtMd(val: string) { this._cacheInput('alignGtMd', val); }; - @Input('fxFlexAlign.gt-lg') set alignGtLg(val: string) { this._cacheInput('alignGtLg', val); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils, - styleBuilder: FlexAlignStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); - } - - - // ********************************************* - // Lifecycle Methods - // ********************************************* - - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['align'] != null || this._mqActivation) { - this._updateWithValue(); - } +export class FlexAlignDirective extends BaseDirective2 { + + protected DIRECTIVE_KEY = 'flex-align'; + + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: FlexAlignStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.addStyles.bind(this)); } - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges('align', 'stretch', (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - } - - // ********************************************* - // Protected methods - // ********************************************* - - protected _updateWithValue(value?: string|number) { - value = value || this._queryInput('align') || 'stretch'; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this.addStyles(value && (value + '') || ''); - } - - protected _styleCache = flexAlignCache; + protected styleCache = flexAlignCache; } const flexAlignCache: Map = new Map(); + +@Directive({selector, inputs}) +export class DefaultFlexAlignDirective extends FlexAlignDirective { + protected inputs = inputs; +} diff --git a/src/lib/flex/flex-fill/flex-fill.ts b/src/lib/flex/flex-fill/flex-fill.ts index 568f460fe..dd8698513 100644 --- a/src/lib/flex/flex-fill/flex-fill.ts +++ b/src/lib/flex/flex-fill/flex-fill.ts @@ -7,11 +7,11 @@ */ import {Directive, ElementRef, Injectable} from '@angular/core'; import { - BaseDirective, - MediaMonitor, + BaseDirective2, StyleBuilder, StyleDefinition, StyleUtils, + MediaMarshaller, } from '@angular/flex-layout/core'; const FLEX_FILL_CSS = { @@ -35,20 +35,17 @@ export class FlexFillStyleBuilder extends StyleBuilder { * * NOTE: fxFill is NOT responsive API!! */ -@Directive({selector: ` - [fxFill], - [fxFlexFill] -`}) -export class FlexFillDirective extends BaseDirective { - constructor(monitor: MediaMonitor, - public elRef: ElementRef, - styleUtils: StyleUtils, - styleBuilder: FlexFillStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); +@Directive({selector: `[fxFill], [fxFlexFill]`}) +export class FlexFillDirective extends BaseDirective2 { + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + protected styleBuilder: FlexFillStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); this.addStyles(''); } - protected _styleCache = flexFillCache; + protected styleCache = flexFillCache; } const flexFillCache: Map = new Map(); diff --git a/src/lib/flex/flex-offset/flex-offset.spec.ts b/src/lib/flex/flex-offset/flex-offset.spec.ts index 342fa0d4c..ee71d76b2 100644 --- a/src/lib/flex/flex-offset/flex-offset.spec.ts +++ b/src/lib/flex/flex-offset/flex-offset.spec.ts @@ -224,6 +224,7 @@ describe('flex-offset directive', () => { @Injectable({providedIn: FlexModule}) export class MockFlexOffsetStyleBuilder extends StyleBuilder { + shouldCache = false; buildStyles(_input: string) { return {'margin-top': '10px'}; } diff --git a/src/lib/flex/flex-offset/flex-offset.ts b/src/lib/flex/flex-offset/flex-offset.ts index d1a540ca8..3a79a4f62 100644 --- a/src/lib/flex/flex-offset/flex-offset.ts +++ b/src/lib/flex/flex-offset/flex-offset.ts @@ -8,27 +8,20 @@ import { Directive, ElementRef, - Input, - OnInit, OnChanges, - OnDestroy, Optional, - SimpleChanges, - SkipSelf, Injectable, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { - BaseDirective, - MediaChange, - MediaMonitor, + MediaMarshaller, + BaseDirective2, StyleBuilder, StyleDefinition, StyleUtils, } from '@angular/flex-layout/core'; -import {Subscription} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; -import {Layout, LayoutDirective} from '../layout/layout'; import {isFlowHorizontal} from '../../utils/layout-validator'; export interface FlexOffsetParent { @@ -39,6 +32,9 @@ export interface FlexOffsetParent { @Injectable({providedIn: 'root'}) export class FlexOffsetStyleBuilder extends StyleBuilder { buildStyles(offset: string, parent: FlexOffsetParent) { + if (offset === '') { + offset = '0'; + } const isPercent = String(offset).indexOf('%') > -1; const isPx = String(offset).indexOf('px') > -1; if (!isPx && !isPercent && !isNaN(+offset)) { @@ -52,153 +48,74 @@ export class FlexOffsetStyleBuilder extends StyleBuilder { } } +const inputs = [ + 'fxFlexOffset', 'fxFlexOffset.xs', 'fxFlexOffset.sm', 'fxFlexOffset.md', + 'fxFlexOffset.lg', 'fxFlexOffset.xl', 'fxFlexOffset.lt-sm', 'fxFlexOffset.lt-md', + 'fxFlexOffset.lt-lg', 'fxFlexOffset.lt-xl', 'fxFlexOffset.gt-xs', 'fxFlexOffset.gt-sm', + 'fxFlexOffset.gt-md', 'fxFlexOffset.gt-lg' +]; +const selector = ` + [fxFlexOffset], [fxFlexOffset.xs], [fxFlexOffset.sm], [fxFlexOffset.md], + [fxFlexOffset.lg], [fxFlexOffset.xl], [fxFlexOffset.lt-sm], [fxFlexOffset.lt-md], + [fxFlexOffset.lt-lg], [fxFlexOffset.lt-xl], [fxFlexOffset.gt-xs], [fxFlexOffset.gt-sm], + [fxFlexOffset.gt-md], [fxFlexOffset.gt-lg] +`; + /** * 'flex-offset' flexbox styling directive * Configures the 'margin-left' of the element in a layout container */ -@Directive({selector: ` - [fxFlexOffset], - [fxFlexOffset.xs], [fxFlexOffset.sm], [fxFlexOffset.md], [fxFlexOffset.lg], [fxFlexOffset.xl], - [fxFlexOffset.lt-sm], [fxFlexOffset.lt-md], [fxFlexOffset.lt-lg], [fxFlexOffset.lt-xl], - [fxFlexOffset.gt-xs], [fxFlexOffset.gt-sm], [fxFlexOffset.gt-md], [fxFlexOffset.gt-lg] -`}) -export class FlexOffsetDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - private _directionWatcher: Subscription; - - /* tslint:disable */ - @Input('fxFlexOffset') set offset(val: string) { this._cacheInput('offset', val); } - @Input('fxFlexOffset.xs') set offsetXs(val: string) { this._cacheInput('offsetXs', val); } - @Input('fxFlexOffset.sm') set offsetSm(val: string) { this._cacheInput('offsetSm', val); }; - @Input('fxFlexOffset.md') set offsetMd(val: string) { this._cacheInput('offsetMd', val); }; - @Input('fxFlexOffset.lg') set offsetLg(val: string) { this._cacheInput('offsetLg', val); }; - @Input('fxFlexOffset.xl') set offsetXl(val: string) { this._cacheInput('offsetXl', val); }; - - @Input('fxFlexOffset.lt-sm') set offsetLtSm(val: string) { this._cacheInput('offsetLtSm', val); }; - @Input('fxFlexOffset.lt-md') set offsetLtMd(val: string) { this._cacheInput('offsetLtMd', val); }; - @Input('fxFlexOffset.lt-lg') set offsetLtLg(val: string) { this._cacheInput('offsetLtLg', val); }; - @Input('fxFlexOffset.lt-xl') set offsetLtXl(val: string) { this._cacheInput('offsetLtXl', val); }; - - @Input('fxFlexOffset.gt-xs') set offsetGtXs(val: string) { this._cacheInput('offsetGtXs', val); }; - @Input('fxFlexOffset.gt-sm') set offsetGtSm(val: string) { this._cacheInput('offsetGtSm', val); }; - @Input('fxFlexOffset.gt-md') set offsetGtMd(val: string) { this._cacheInput('offsetGtMd', val); }; - @Input('fxFlexOffset.gt-lg') set offsetGtLg(val: string) { this._cacheInput('offsetGtLg', val); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - @Optional() @SkipSelf() protected _container: LayoutDirective, - private _directionality: Directionality, - styleUtils: StyleUtils, - styleBuilder: FlexOffsetStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); - - this._directionWatcher = - this._directionality.change.subscribe(this._updateWithValue.bind(this)); - - this.watchParentFlow(); - } - - // ********************************************* - // Lifecycle Methods - // ********************************************* - - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['offset'] != null || this._mqActivation) { - this._updateWithValue(); +export class FlexOffsetDirective extends BaseDirective2 implements OnChanges { + protected DIRECTIVE_KEY = 'flex-offset'; + + constructor(protected elRef: ElementRef, + protected directionality: Directionality, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: FlexOffsetStyleBuilder, + protected marshal: MediaMarshaller, + protected styler: StyleUtils) { + super(elRef, styleBuilder, styler, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this), [this.directionality.change]); + if (this.parentElement) { + this.marshal.trackValue(this.parentElement, 'layout-gap') + .pipe(takeUntil(this.destroySubject)) + .subscribe(this.triggerUpdate.bind(this)); } } - /** - * Cleanup - */ - ngOnDestroy() { - super.ngOnDestroy(); - if (this._layoutWatcher) { - this._layoutWatcher.unsubscribe(); - } - if (this._directionWatcher) { - this._directionWatcher.unsubscribe(); - } - } - - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges('offset', 0 , (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - } - // ********************************************* // Protected methods // ********************************************* - /** The flex-direction of this element's host container. Defaults to 'row'. */ - protected _layout = {direction: 'row', wrap: false}; - - /** - * Subscription to the parent flex container's layout changes. - * Stored so we can unsubscribe when this directive is destroyed. - */ - protected _layoutWatcher?: Subscription; - - /** - * If parent flow-direction changes, then update the margin property - * used to offset - */ - protected watchParentFlow() { - if (this._container) { - // Subscribe to layout immediate parent direction changes (if any) - this._layoutWatcher = this._container.layout$.subscribe((layout) => { - // `direction` === null if parent container does not have a `fxLayout` - this._onLayoutChange(layout); - }); - } - } - - /** - * Caches the parent container's 'flex-direction' and updates the element's style. - * Used as a handler for layout change events from the parent flex container. - */ - protected _onLayoutChange(layout?: Layout) { - this._layout = layout || this._layout || {direction: 'row', wrap: false}; - this._updateWithValue(); - } - /** * Using the current fxFlexOffset value, update the inline CSS * NOTE: this will assign `margin-left` if the parent flex-direction == 'row', * otherwise `margin-top` is used for the offset. */ - protected _updateWithValue(value?: string|number) { - value = value || this._queryInput('offset') || 0; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - + protected updateWithValue(value: string|number = ''): void { // The flex-direction of this element's flex container. Defaults to 'row'. - const layout = this._getFlexFlowDirection(this.parentElement, true); - const isRtl = this._directionality.value === 'rtl'; + const layout = this.getFlexFlowDirection(this.parentElement!, true); + const isRtl = this.directionality.value === 'rtl'; if (layout === 'row' && isRtl) { - this._styleCache = flexOffsetCacheRowRtl; + this.styleCache = flexOffsetCacheRowRtl; } else if (layout === 'row' && !isRtl) { - this._styleCache = flexOffsetCacheRowLtr; + this.styleCache = flexOffsetCacheRowLtr; } else if (layout === 'column' && isRtl) { - this._styleCache = flexOffsetCacheColumnRtl; + this.styleCache = flexOffsetCacheColumnRtl; } else if (layout === 'column' && !isRtl) { - this._styleCache = flexOffsetCacheColumnLtr; + this.styleCache = flexOffsetCacheColumnLtr; } - this.addStyles((value && (value + '') || ''), {layout, isRtl}); + this.addStyles(value + '', {layout, isRtl}); } } +@Directive({selector, inputs}) +export class DefaultFlexOffsetDirective extends FlexOffsetDirective { + protected inputs = inputs; +} + const flexOffsetCacheRowRtl: Map = new Map(); const flexOffsetCacheColumnRtl: Map = new Map(); const flexOffsetCacheRowLtr: Map = new Map(); diff --git a/src/lib/flex/flex-order/flex-order.ts b/src/lib/flex/flex-order/flex-order.ts index 644a8c784..0a7c8a4cd 100644 --- a/src/lib/flex/flex-order/flex-order.ts +++ b/src/lib/flex/flex-order/flex-order.ts @@ -5,112 +5,62 @@ * 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 {Directive, ElementRef, OnChanges, Injectable, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, - Injectable, -} from '@angular/core'; -import { - BaseDirective, - MediaChange, - MediaMonitor, + BaseDirective2, StyleBuilder, StyleDefinition, StyleUtils, + MediaMarshaller, } from '@angular/flex-layout/core'; @Injectable({providedIn: 'root'}) export class FlexOrderStyleBuilder extends StyleBuilder { buildStyles(value: string) { - const val = parseInt(value, 10); - const styles = {order: isNaN(val) ? 0 : val}; - return styles; + const val = parseInt((value || '0'), 10); + return {order: isNaN(val) ? 0 : val}; } } +const inputs = [ + 'fxFlexOrder', 'fxFlexOrder.xs', 'fxFlexOrder.sm', 'fxFlexOrder.md', + 'fxFlexOrder.lg', 'fxFlexOrder.xl', 'fxFlexOrder.lt-sm', 'fxFlexOrder.lt-md', + 'fxFlexOrder.lt-lg', 'fxFlexOrder.lt-xl', 'fxFlexOrder.gt-xs', 'fxFlexOrder.gt-sm', + 'fxFlexOrder.gt-md', 'fxFlexOrder.gt-lg' +]; +const selector = ` + [fxFlexOrder], [fxFlexOrder.xs], [fxFlexOrder.sm], [fxFlexOrder.md], + [fxFlexOrder.lg], [fxFlexOrder.xl], [fxFlexOrder.lt-sm], [fxFlexOrder.lt-md], + [fxFlexOrder.lt-lg], [fxFlexOrder.lt-xl], [fxFlexOrder.gt-xs], [fxFlexOrder.gt-sm], + [fxFlexOrder.gt-md], [fxFlexOrder.gt-lg] +`; + /** * 'flex-order' flexbox styling directive * Configures the positional ordering of the element in a sorted layout container * @see https://css-tricks.com/almanac/properties/o/order/ */ -@Directive({selector: ` - [fxFlexOrder], - [fxFlexOrder.xs], [fxFlexOrder.sm], [fxFlexOrder.md], [fxFlexOrder.lg], [fxFlexOrder.xl], - [fxFlexOrder.lt-sm], [fxFlexOrder.lt-md], [fxFlexOrder.lt-lg], [fxFlexOrder.lt-xl], - [fxFlexOrder.gt-xs], [fxFlexOrder.gt-sm], [fxFlexOrder.gt-md], [fxFlexOrder.gt-lg] -`}) -export class FlexOrderDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('fxFlexOrder') set order(val: string) { this._cacheInput('order', val); } - @Input('fxFlexOrder.xs') set orderXs(val: string) { this._cacheInput('orderXs', val); } - @Input('fxFlexOrder.sm') set orderSm(val: string) { this._cacheInput('orderSm', val); }; - @Input('fxFlexOrder.md') set orderMd(val: string) { this._cacheInput('orderMd', val); }; - @Input('fxFlexOrder.lg') set orderLg(val: string) { this._cacheInput('orderLg', val); }; - @Input('fxFlexOrder.xl') set orderXl(val: string) { this._cacheInput('orderXl', val); }; - - @Input('fxFlexOrder.gt-xs') set orderGtXs(val: string) { this._cacheInput('orderGtXs', val); }; - @Input('fxFlexOrder.gt-sm') set orderGtSm(val: string) { this._cacheInput('orderGtSm', val); }; - @Input('fxFlexOrder.gt-md') set orderGtMd(val: string) { this._cacheInput('orderGtMd', val); }; - @Input('fxFlexOrder.gt-lg') set orderGtLg(val: string) { this._cacheInput('orderGtLg', val); }; - - @Input('fxFlexOrder.lt-sm') set orderLtSm(val: string) { this._cacheInput('orderLtSm', val); }; - @Input('fxFlexOrder.lt-md') set orderLtMd(val: string) { this._cacheInput('orderLtMd', val); }; - @Input('fxFlexOrder.lt-lg') set orderLtLg(val: string) { this._cacheInput('orderLtLg', val); }; - @Input('fxFlexOrder.lt-xl') set orderLtXl(val: string) { this._cacheInput('orderLtXl', val); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils, - styleBuilder: FlexOrderStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); - } - - // ********************************************* - // Lifecycle Methods - // ********************************************* +export class FlexOrderDirective extends BaseDirective2 implements OnChanges { - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['order'] != null || this._mqActivation) { - this._updateWithValue(); - } - } - - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges('order', '0', (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - } + protected DIRECTIVE_KEY = 'flex-order'; - // ********************************************* - // Protected methods - // ********************************************* - - protected _updateWithValue(value?: string) { - value = value || this._queryInput('order') || '0'; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this.addStyles(value || ''); + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: FlexOrderStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.addStyles.bind(this)); } - protected _styleCache = flexOrderCache; + protected styleCache = flexOrderCache; } const flexOrderCache: Map = new Map(); + +@Directive({selector, inputs}) +export class DefaultFlexOrderDirective extends FlexOrderDirective { + protected inputs = inputs; +} diff --git a/src/lib/flex/flex/flex.spec.ts b/src/lib/flex/flex/flex.spec.ts index e47747934..bb1359e10 100644 --- a/src/lib/flex/flex/flex.spec.ts +++ b/src/lib/flex/flex/flex.spec.ts @@ -18,8 +18,8 @@ import { } from '@angular/flex-layout/core'; import {FlexLayoutModule} from '../../module'; -import {FlexDirective, FlexStyleBuilder} from './flex'; -import {LayoutDirective} from '../layout/layout'; +import {DefaultFlexDirective, FlexStyleBuilder} from './flex'; +import {DefaultLayoutDirective} from '../layout/layout'; import {customMatchers, expect} from '../../utils/testing/custom-matchers'; import { makeCreateTestComponent, @@ -103,6 +103,77 @@ describe('flex directive', () => { expectEl(element).toHaveStyle({'flex': '10 1 auto'}, styler); }); + it('should add correct styles for `fxFlex` with multiple layout changes and wraps', () => { + componentWithTemplate(` +
+
+
+ `); + fixture.detectChanges(); + let element = queryFor(fixture, '[fxFlex]')[0]; + expectNativeEl(fixture).toHaveStyle({'flex-direction': 'column'}, styler); + expectEl(element).toHaveStyle({'box-sizing': 'border-box'}, styler); + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + expectEl(element).toHaveStyle({'max-height': '30%'}, styler); + + fixture.debugElement.componentInstance.direction = 'row'; + fixture.detectChanges(); + + expectNativeEl(fixture).toHaveStyle({'flex-direction': 'row'}, styler); + expectEl(element).toHaveStyle({'box-sizing': 'border-box'}, styler); + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + expectEl(element).toHaveStyle({'max-width': '30%'}, styler); + + fixture.debugElement.componentInstance.direction = 'column'; + fixture.detectChanges(); + + expectNativeEl(fixture).toHaveStyle({'flex-direction': 'column'}, styler); + expectEl(element).toHaveStyle({'box-sizing': 'border-box'}, styler); + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + expectEl(element).toHaveStyle({'max-height': '30%'}, styler); + }); + + it('should add correct styles for `fxFlex` with gap in grid mode and wrap parent', () => { + componentWithTemplate(` +
+
+
+
+
+ `); + fixture.debugElement.componentInstance.direction = 'row'; + fixture.detectChanges(); + let element = queryFor(fixture, '[fxFlex]')[0]; + expectNativeEl(fixture).toHaveStyle({'flex-direction': 'row'}, styler); + expectEl(element).toHaveStyle({'box-sizing': 'border-box'}, styler); + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + expectEl(element).toHaveStyle({'max-width': '30%'}, styler); + + fixture.debugElement.componentInstance.direction = 'row-reverse'; + fixture.detectChanges(); + + expectNativeEl(fixture).toHaveStyle({'flex-direction': 'row-reverse'}, styler); + expectEl(element).toHaveStyle({'box-sizing': 'border-box'}, styler); + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + expectEl(element).toHaveStyle({'max-width': '30%'}, styler); + + fixture.debugElement.componentInstance.direction = 'column'; + fixture.detectChanges(); + + expectNativeEl(fixture).toHaveStyle({'flex-direction': 'column'}, styler); + expectEl(element).toHaveStyle({'box-sizing': 'border-box'}, styler); + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + expectEl(element).toHaveStyle({'max-height': '30%'}, styler); + + fixture.debugElement.componentInstance.direction = 'column-reverse'; + fixture.detectChanges(); + + expectNativeEl(fixture).toHaveStyle({'flex-direction': 'column-reverse'}, styler); + expectEl(element).toHaveStyle({'box-sizing': 'border-box'}, styler); + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + expectEl(element).toHaveStyle({'max-height': '30%'}, styler); + }); + it('should add correct styles for `fxFlex` and ngStyle with multiple layout changes', () => { // NOTE: the presence of ngIf on the child element is imperative for detecting 700 componentWithTemplate(` @@ -699,7 +770,7 @@ describe('flex directive', () => { fixture = TestBed.createComponent(TestQueryWithFlexComponent); fixture.detectChanges(); - const layout: LayoutDirective = fixture.debugElement.componentInstance.layout; + const layout: DefaultLayoutDirective = fixture.debugElement.componentInstance.layout; expect(layout).toBeDefined(); expect(layout.activatedValue).toBe(''); @@ -714,13 +785,12 @@ describe('flex directive', () => { }, _styler); }) ); - it('should query the ViewChild `fxFlex` directive properly', inject([StyleUtils], (_styler: StyleUtils) => { fixture = TestBed.createComponent(TestQueryWithFlexComponent); fixture.detectChanges(); - const flex: FlexDirective = fixture.debugElement.componentInstance.flex; + const flex: DefaultFlexDirective = fixture.debugElement.componentInstance.flex; // Test for percentage value assignments expect(flex).toBeDefined(); @@ -747,14 +817,13 @@ describe('flex directive', () => { expectEl(nodes[0]).toHaveStyle({'max-width': '27.5px'}, _styler); }) ); - it('should restore `fxFlex` value after breakpoint activations', inject([MatchMedia, StyleUtils], (_matchMedia: MockMatchMedia, _styler: StyleUtils) => { fixture = TestBed.createComponent(TestQueryWithFlexComponent); fixture.detectChanges(); - const flex: FlexDirective = fixture.debugElement.componentInstance.flex; + const flex: DefaultFlexDirective = fixture.debugElement.componentInstance.flex; // Test for raw value assignments that are converted to percentages expect(flex).toBeDefined(); @@ -940,6 +1009,7 @@ describe('flex directive', () => { @Injectable() export class MockFlexStyleBuilder extends StyleBuilder { + shouldCache = false; buildStyles(_input: string) { return {'flex': '1 1 30%'}; } @@ -967,6 +1037,6 @@ class TestFlexComponent { ` }) class TestQueryWithFlexComponent { - @ViewChild(FlexDirective) flex!: FlexDirective; - @ViewChild(LayoutDirective) layout!: LayoutDirective; + @ViewChild(DefaultFlexDirective) flex!: DefaultFlexDirective; + @ViewChild(DefaultLayoutDirective) layout!: DefaultLayoutDirective; } diff --git a/src/lib/flex/flex/flex.ts b/src/lib/flex/flex/flex.ts index 3d9c5735f..6577599ad 100644 --- a/src/lib/flex/flex/flex.ts +++ b/src/lib/flex/flex/flex.ts @@ -5,39 +5,23 @@ * 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 {Directive, ElementRef, Inject, Injectable, Input} from '@angular/core'; import { - Directive, - ElementRef, - Inject, - Injectable, - Input, - OnChanges, - OnDestroy, - OnInit, - Optional, - SimpleChanges, - SkipSelf, -} from '@angular/core'; -import { - BaseDirective, + BaseDirective2, LayoutConfigOptions, LAYOUT_CONFIG, - MediaChange, - MediaMonitor, StyleUtils, validateBasis, StyleBuilder, StyleDefinition, + MediaMarshaller, + ElementMatcher, } from '@angular/flex-layout/core'; -import {Subscription} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; import {extendObject} from '../../utils/object-extend'; -import {Layout, LayoutDirective} from '../layout/layout'; import {isFlowHorizontal} from '../../utils/layout-validator'; -/** Built-in aliases for different flex-basis values. */ -export type FlexBasisAlias = 'grow' | 'initial' | 'auto' | 'none' | 'nogrow' | 'noshrink'; - interface FlexBuilderParent { direction: string; hasWrap: boolean; @@ -196,102 +180,61 @@ export class FlexStyleBuilder extends StyleBuilder { } } +const inputs = [ + 'fxFlex', 'fxFlex.xs', 'fxFlex.sm', 'fxFlex.md', + 'fxFlex.lg', 'fxFlex.xl', 'fxFlex.lt-sm', 'fxFlex.lt-md', + 'fxFlex.lt-lg', 'fxFlex.lt-xl', 'fxFlex.gt-xs', 'fxFlex.gt-sm', + 'fxFlex.gt-md', 'fxFlex.gt-lg' +]; +const selector = ` + [fxFlex], [fxFlex.xs], [fxFlex.sm], [fxFlex.md], + [fxFlex.lg], [fxFlex.xl], [fxFlex.lt-sm], [fxFlex.lt-md], + [fxFlex.lt-lg], [fxFlex.lt-xl], [fxFlex.gt-xs], [fxFlex.gt-sm], + [fxFlex.gt-md], [fxFlex.gt-lg] +`; + /** * Directive to control the size of a flex item using flex-basis, flex-grow, and flex-shrink. * Corresponds to the css `flex` shorthand property. * * @see https://css-tricks.com/snippets/css/a-guide-to-flexbox/ */ -@Directive({ - selector: ` - [fxFlex], - [fxFlex.xs], [fxFlex.sm], [fxFlex.md], [fxFlex.lg], [fxFlex.xl], - [fxFlex.lt-sm], [fxFlex.lt-md], [fxFlex.lt-lg], [fxFlex.lt-xl], - [fxFlex.gt-xs], [fxFlex.gt-sm], [fxFlex.gt-md], [fxFlex.gt-lg], - `, -}) -export class FlexDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /** The flex-direction of this element's flex container. Defaults to 'row'. */ - protected _layout?: Layout; +export class FlexDirective extends BaseDirective2 { - /** - * Subscription to the parent flex container's layout changes. - * Stored so we can unsubscribe when this directive is destroyed. - */ - protected _layoutWatcher?: Subscription; - - /* tslint:disable */ - @Input('fxShrink') set shrink(val: string) { this._cacheInput('shrink', val); }; - @Input('fxGrow') set grow(val: string) { this._cacheInput('grow', val); }; - - @Input('fxFlex') set flex(val: string) { this._cacheInput('flex', val); }; - @Input('fxFlex.xs') set flexXs(val: string) { this._cacheInput('flexXs', val); }; - @Input('fxFlex.sm') set flexSm(val: string) { this._cacheInput('flexSm', val); }; - @Input('fxFlex.md') set flexMd(val: string) { this._cacheInput('flexMd', val); }; - @Input('fxFlex.lg') set flexLg(val: string) { this._cacheInput('flexLg', val); }; - @Input('fxFlex.xl') set flexXl(val: string) { this._cacheInput('flexXl', val); }; - - @Input('fxFlex.gt-xs') set flexGtXs(val: string) { this._cacheInput('flexGtXs', val); }; - @Input('fxFlex.gt-sm') set flexGtSm(val: string) { this._cacheInput('flexGtSm', val); }; - @Input('fxFlex.gt-md') set flexGtMd(val: string) { this._cacheInput('flexGtMd', val); }; - @Input('fxFlex.gt-lg') set flexGtLg(val: string) { this._cacheInput('flexGtLg', val); }; - - @Input('fxFlex.lt-sm') set flexLtSm(val: string) { this._cacheInput('flexLtSm', val); }; - @Input('fxFlex.lt-md') set flexLtMd(val: string) { this._cacheInput('flexLtMd', val); }; - @Input('fxFlex.lt-lg') set flexLtLg(val: string) { this._cacheInput('flexLtLg', val); }; - @Input('fxFlex.lt-xl') set flexLtXl(val: string) { this._cacheInput('flexLtXl', val); }; - /* tslint:enable */ - - // Note: Explicitly @SkipSelf on LayoutDirective because we are looking - // for the parent flex container for this flex item. - constructor(monitor: MediaMonitor, - elRef: ElementRef, - @Optional() @SkipSelf() protected _container: LayoutDirective, - protected styleUtils: StyleUtils, - @Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions, - protected styleBuilder: FlexStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); + protected DIRECTIVE_KEY = 'flex'; + protected direction = ''; + protected wrap = false; - this._cacheInput('flex', ''); - this._cacheInput('shrink', 1); - this._cacheInput('grow', 1); - } - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['flex'] != null || this._mqActivation) { - this._updateStyle(); - } + @Input('fxShrink') + get shrink(): string { return this.flexShrink; } + set shrink(value: string) { + this.flexShrink = value || '1'; + this.triggerReflow(); } - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges('flex', '', (changes: MediaChange) => { - this._updateStyle(changes.value); - }); - - if (this._container) { - // If this flex item is inside of a flex container marked with - // Subscribe to layout immediate parent direction changes - this._layoutWatcher = this._container.layout$.subscribe((layout) => { - // `direction` === null if parent container does not have a `fxLayout` - this._onLayoutChange(layout); - }); - } + @Input('fxGrow') + get grow(): string { return this.flexGrow; } + set grow(value: string) { + this.flexGrow = value || '1'; + this.triggerReflow(); } - ngOnDestroy() { - super.ngOnDestroy(); - if (this._layoutWatcher) { - this._layoutWatcher.unsubscribe(); + protected flexGrow = '1'; + protected flexShrink = '1'; + + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + @Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions, + protected styleBuilder: FlexStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.updateStyle.bind(this)); + if (this.parentElement) { + this.marshal.trackValue(this.parentElement, 'layout') + .pipe(takeUntil(this.destroySubject)) + .subscribe(this.onLayoutChange.bind(this)); } } @@ -299,33 +242,47 @@ export class FlexDirective extends BaseDirective implements OnInit, OnChanges, O * Caches the parent container's 'flex-direction' and updates the element's style. * Used as a handler for layout change events from the parent flex container. */ - protected _onLayoutChange(layout?: Layout) { - this._layout = layout || this._layout || {direction: 'row', wrap: false}; - this._updateStyle(); + protected onLayoutChange(matcher: ElementMatcher) { + const layout: string = matcher.value; + const layoutParts = layout.split(' '); + this.direction = layoutParts[0]; + this.wrap = layoutParts[1] !== undefined && layoutParts[1] === 'wrap'; + this.triggerUpdate(); } - protected _updateStyle(value?: string|number) { - let flexBasis = value || this._queryInput('flex') || ''; - if (this._mqActivation) { - flexBasis = this._mqActivation.activatedInput; - } - - const basis = String(flexBasis).replace(';', ''); - const parts = validateBasis(basis, this._queryInput('grow'), this._queryInput('shrink')); + /** Input to this is exclusively the basis input value */ + protected updateStyle(value: string) { const addFlexToParent = this.layoutConfig.addFlexToParent !== false; - const direction = this._getFlexFlowDirection(this.parentElement, addFlexToParent); - const hasWrap = this._layout && this._layout.wrap; - if (direction === 'row' && hasWrap) { - this._styleCache = flexRowWrapCache; - } else if (direction === 'row' && !hasWrap) { - this._styleCache = flexRowCache; - } else if (direction === 'column' && hasWrap) { - this._styleCache = flexColumnWrapCache; - } else if (direction === 'column' && !hasWrap) { - this._styleCache = flexColumnCache; + if (!this.direction) { + this.direction = this.getFlexFlowDirection(this.parentElement!, addFlexToParent); + } + const direction = this.direction; + const isHorizontal = direction.startsWith('row'); + const hasWrap = this.wrap; + if (isHorizontal && hasWrap) { + this.styleCache = flexRowWrapCache; + } else if (isHorizontal && !hasWrap) { + this.styleCache = flexRowCache; + } else if (!isHorizontal && hasWrap) { + this.styleCache = flexColumnWrapCache; + } else if (!isHorizontal && !hasWrap) { + this.styleCache = flexColumnCache; } + const basis = String(value).replace(';', ''); + const parts = validateBasis(basis, this.flexGrow, this.flexShrink); this.addStyles(parts.join(' '), {direction, hasWrap}); } + + /** Trigger a style reflow, usually based on a shrink/grow input event */ + protected triggerReflow() { + const parts = validateBasis(this.activatedValue, this.flexGrow, this.flexShrink); + this.marshal.updateElement(this.nativeElement, this.DIRECTIVE_KEY, parts.join(' ')); + } +} + +@Directive({inputs, selector}) +export class DefaultFlexDirective extends FlexDirective { + protected inputs = inputs; } const flexRowCache: Map = new Map(); diff --git a/src/lib/flex/layout-align/layout-align.ts b/src/lib/flex/layout-align/layout-align.ts index cc3cac287..850d9e0ca 100644 --- a/src/lib/flex/layout-align/layout-align.ts +++ b/src/lib/flex/layout-align/layout-align.ts @@ -5,30 +5,18 @@ * 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 {Directive, ElementRef, Optional, Injectable} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnChanges, - OnDestroy, - OnInit, - Optional, - SimpleChanges, - Self, - Injectable, -} from '@angular/core'; -import { - BaseDirective, - MediaChange, - MediaMonitor, + BaseDirective2, StyleBuilder, StyleDefinition, StyleUtils, + MediaMarshaller, + ElementMatcher, } from '@angular/flex-layout/core'; -import {Subscription} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; import {extendObject} from '../../utils/object-extend'; -import {Layout, LayoutDirective} from '../layout/layout'; import {LAYOUT_VALUES, isFlowHorizontal} from '../../utils/layout-validator'; export interface LayoutAlignParent { @@ -108,6 +96,19 @@ export class LayoutAlignStyleBuilder extends StyleBuilder { } } +const inputs = [ + 'fxLayoutAlign', 'fxLayoutAlign.xs', 'fxLayoutAlign.sm', 'fxLayoutAlign.md', + 'fxLayoutAlign.lg', 'fxLayoutAlign.xl', 'fxLayoutAlign.lt-sm', 'fxLayoutAlign.lt-md', + 'fxLayoutAlign.lt-lg', 'fxLayoutAlign.lt-xl', 'fxLayoutAlign.gt-xs', 'fxLayoutAlign.gt-sm', + 'fxLayoutAlign.gt-md', 'fxLayoutAlign.gt-lg' +]; +const selector = ` + [fxLayoutAlign], [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], + [fxLayoutAlign.lg], [fxLayoutAlign.xl], [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], + [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], + [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg] +`; + /** * 'layout-align' flexbox styling directive * Defines positioning of child elements along main and cross axis in a layout container @@ -117,76 +118,22 @@ export class LayoutAlignStyleBuilder extends StyleBuilder { * @see https://css-tricks.com/almanac/properties/a/align-items/ * @see https://css-tricks.com/almanac/properties/a/align-content/ */ -@Directive({selector: ` - [fxLayoutAlign], - [fxLayoutAlign.xs], [fxLayoutAlign.sm], [fxLayoutAlign.md], [fxLayoutAlign.lg],[fxLayoutAlign.xl], - [fxLayoutAlign.lt-sm], [fxLayoutAlign.lt-md], [fxLayoutAlign.lt-lg], [fxLayoutAlign.lt-xl], - [fxLayoutAlign.gt-xs], [fxLayoutAlign.gt-sm], [fxLayoutAlign.gt-md], [fxLayoutAlign.gt-lg] -`}) -export class LayoutAlignDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - protected _layout = 'row'; // default flex-direction - protected _layoutWatcher?: Subscription; - - /* tslint:disable */ - @Input('fxLayoutAlign') set align(val: string) { this._cacheInput('align', val); } - @Input('fxLayoutAlign.xs') set alignXs(val: string) { this._cacheInput('alignXs', val); } - @Input('fxLayoutAlign.sm') set alignSm(val: string) { this._cacheInput('alignSm', val); }; - @Input('fxLayoutAlign.md') set alignMd(val: string) { this._cacheInput('alignMd', val); }; - @Input('fxLayoutAlign.lg') set alignLg(val: string) { this._cacheInput('alignLg', val); }; - @Input('fxLayoutAlign.xl') set alignXl(val: string) { this._cacheInput('alignXl', val); }; - - @Input('fxLayoutAlign.gt-xs') set alignGtXs(val: string) { this._cacheInput('alignGtXs', val); }; - @Input('fxLayoutAlign.gt-sm') set alignGtSm(val: string) { this._cacheInput('alignGtSm', val); }; - @Input('fxLayoutAlign.gt-md') set alignGtMd(val: string) { this._cacheInput('alignGtMd', val); }; - @Input('fxLayoutAlign.gt-lg') set alignGtLg(val: string) { this._cacheInput('alignGtLg', val); }; - - @Input('fxLayoutAlign.lt-sm') set alignLtSm(val: string) { this._cacheInput('alignLtSm', val); }; - @Input('fxLayoutAlign.lt-md') set alignLtMd(val: string) { this._cacheInput('alignLtMd', val); }; - @Input('fxLayoutAlign.lt-lg') set alignLtLg(val: string) { this._cacheInput('alignLtLg', val); }; - @Input('fxLayoutAlign.lt-xl') set alignLtXl(val: string) { this._cacheInput('alignLtXl', val); }; - - /* tslint:enable */ - constructor( - monitor: MediaMonitor, - elRef: ElementRef, - @Optional() @Self() container: LayoutDirective, - styleUtils: StyleUtils, - styleBuilder: LayoutAlignStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); - - if (container) { // Subscribe to layout direction changes - this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); - } - } - - // ********************************************* - // Lifecycle Methods - // ********************************************* - - ngOnChanges(changes: SimpleChanges) { - if (changes['align'] != null || this._mqActivation) { - this._updateWithValue(); - } - } - - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges('align', 'start stretch', (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - } - - ngOnDestroy() { - super.ngOnDestroy(); - if ( this._layoutWatcher ) { - this._layoutWatcher.unsubscribe(); - } +export class LayoutAlignDirective extends BaseDirective2 { + protected DIRECTIVE_KEY = 'layout-align'; + protected layout = 'row'; // default flex-direction + + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: LayoutAlignStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); + this.marshal.trackValue(this.nativeElement, 'layout') + .pipe(takeUntil(this.destroySubject)) + .subscribe(this.onLayoutChange.bind(this)); } // ********************************************* @@ -196,34 +143,39 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha /** * */ - protected _updateWithValue(value?: string) { - value = value || this._queryInput('align') || 'start stretch'; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; + protected updateWithValue(value: string) { + const layout = this.layout || 'row'; + if (layout === 'row') { + this.styleCache = layoutAlignHorizontalCache; + } else if (layout === 'row-reverse') { + this.styleCache = layoutAlignHorizontalRevCache; + } else if (layout === 'column') { + this.styleCache = layoutAlignVerticalCache; + } else if (layout === 'column-reverse') { + this.styleCache = layoutAlignVerticalRevCache; } - - const layout = this._layout || 'row'; - this._styleCache = layout === 'row' ? - layoutAlignHorizontalCache : layoutAlignVerticalCache; - this.addStyles(value || '', {layout}); + this.addStyles(value, {layout}); } /** * Cache the parent container 'flex-direction' and update the 'flex' styles */ - protected _onLayoutChange(layout: Layout) { - this._layout = (layout.direction || '').toLowerCase(); - if (!LAYOUT_VALUES.find(x => x === this._layout)) { - this._layout = 'row'; + protected onLayoutChange(matcher: ElementMatcher) { + const layout: string = matcher.value; + this.layout = layout.split(' ')[0]; + if (!LAYOUT_VALUES.find(x => x === this.layout)) { + this.layout = 'row'; } - - let value = this._queryInput('align') || 'start stretch'; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - this.addStyles(value, {layout: this._layout || 'row'}); + this.triggerUpdate(); } } +@Directive({selector, inputs}) +export class DefaultLayoutAlignDirective extends LayoutAlignDirective { + protected inputs = inputs; +} + const layoutAlignHorizontalCache: Map = new Map(); const layoutAlignVerticalCache: Map = new Map(); +const layoutAlignHorizontalRevCache: Map = new Map(); +const layoutAlignVerticalRevCache: Map = new Map(); diff --git a/src/lib/flex/layout-gap/layout-gap.ts b/src/lib/flex/layout-gap/layout-gap.ts index e1c6ffb54..c7dc13ac5 100644 --- a/src/lib/flex/layout-gap/layout-gap.ts +++ b/src/lib/flex/layout-gap/layout-gap.ts @@ -8,28 +8,24 @@ import { Directive, ElementRef, - Input, - OnChanges, - SimpleChanges, - Self, - AfterContentInit, Optional, OnDestroy, NgZone, Injectable, + AfterContentInit, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { - BaseDirective, - MediaChange, - MediaMonitor, + BaseDirective2, StyleBuilder, StyleDefinition, - StyleUtils + StyleUtils, + MediaMarshaller, + ElementMatcher, } from '@angular/flex-layout/core'; -import {Subscription} from 'rxjs'; +import {Subject} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; -import {Layout, LayoutDirective} from '../layout/layout'; import {LAYOUT_VALUES} from '../../utils/layout-validator'; export interface LayoutGapParent { @@ -83,92 +79,70 @@ export class LayoutGapStyleBuilder extends StyleBuilder { } } +const inputs = [ + 'fxLayoutGap', 'fxLayoutGap.xs', 'fxLayoutGap.sm', 'fxLayoutGap.md', + 'fxLayoutGap.lg', 'fxLayoutGap.xl', 'fxLayoutGap.lt-sm', 'fxLayoutGap.lt-md', + 'fxLayoutGap.lt-lg', 'fxLayoutGap.lt-xl', 'fxLayoutGap.gt-xs', 'fxLayoutGap.gt-sm', + 'fxLayoutGap.gt-md', 'fxLayoutGap.gt-lg' +]; +const selector = ` + [fxLayoutGap], [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], + [fxLayoutGap.lg], [fxLayoutGap.xl], [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], + [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], + [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg] +`; + /** * 'layout-padding' styling directive * Defines padding of child elements in a layout container */ -@Directive({ - selector: ` - [fxLayoutGap], - [fxLayoutGap.xs], [fxLayoutGap.sm], [fxLayoutGap.md], [fxLayoutGap.lg], [fxLayoutGap.xl], - [fxLayoutGap.lt-sm], [fxLayoutGap.lt-md], [fxLayoutGap.lt-lg], [fxLayoutGap.lt-xl], - [fxLayoutGap.gt-xs], [fxLayoutGap.gt-sm], [fxLayoutGap.gt-md], [fxLayoutGap.gt-lg] -` -}) -export class LayoutGapDirective extends BaseDirective - implements AfterContentInit, OnChanges, OnDestroy { - protected _layout = 'row'; // default flex-direction - protected _layoutWatcher?: Subscription; - protected _observer?: MutationObserver; - private readonly _directionWatcher: Subscription; - - /* tslint:disable */ - @Input('fxLayoutGap') set gap(val: string) { this._cacheInput('gap', val); } - @Input('fxLayoutGap.xs') set gapXs(val: string) { this._cacheInput('gapXs', val); } - @Input('fxLayoutGap.sm') set gapSm(val: string) { this._cacheInput('gapSm', val); }; - @Input('fxLayoutGap.md') set gapMd(val: string) { this._cacheInput('gapMd', val); }; - @Input('fxLayoutGap.lg') set gapLg(val: string) { this._cacheInput('gapLg', val); }; - @Input('fxLayoutGap.xl') set gapXl(val: string) { this._cacheInput('gapXl', val); }; - - @Input('fxLayoutGap.gt-xs') set gapGtXs(val: string) { this._cacheInput('gapGtXs', val); }; - @Input('fxLayoutGap.gt-sm') set gapGtSm(val: string) { this._cacheInput('gapGtSm', val); }; - @Input('fxLayoutGap.gt-md') set gapGtMd(val: string) { this._cacheInput('gapGtMd', val); }; - @Input('fxLayoutGap.gt-lg') set gapGtLg(val: string) { this._cacheInput('gapGtLg', val); }; - - @Input('fxLayoutGap.lt-sm') set gapLtSm(val: string) { this._cacheInput('gapLtSm', val); }; - @Input('fxLayoutGap.lt-md') set gapLtMd(val: string) { this._cacheInput('gapLtMd', val); }; - @Input('fxLayoutGap.lt-lg') set gapLtLg(val: string) { this._cacheInput('gapLtLg', val); }; - @Input('fxLayoutGap.lt-xl') set gapLtXl(val: string) { this._cacheInput('gapLtXl', val); }; - - /* tslint:enable */ - constructor(protected monitor: MediaMonitor, - protected elRef: ElementRef, - @Optional() @Self() protected container: LayoutDirective, - protected _zone: NgZone, - protected _directionality: Directionality, - protected styleUtils: StyleUtils, - protected styleBuilder: LayoutGapStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); - - if (container) { // Subscribe to layout direction changes - this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); +export class LayoutGapDirective extends BaseDirective2 implements AfterContentInit, OnDestroy { + protected layout = 'row'; // default flex-direction + protected DIRECTIVE_KEY = 'layout-gap'; + protected observerSubject = new Subject(); + + /** Special accessor to query for all child 'element' nodes regardless of type, class, etc */ + protected get childrenNodes(): HTMLElement[] { + const obj = this.nativeElement.children; + const buffer: any[] = []; + + // iterate backwards ensuring that length is an UInt32 + for (let i = obj.length; i--; ) { + buffer[i] = obj[i]; } - this._directionWatcher = - this._directionality.change.subscribe(this._updateWithValue.bind(this)); + return buffer; + } + + constructor(protected elRef: ElementRef, + protected zone: NgZone, + protected directionality: Directionality, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: LayoutGapStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this), [this.directionality.change, + this.observerSubject.asObservable()]); + this.marshal.trackValue(this.nativeElement, 'layout') + .pipe(takeUntil(this.destroySubject)) + .subscribe(this.onLayoutChange.bind(this)); } // ********************************************* // Lifecycle Methods // ********************************************* - ngOnChanges(changes: SimpleChanges) { - if (changes['gap'] != null || this._mqActivation) { - this._updateWithValue(); - } - } - - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ ngAfterContentInit() { - this._watchContentChanges(); - this._listenForMediaQueryChanges('gap', '0', (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + this.buildChildObservable(); + this.triggerUpdate(); } ngOnDestroy() { super.ngOnDestroy(); - if (this._layoutWatcher) { - this._layoutWatcher.unsubscribe(); - } - if (this._observer) { - this._observer.disconnect(); - } - if (this._directionWatcher) { - this._directionWatcher.unsubscribe(); + if (this.observer) { + this.observer.disconnect(); } } @@ -176,56 +150,33 @@ export class LayoutGapDirective extends BaseDirective // Protected methods // ********************************************* - /** - * Watch for child nodes to be added... and apply the layout gap styles to each. - * NOTE: this does NOT! differentiate between viewChildren and contentChildren - */ - protected _watchContentChanges() { - this._zone.runOutsideAngular(() => { - - if (typeof MutationObserver !== 'undefined') { - this._observer = new MutationObserver((mutations: MutationRecord[]) => { - const validatedChanges = (it: MutationRecord): boolean => { - return (it.addedNodes && it.addedNodes.length > 0) || - (it.removedNodes && it.removedNodes.length > 0); - }; - - // update gap styles only for child 'added' or 'removed' events - if (mutations.some(validatedChanges)) { - this._updateWithValue(); - } - }); - this._observer.observe(this.nativeElement, {childList: true}); - } - }); - } - /** * Cache the parent container 'flex-direction' and update the 'margin' styles */ - protected _onLayoutChange(layout: Layout) { - this._layout = (layout.direction || '').toLowerCase(); - if (!LAYOUT_VALUES.find(x => x === this._layout)) { - this._layout = 'row'; + protected onLayoutChange(matcher: ElementMatcher) { + const layout: string = matcher.value; + // Make sure to filter out 'wrap' option + const direction = layout.split(' '); + this.layout = direction[0]; + if (!LAYOUT_VALUES.find(x => x === this.layout)) { + this.layout = 'row'; } - this._updateWithValue(); + this.triggerUpdate(); } /** * */ - protected _updateWithValue(value?: string) { - let gapValue = value || this._queryInput('gap') || '0'; - if (this._mqActivation) { - gapValue = this._mqActivation.activatedInput; + protected updateWithValue(value: string) { + if (!value) { + value = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY); } - // Gather all non-hidden Element nodes const items = this.childrenNodes - .filter(el => el.nodeType === 1 && this._getDisplayStyle(el) != 'none') + .filter(el => el.nodeType === 1 && this.getDisplayStyle(el) !== 'none') .sort((a, b) => { - const orderA = +this._styler.lookupStyle(a, 'order'); - const orderB = +this._styler.lookupStyle(b, 'order'); + const orderA = +this.styler.lookupStyle(a, 'order'); + const orderB = +this.styler.lookupStyle(b, 'order'); if (isNaN(orderA) || isNaN(orderB) || orderA === orderB) { return 0; } else { @@ -234,20 +185,56 @@ export class LayoutGapDirective extends BaseDirective }); if (items.length > 0) { - const directionality = this._directionality.value; - const layout = this._layout; + const directionality = this.directionality.value; + const layout = this.layout; if (layout === 'row' && directionality === 'rtl') { - this._styleCache = layoutGapCacheRowRtl; + this.styleCache = layoutGapCacheRowRtl; } else if (layout === 'row' && directionality !== 'rtl') { - this._styleCache = layoutGapCacheRowLtr; + this.styleCache = layoutGapCacheRowLtr; } else if (layout === 'column' && directionality === 'rtl') { - this._styleCache = layoutGapCacheColumnRtl; + this.styleCache = layoutGapCacheColumnRtl; } else if (layout === 'column' && directionality !== 'rtl') { - this._styleCache = layoutGapCacheColumnLtr; + this.styleCache = layoutGapCacheColumnLtr; } - this.addStyles(gapValue, {directionality, items, layout}); + this.addStyles(value, {directionality, items, layout}); } } + + /** + * Quick accessor to the current HTMLElement's `display` style + * Note: this allows us to preserve the original style + * and optional restore it when the mediaQueries deactivate + */ + protected getDisplayStyle(source: HTMLElement = this.nativeElement): string { + const query = 'display'; + return this.styler.lookupStyle(source, query); + } + + protected buildChildObservable(): void { + this.zone.runOutsideAngular(() => { + if (typeof MutationObserver !== 'undefined') { + this.observer = new MutationObserver((mutations: MutationRecord[]) => { + const validatedChanges = (it: MutationRecord): boolean => { + return (it.addedNodes && it.addedNodes.length > 0) || + (it.removedNodes && it.removedNodes.length > 0); + }; + + // update gap styles only for child 'added' or 'removed' events + if (mutations.some(validatedChanges)) { + this.observerSubject.next(); + } + }); + this.observer.observe(this.nativeElement, {childList: true}); + } + }); + } + + protected observer?: MutationObserver; +} + +@Directive({selector, inputs}) +export class DefaultLayoutGapDirective extends LayoutGapDirective { + protected inputs = inputs; } const layoutGapCacheRowRtl: Map = new Map(); diff --git a/src/lib/flex/layout/layout.ts b/src/lib/flex/layout/layout.ts index b90763413..4cf6103d0 100644 --- a/src/lib/flex/layout/layout.ts +++ b/src/lib/flex/layout/layout.ts @@ -5,51 +5,37 @@ * 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 {Directive, ElementRef, OnChanges, Injectable, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, - Injectable, -} from '@angular/core'; -import { - BaseDirective, - MediaChange, - MediaMonitor, + BaseDirective2, StyleBuilder, StyleDefinition, StyleUtils, + MediaMarshaller, } from '@angular/flex-layout/core'; -import {Observable, ReplaySubject} from 'rxjs'; import {buildLayoutCSS} from '../../utils/layout-validator'; -export type Layout = { - direction: string; - wrap: boolean; -}; - -export interface LayoutParent { - announcer: ReplaySubject; -} - @Injectable({providedIn: 'root'}) export class LayoutStyleBuilder extends StyleBuilder { - buildStyles(input: string, _parent: LayoutParent) { - const styles = buildLayoutCSS(input); - return styles; - } - sideEffect(_input: string, styles: StyleDefinition, parent: LayoutParent) { - parent.announcer.next({ - direction: styles['flex-direction'] as string, - wrap: !!styles['flex-wrap'] && styles['flex-wrap'] !== 'nowrap' - }); + buildStyles(input: string) { + return buildLayoutCSS(input); } } +const inputs = [ + 'fxLayout', 'fxLayout.xs', 'fxLayout.sm', 'fxLayout.md', + 'fxLayout.lg', 'fxLayout.xl', 'fxLayout.lt-sm', 'fxLayout.lt-md', + 'fxLayout.lt-lg', 'fxLayout.lt-xl', 'fxLayout.gt-xs', 'fxLayout.gt-sm', + 'fxLayout.gt-md', 'fxLayout.gt-lg' +]; +const selector = ` + [fxLayout], [fxLayout.xs], [fxLayout.sm], [fxLayout.md], + [fxLayout.lg], [fxLayout.xl], [fxLayout.lt-sm], [fxLayout.lt-md], + [fxLayout.lt-lg], [fxLayout.lt-xl], [fxLayout.gt-xs], [fxLayout.gt-sm], + [fxLayout.gt-md], [fxLayout.gt-lg] +`; + /** * 'layout' flexbox styling directive * Defines the positioning flow direction for the child elements: row or column @@ -57,95 +43,27 @@ export class LayoutStyleBuilder extends StyleBuilder { * @see https://css-tricks.com/almanac/properties/f/flex-direction/ * */ -@Directive({selector: ` - [fxLayout], - [fxLayout.xs], [fxLayout.sm], [fxLayout.md], [fxLayout.lg], [fxLayout.xl], - [fxLayout.lt-sm], [fxLayout.lt-md], [fxLayout.lt-lg], [fxLayout.lt-xl], - [fxLayout.gt-xs], [fxLayout.gt-sm], [fxLayout.gt-md], [fxLayout.gt-lg] -`}) -export class LayoutDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /** - * Create Observable for nested/child 'flex' directives. This allows - * child flex directives to subscribe/listen for flexbox direction changes. - */ - protected _announcer: ReplaySubject; - - /** - * Publish observer to enabled nested, dependent directives to listen - * to parent 'layout' direction changes - */ - layout$: Observable; - - /* tslint:disable */ - @Input('fxLayout') set layout(val: string) { this._cacheInput('layout', val); }; - @Input('fxLayout.xs') set layoutXs(val: string) { this._cacheInput('layoutXs', val); }; - @Input('fxLayout.sm') set layoutSm(val: string) { this._cacheInput('layoutSm', val); }; - @Input('fxLayout.md') set layoutMd(val: string) { this._cacheInput('layoutMd', val); }; - @Input('fxLayout.lg') set layoutLg(val: string) { this._cacheInput('layoutLg', val); }; - @Input('fxLayout.xl') set layoutXl(val: string) { this._cacheInput('layoutXl', val); }; - - @Input('fxLayout.gt-xs') set layoutGtXs(val: string) { this._cacheInput('layoutGtXs', val); }; - @Input('fxLayout.gt-sm') set layoutGtSm(val: string) { this._cacheInput('layoutGtSm', val); }; - @Input('fxLayout.gt-md') set layoutGtMd(val: string) { this._cacheInput('layoutGtMd', val); }; - @Input('fxLayout.gt-lg') set layoutGtLg(val: string) { this._cacheInput('layoutGtLg', val); }; - - @Input('fxLayout.lt-sm') set layoutLtSm(val: string) { this._cacheInput('layoutLtSm', val); }; - @Input('fxLayout.lt-md') set layoutLtMd(val: string) { this._cacheInput('layoutLtMd', val); }; - @Input('fxLayout.lt-lg') set layoutLtLg(val: string) { this._cacheInput('layoutLtLg', val); }; - @Input('fxLayout.lt-xl') set layoutLtXl(val: string) { this._cacheInput('layoutLtXl', val); }; - /* tslint:enable */ - - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils, - styleBuilder: LayoutStyleBuilder) { - super(monitor, elRef, styleUtils, styleBuilder); - this._announcer = new ReplaySubject(1); - this.layout$ = this._announcer.asObservable(); - } - - // ********************************************* - // Lifecycle Methods - // ********************************************* - - /** - * On changes to any @Input properties... - * Default to use the non-responsive Input value ('fxLayout') - * Then conditionally override with the mq-activated Input's current value - */ - ngOnChanges(changes: SimpleChanges) { - if (changes['layout'] != null || this._mqActivation) { - this._updateWithDirection(); - } - } - - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges('layout', 'row', (changes: MediaChange) => { - this._updateWithDirection(changes.value); - }); +export class LayoutDirective extends BaseDirective2 implements OnChanges { + + protected DIRECTIVE_KEY = 'layout'; + + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: LayoutStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.addStyles.bind(this)); } - // ********************************************* - // Protected methods - // ********************************************* - - /** Validate the direction value and then update the host's inline flexbox styles */ - protected _updateWithDirection(value?: string) { - value = value || this._queryInput('layout') || 'row'; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - this.addStyles(value || '', {announcer: this._announcer}); - } + protected styleCache = layoutCache; +} - protected _styleCache = layoutCache; +@Directive({selector, inputs}) +export class DefaultLayoutDirective extends LayoutDirective { + protected inputs = inputs; } const layoutCache: Map = new Map(); diff --git a/src/lib/flex/module.ts b/src/lib/flex/module.ts index 57f830248..9df86b458 100644 --- a/src/lib/flex/module.ts +++ b/src/lib/flex/module.ts @@ -9,25 +9,25 @@ import {NgModule} from '@angular/core'; import {BidiModule} from '@angular/cdk/bidi'; import {CoreModule} from '@angular/flex-layout/core'; -import {LayoutDirective} from './layout/layout'; -import {LayoutGapDirective} from './layout-gap/layout-gap'; -import {FlexDirective} from './flex/flex'; -import {FlexOrderDirective} from './flex-order/flex-order'; -import {FlexOffsetDirective} from './flex-offset/flex-offset'; -import {FlexAlignDirective} from './flex-align/flex-align'; +import {DefaultLayoutDirective} from './layout/layout'; +import {DefaultLayoutGapDirective} from './layout-gap/layout-gap'; +import {DefaultFlexDirective} from './flex/flex'; +import {DefaultFlexOrderDirective} from './flex-order/flex-order'; +import {DefaultFlexOffsetDirective} from './flex-offset/flex-offset'; +import {DefaultFlexAlignDirective} from './flex-align/flex-align'; import {FlexFillDirective} from './flex-fill/flex-fill'; -import {LayoutAlignDirective} from './layout-align/layout-align'; +import {DefaultLayoutAlignDirective} from './layout-align/layout-align'; const ALL_DIRECTIVES = [ - LayoutDirective, - LayoutGapDirective, - LayoutAlignDirective, - FlexDirective, - FlexOrderDirective, - FlexOffsetDirective, + DefaultLayoutDirective, + DefaultLayoutGapDirective, + DefaultLayoutAlignDirective, + DefaultFlexOrderDirective, + DefaultFlexOffsetDirective, FlexFillDirective, - FlexAlignDirective, + DefaultFlexAlignDirective, + DefaultFlexDirective, ]; /** diff --git a/src/lib/grid/align-columns/align-columns.ts b/src/lib/grid/align-columns/align-columns.ts index 7c739167d..f7d9d317c 100644 --- a/src/lib/grid/align-columns/align-columns.ts +++ b/src/lib/grid/align-columns/align-columns.ts @@ -5,158 +5,142 @@ * 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 {Directive, ElementRef, Injectable, Input, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; -import {extendObject} from '../../utils/object-extend'; + BaseDirective2, + StyleUtils, + StyleBuilder, + StyleDefinition, + MediaMarshaller, +} from '@angular/flex-layout/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -const CACHE_KEY = 'alignColumns'; const DEFAULT_MAIN = 'start'; const DEFAULT_CROSS = 'stretch'; -/** - * 'column alignment' CSS Grid styling directive - * Configures the alignment in the column direction - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-19 - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-21 - */ -@Directive({selector: ` - [gdAlignColumns], - [gdAlignColumns.xs], [gdAlignColumns.sm], [gdAlignColumns.md], - [gdAlignColumns.lg], [gdAlignColumns.xl], [gdAlignColumns.lt-sm], - [gdAlignColumns.lt-md], [gdAlignColumns.lt-lg], [gdAlignColumns.lt-xl], - [gdAlignColumns.gt-xs], [gdAlignColumns.gt-sm], [gdAlignColumns.gt-md], - [gdAlignColumns.gt-lg] -`}) -export class GridAlignColumnsDirective extends BaseDirective - implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdAlignColumns') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdAlignColumns.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdAlignColumns.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdAlignColumns.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdAlignColumns.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdAlignColumns.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdAlignColumns.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdAlignColumns.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdAlignColumns.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdAlignColumns.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdAlignColumns.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdAlignColumns.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdAlignColumns.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdAlignColumns.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - @Input('gdInline') set inline(val: string) { this._cacheInput('inline', coerceBooleanProperty(val)); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); +export interface GridAlignColumnsParent { + inline: boolean; +} + +@Injectable({providedIn: 'root'}) +export class GridAlignColumnsStyleBuilder extends StyleBuilder { + buildStyles(input: string, parent: GridAlignColumnsParent) { + return buildCss(input || `${DEFAULT_MAIN} ${DEFAULT_CROSS}`, parent.inline); } +} - // ********************************************* - // Lifecycle Methods - // ********************************************* +export class GridAlignColumnsDirective extends BaseDirective2 { - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } - } + protected DIRECTIVE_KEY = 'grid-align-columns'; - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges(CACHE_KEY, `${DEFAULT_MAIN} ${DEFAULT_CROSS}`, - (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + @Input('gdInline') + get inline(): boolean { return this._inline; } + set inline(val: boolean) { this._inline = coerceBooleanProperty(val); } + protected _inline = false; + + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridAlignColumnsStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); } // ********************************************* // Protected methods // ********************************************* - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || `${DEFAULT_MAIN} ${DEFAULT_CROSS}`; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this._applyStyleToElement(this._buildCSS(value)); + protected updateWithValue(value: string) { + this.styleCache = this.inline ? alignColumnsInlineCache : alignColumnsCache; + this.addStyles(value, {inline: this.inline}); } +} +const alignColumnsCache: Map = new Map(); +const alignColumnsInlineCache: Map = new Map(); + +const inputs = [ + 'gdAlignColumns', + 'gdAlignColumns.xs', 'gdAlignColumns.sm', 'gdAlignColumns.md', + 'gdAlignColumns.lg', 'gdAlignColumns.xl', 'gdAlignColumns.lt-sm', + 'gdAlignColumns.lt-md', 'gdAlignColumns.lt-lg', 'gdAlignColumns.lt-xl', + 'gdAlignColumns.gt-xs', 'gdAlignColumns.gt-sm', 'gdAlignColumns.gt-md', + 'gdAlignColumns.gt-lg' +]; +const selector = ` + [gdAlignColumns], + [gdAlignColumns.xs], [gdAlignColumns.sm], [gdAlignColumns.md], + [gdAlignColumns.lg], [gdAlignColumns.xl], [gdAlignColumns.lt-sm], + [gdAlignColumns.lt-md], [gdAlignColumns.lt-lg], [gdAlignColumns.lt-xl], + [gdAlignColumns.gt-xs], [gdAlignColumns.gt-sm], [gdAlignColumns.gt-md], + [gdAlignColumns.gt-lg] +`; - protected _buildCSS(align: string = '') { - let css: {[key: string]: string} = {}, [mainAxis, crossAxis] = align.split(' '); - - // Main axis - switch (mainAxis) { - case 'center': - css['align-content'] = 'center'; - break; - case 'space-around': - css['align-content'] = 'space-around'; - break; - case 'space-between': - css['align-content'] = 'space-between'; - break; - case 'space-evenly': - css['align-content'] = 'space-evenly'; - break; - case 'end': - css['align-content'] = 'end'; - break; - case 'start': - css['align-content'] = 'start'; - break; - case 'stretch': - css['align-content'] = 'stretch'; - break; - default: - css['align-content'] = DEFAULT_MAIN; // default main axis - break; - } - - // Cross-axis - switch (crossAxis) { - case 'start': - css['align-items'] = 'start'; - break; - case 'center': - css['align-items'] = 'center'; - break; - case 'end': - css['align-items'] = 'end'; - break; - case 'stretch': - css['align-items'] = 'stretch'; - break; - default : // 'stretch' - css['align-items'] = DEFAULT_CROSS; // default cross axis - break; - } - - return extendObject(css, {'display' : this._queryInput('inline') ? 'inline-grid' : 'grid'}); +/** + * 'column alignment' CSS Grid styling directive + * Configures the alignment in the column direction + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-19 + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-21 + */ +@Directive({selector, inputs}) +export class DefaultGridAlignColumnsDirective extends GridAlignColumnsDirective { + protected inputs = inputs; +} + +function buildCss(align: string, inline: boolean): StyleDefinition { + const css: {[key: string]: string} = {}, [mainAxis, crossAxis] = align.split(' '); + + // Main axis + switch (mainAxis) { + case 'center': + css['align-content'] = 'center'; + break; + case 'space-around': + css['align-content'] = 'space-around'; + break; + case 'space-between': + css['align-content'] = 'space-between'; + break; + case 'space-evenly': + css['align-content'] = 'space-evenly'; + break; + case 'end': + css['align-content'] = 'end'; + break; + case 'start': + css['align-content'] = 'start'; + break; + case 'stretch': + css['align-content'] = 'stretch'; + break; + default: + css['align-content'] = DEFAULT_MAIN; // default main axis + break; } + + // Cross-axis + switch (crossAxis) { + case 'start': + css['align-items'] = 'start'; + break; + case 'center': + css['align-items'] = 'center'; + break; + case 'end': + css['align-items'] = 'end'; + break; + case 'stretch': + css['align-items'] = 'stretch'; + break; + default : // 'stretch' + css['align-items'] = DEFAULT_CROSS; // default cross axis + break; + } + + css['display'] = inline ? 'inline-grid' : 'grid'; + + return css; } diff --git a/src/lib/grid/align-rows/align-rows.ts b/src/lib/grid/align-rows/align-rows.ts index a144de4f2..8bf437974 100644 --- a/src/lib/grid/align-rows/align-rows.ts +++ b/src/lib/grid/align-rows/align-rows.ts @@ -5,139 +5,124 @@ * 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 {Directive, ElementRef, Injectable, Input, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; -import {extendObject} from '../../utils/object-extend'; + BaseDirective2, + StyleUtils, + StyleBuilder, + StyleDefinition, + MediaMarshaller, +} from '@angular/flex-layout/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -const CACHE_KEY = 'alignRows'; const DEFAULT_MAIN = 'start'; const DEFAULT_CROSS = 'stretch'; -/** - * 'row alignment' CSS Grid styling directive - * Configures the alignment in the row direction - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-18 - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-20 - */ -@Directive({selector: ` - [gdAlignRows], - [gdAlignRows.xs], [gdAlignRows.sm], [gdAlignRows.md], - [gdAlignRows.lg], [gdAlignRows.xl], [gdAlignRows.lt-sm], - [gdAlignRows.lt-md], [gdAlignRows.lt-lg], [gdAlignRows.lt-xl], - [gdAlignRows.gt-xs], [gdAlignRows.gt-sm], [gdAlignRows.gt-md], - [gdAlignRows.gt-lg] -`}) -export class GridAlignRowsDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdAlignRows') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdAlignRows.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdAlignRows.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdAlignRows.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdAlignRows.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdAlignRows.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdAlignRows.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdAlignRows.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdAlignRows.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdAlignRows.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdAlignRows.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdAlignRows.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdAlignRows.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdAlignRows.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - @Input('gdInline') set inline(val: string) { this._cacheInput('inline', coerceBooleanProperty(val)); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); +export interface GridAlignRowsParent { + inline: boolean; +} + +@Injectable({providedIn: 'root'}) +export class GridAlignRowsStyleBuilder extends StyleBuilder { + buildStyles(input: string, parent: GridAlignRowsParent) { + return buildCss(input || `${DEFAULT_MAIN} ${DEFAULT_CROSS}`, parent.inline); } +} - // ********************************************* - // Lifecycle Methods - // ********************************************* +export class GridAlignRowsDirective extends BaseDirective2 { - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } - } + protected DIRECTIVE_KEY = 'grid-align-rows'; - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges(CACHE_KEY, `${DEFAULT_MAIN} ${DEFAULT_CROSS}`, - (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + @Input('gdInline') + get inline(): boolean { return this._inline; } + set inline(val: boolean) { this._inline = coerceBooleanProperty(val); } + protected _inline = false; + + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridAlignRowsStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); } // ********************************************* // Protected methods // ********************************************* - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || `${DEFAULT_MAIN} ${DEFAULT_CROSS}`; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this._applyStyleToElement(this._buildCSS(value)); + protected updateWithValue(value: string) { + this.styleCache = this.inline ? alignRowsInlineCache : alignRowsCache; + this.addStyles(value, {inline: this.inline}); } +} +const alignRowsCache: Map = new Map(); +const alignRowsInlineCache: Map = new Map(); + +const inputs = [ + 'gdAlignRows', + 'gdAlignRows.xs', 'gdAlignRows.sm', 'gdAlignRows.md', + 'gdAlignRows.lg', 'gdAlignRows.xl', 'gdAlignRows.lt-sm', + 'gdAlignRows.lt-md', 'gdAlignRows.lt-lg', 'gdAlignRows.lt-xl', + 'gdAlignRows.gt-xs', 'gdAlignRows.gt-sm', 'gdAlignRows.gt-md', + 'gdAlignRows.gt-lg' +]; +const selector = ` + [gdAlignRows], + [gdAlignRows.xs], [gdAlignRows.sm], [gdAlignRows.md], + [gdAlignRows.lg], [gdAlignRows.xl], [gdAlignRows.lt-sm], + [gdAlignRows.lt-md], [gdAlignRows.lt-lg], [gdAlignRows.lt-xl], + [gdAlignRows.gt-xs], [gdAlignRows.gt-sm], [gdAlignRows.gt-md], + [gdAlignRows.gt-lg] +`; - protected _buildCSS(align: string = '') { - let css: {[key: string]: string} = {}, [mainAxis, crossAxis] = align.split(' '); - - // Main axis - switch (mainAxis) { - case 'center': - case 'space-around': - case 'space-between': - case 'space-evenly': - case 'end': - case 'start': - case 'stretch': - css['justify-content'] = mainAxis; - break; - default: - css['justify-content'] = DEFAULT_MAIN; // default main axis - break; - } - - // Cross-axis - switch (crossAxis) { - case 'start': - case 'center': - case 'end': - case 'stretch': - css['justify-items'] = crossAxis; - break; - default : // 'stretch' - css['justify-items'] = DEFAULT_CROSS; // default cross axis - break; - } - - return extendObject(css, {'display' : this._queryInput('inline') ? 'inline-grid' : 'grid'}); +/** + * 'row alignment' CSS Grid styling directive + * Configures the alignment in the row direction + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-18 + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-20 + */ +@Directive({selector, inputs}) +export class DefaultGridAlignRowsDirective extends GridAlignRowsDirective { + protected inputs = inputs; +} + +function buildCss(align: string, inline: boolean): StyleDefinition { + const css: {[key: string]: string} = {}, [mainAxis, crossAxis] = align.split(' '); + + // Main axis + switch (mainAxis) { + case 'center': + case 'space-around': + case 'space-between': + case 'space-evenly': + case 'end': + case 'start': + case 'stretch': + css['justify-content'] = mainAxis; + break; + default: + css['justify-content'] = DEFAULT_MAIN; // default main axis + break; } + + // Cross-axis + switch (crossAxis) { + case 'start': + case 'center': + case 'end': + case 'stretch': + css['justify-items'] = crossAxis; + break; + default : // 'stretch' + css['justify-items'] = DEFAULT_CROSS; // default cross axis + break; + } + + css['display'] = inline ? 'inline-grid' : 'grid'; + + return css; } diff --git a/src/lib/grid/area/area.ts b/src/lib/grid/area/area.ts index e995a88ac..3d42edd0f 100644 --- a/src/lib/grid/area/area.ts +++ b/src/lib/grid/area/area.ts @@ -5,99 +5,63 @@ * 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 {Directive, ElementRef, Injectable, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + BaseDirective2, + StyleUtils, + MediaMarshaller, + StyleBuilder, + StyleDefinition, +} from '@angular/flex-layout/core'; -const CACHE_KEY = 'area'; const DEFAULT_VALUE = 'auto'; -/** - * 'grid-area' CSS Grid styling directive - * Configures the name or position of an element within the grid - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-27 - */ -@Directive({selector: ` - [gdArea], - [gdArea.xs], [gdArea.sm], [gdArea.md], [gdArea.lg], [gdArea.xl], - [gdArea.lt-sm], [gdArea.lt-md], [gdArea.lt-lg], [gdArea.lt-xl], - [gdArea.gt-xs], [gdArea.gt-sm], [gdArea.gt-md], [gdArea.gt-lg] -`}) -export class GridAreaDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdArea') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdArea.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdArea.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdArea.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdArea.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdArea.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdArea.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdArea.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdArea.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdArea.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdArea.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdArea.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdArea.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdArea.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); +@Injectable({providedIn: 'root'}) +export class GridAreaStyleBuilder extends StyleBuilder { + buildStyles(input: string) { + return {'grid-area': input || DEFAULT_VALUE}; } +} - // ********************************************* - // Lifecycle Methods - // ********************************************* +export class GridAreaDirective extends BaseDirective2 { - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } - } + protected DIRECTIVE_KEY = 'grid-area'; - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridAreaStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.addStyles.bind(this)); } - // ********************************************* - // Protected methods - // ********************************************* - - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } + protected styleCache = gridAreaCache; +} - this._applyStyleToElement(this._buildCSS(value)); - } +const gridAreaCache: Map = new Map(); +const inputs = [ + 'gdArea', + 'gdArea.xs', 'gdArea.sm', 'gdArea.md', 'gdArea.lg', 'gdArea.xl', + 'gdArea.lt-sm', 'gdArea.lt-md', 'gdArea.lt-lg', 'gdArea.lt-xl', + 'gdArea.gt-xs', 'gdArea.gt-sm', 'gdArea.gt-md', 'gdArea.gt-lg' +]; +const selector = ` + [gdArea], + [gdArea.xs], [gdArea.sm], [gdArea.md], [gdArea.lg], [gdArea.xl], + [gdArea.lt-sm], [gdArea.lt-md], [gdArea.lt-lg], [gdArea.lt-xl], + [gdArea.gt-xs], [gdArea.gt-sm], [gdArea.gt-md], [gdArea.gt-lg] +`; - protected _buildCSS(value: string = '') { - return {'grid-area': value}; - } +/** + * 'grid-area' CSS Grid styling directive + * Configures the name or position of an element within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-27 + */ +@Directive({selector, inputs}) +export class DefaultGridAreaDirective extends GridAreaDirective { + protected inputs = inputs; } diff --git a/src/lib/grid/areas/areas.ts b/src/lib/grid/areas/areas.ts index 10fcefa81..515316119 100644 --- a/src/lib/grid/areas/areas.ts +++ b/src/lib/grid/areas/areas.ts @@ -5,108 +5,88 @@ * 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 {Directive, ElementRef, Injectable, Input, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + BaseDirective2, + StyleUtils, + StyleBuilder, + MediaMarshaller, + StyleDefinition, +} from '@angular/flex-layout/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -const CACHE_KEY = 'areas'; const DEFAULT_VALUE = 'none'; const DELIMETER = '|'; -/** - * 'grid-template-areas' CSS Grid styling directive - * Configures the names of elements within the grid - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-14 - */ -@Directive({selector: ` - [gdAreas], - [gdAreas.xs], [gdAreas.sm], [gdAreas.md], [gdAreas.lg], [gdAreas.xl], - [gdAreas.lt-sm], [gdAreas.lt-md], [gdAreas.lt-lg], [gdAreas.lt-xl], - [gdAreas.gt-xs], [gdAreas.gt-sm], [gdAreas.gt-md], [gdAreas.gt-lg] -`}) -export class GridAreasDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdAreas') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdAreas.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdAreas.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdAreas.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdAreas.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdAreas.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdAreas.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdAreas.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdAreas.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdAreas.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdAreas.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdAreas.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdAreas.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdAreas.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; +export interface GridAreasParent { + inline: boolean; +} - @Input('gdInline') set inline(val: string) { this._cacheInput('inline', coerceBooleanProperty(val)); }; +@Injectable({providedIn: 'root'}) +export class GridAreasStyleBuiler extends StyleBuilder { + buildStyles(input: string, parent: GridAreasParent) { + const areas = (input || DEFAULT_VALUE).split(DELIMETER).map(v => `"${v.trim()}"`); - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); + return { + 'display': parent.inline ? 'inline-grid' : 'grid', + 'grid-template-areas': areas.join(' ') + }; } +} - // ********************************************* - // Lifecycle Methods - // ********************************************* +export class GridAreasDirective extends BaseDirective2 { - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } - } + protected DIRECTIVE_KEY = 'grid-areas'; - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); + @Input('gdInline') + get inline(): boolean { return this._inline; } + set inline(val: boolean) { this._inline = coerceBooleanProperty(val); } + protected _inline = false; - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridAreasStyleBuiler, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); } // ********************************************* // Protected methods // ********************************************* - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this._applyStyleToElement(this._buildCSS(value)); + protected updateWithValue(value: string) { + this.styleCache = this.inline ? areasInlineCache : areasCache; + this.addStyles(value, {inline: this.inline}); } +} +const areasCache: Map = new Map(); +const areasInlineCache: Map = new Map(); - protected _buildCSS(value: string = '') { - const areas = value.split(DELIMETER).map(v => `"${v.trim()}"`); +const inputs = [ + 'gdAreas', + 'gdAreas.xs', 'gdAreas.sm', 'gdAreas.md', 'gdAreas.lg', 'gdAreas.xl', + 'gdAreas.lt-sm', 'gdAreas.lt-md', 'gdAreas.lt-lg', 'gdAreas.lt-xl', + 'gdAreas.gt-xs', 'gdAreas.gt-sm', 'gdAreas.gt-md', 'gdAreas.gt-lg' +]; - return { - 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', - 'grid-template-areas': areas.join(' ') - }; - } +const selector = ` + [gdAreas], + [gdAreas.xs], [gdAreas.sm], [gdAreas.md], [gdAreas.lg], [gdAreas.xl], + [gdAreas.lt-sm], [gdAreas.lt-md], [gdAreas.lt-lg], [gdAreas.lt-xl], + [gdAreas.gt-xs], [gdAreas.gt-sm], [gdAreas.gt-md], [gdAreas.gt-lg] +`; + +/** + * 'grid-template-areas' CSS Grid styling directive + * Configures the names of elements within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-14 + */ +@Directive({selector, inputs}) +export class DefaultGridAreasDirective extends GridAreasDirective { + protected inputs = inputs; } diff --git a/src/lib/grid/auto/auto.ts b/src/lib/grid/auto/auto.ts index 35d4194e4..76542093d 100644 --- a/src/lib/grid/auto/auto.ts +++ b/src/lib/grid/auto/auto.ts @@ -5,112 +5,90 @@ * 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 {Directive, ElementRef, Input, Optional, Injectable} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + BaseDirective2, + StyleUtils, + StyleBuilder, + MediaMarshaller, + StyleDefinition, +} from '@angular/flex-layout/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -const CACHE_KEY = 'autoFlow'; const DEFAULT_VALUE = 'initial'; -/** - * 'grid-auto-flow' CSS Grid styling directive - * Configures the auto placement algorithm for the grid - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-23 - */ -@Directive({selector: ` - [gdAuto], - [gdAuto.xs], [gdAuto.sm], [gdAuto.md], [gdAuto.lg], [gdAuto.xl], - [gdAuto.lt-sm], [gdAuto.lt-md], [gdAuto.lt-lg], [gdAuto.lt-xl], - [gdAuto.gt-xs], [gdAuto.gt-sm], [gdAuto.gt-md], [gdAuto.gt-lg] -`}) -export class GridAutoDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdAuto') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdAuto.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdAuto.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdAuto.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdAuto.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdAuto.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdAuto.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdAuto.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdAuto.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdAuto.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdAuto.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdAuto.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdAuto.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdAuto.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - @Input('gdInline') set inline(val: string) { this._cacheInput('inline', coerceBooleanProperty(val)); }; +export interface GridAutoParent { + inline: boolean; +} - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); - } +@Injectable({providedIn: 'root'}) +export class GridAutoStyleBuilder extends StyleBuilder { + buildStyles(input: string, parent: GridAutoParent) { + let [direction, dense] = (input || DEFAULT_VALUE).split(' '); + if (direction !== 'column' && direction !== 'row' && direction !== 'dense') { + direction = 'row'; + } - // ********************************************* - // Lifecycle Methods - // ********************************************* + dense = (dense === 'dense' && direction !== 'dense') ? ' dense' : ''; - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } + return { + 'display': parent.inline ? 'inline-grid' : 'grid', + 'grid-auto-flow': direction + dense + }; } +} - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); +export class GridAutoDirective extends BaseDirective2 { + @Input('gdInline') + get inline(): boolean { return this._inline; } + set inline(val: boolean) { this._inline = coerceBooleanProperty(val); } + protected _inline = false; + + protected DIRECTIVE_KEY = 'grid-auto'; + + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridAutoStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); } // ********************************************* // Protected methods // ********************************************* - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this._applyStyleToElement(this._buildCSS(value)); + protected updateWithValue(value: string) { + this.styleCache = this.inline ? autoInlineCache : autoCache; + this.addStyles(value, {inline: this.inline}); } +} +const autoCache: Map = new Map(); +const autoInlineCache: Map = new Map(); - protected _buildCSS(value: string = '') { - let [direction, dense] = value.split(' '); - if (direction !== 'column' && direction !== 'row' && direction !== 'dense') { - direction = 'row'; - } - - dense = (dense === 'dense' && direction !== 'dense') ? ' dense' : ''; +const inputs = [ + 'gdAuto', + 'gdAuto.xs', 'gdAuto.sm', 'gdAuto.md', 'gdAuto.lg', 'gdAuto.xl', + 'gdAuto.lt-sm', 'gdAuto.lt-md', 'gdAuto.lt-lg', 'gdAuto.lt-xl', + 'gdAuto.gt-xs', 'gdAuto.gt-sm', 'gdAuto.gt-md', 'gdAuto.gt-lg' +]; +const selector = ` + [gdAuto], + [gdAuto.xs], [gdAuto.sm], [gdAuto.md], [gdAuto.lg], [gdAuto.xl], + [gdAuto.lt-sm], [gdAuto.lt-md], [gdAuto.lt-lg], [gdAuto.lt-xl], + [gdAuto.gt-xs], [gdAuto.gt-sm], [gdAuto.gt-md], [gdAuto.gt-lg] +`; - return { - 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', - 'grid-auto-flow': direction + dense - }; - } +/** + * 'grid-auto-flow' CSS Grid styling directive + * Configures the auto placement algorithm for the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-23 + */ +@Directive({selector, inputs}) +export class DefaultGridAutoDirective extends GridAutoDirective { + protected inputs = inputs; } diff --git a/src/lib/grid/column/column.ts b/src/lib/grid/column/column.ts index bb1db2111..49bb18416 100644 --- a/src/lib/grid/column/column.ts +++ b/src/lib/grid/column/column.ts @@ -5,99 +5,63 @@ * 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 {Directive, ElementRef, Optional, Injectable} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + BaseDirective2, + StyleUtils, + MediaMarshaller, + StyleBuilder, + StyleDefinition, +} from '@angular/flex-layout/core'; -const CACHE_KEY = 'column'; const DEFAULT_VALUE = 'auto'; -/** - * 'grid-column' CSS Grid styling directive - * Configures the name or position of an element within the grid - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-26 - */ -@Directive({selector: ` - [gdColumn], - [gdColumn.xs], [gdColumn.sm], [gdColumn.md], [gdColumn.lg], [gdColumn.xl], - [gdColumn.lt-sm], [gdColumn.lt-md], [gdColumn.lt-lg], [gdColumn.lt-xl], - [gdColumn.gt-xs], [gdColumn.gt-sm], [gdColumn.gt-md], [gdColumn.gt-lg] -`}) -export class GridColumnDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdColumn') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdColumn.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdColumn.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdColumn.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdColumn.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdColumn.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdColumn.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdColumn.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdColumn.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdColumn.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdColumn.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdColumn.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdColumn.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdColumn.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); - } - - // ********************************************* - // Lifecycle Methods - // ********************************************* - - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } +@Injectable({providedIn: 'root'}) +export class GridColumnStyleBuilder extends StyleBuilder { + buildStyles(input: string) { + return {'grid-column': input || DEFAULT_VALUE}; } +} - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); +export class GridColumnDirective extends BaseDirective2 { + protected DIRECTIVE_KEY = 'grid-column'; - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridColumnStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.addStyles.bind(this)); } - // ********************************************* - // Protected methods - // ********************************************* + protected styleCache = columnCache; +} - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } +const columnCache: Map = new Map(); - this._applyStyleToElement(this._buildCSS(value)); - } +const inputs = [ + 'gdColumn', + 'gdColumn.xs', 'gdColumn.sm', 'gdColumn.md', 'gdColumn.lg', 'gdColumn.xl', + 'gdColumn.lt-sm', 'gdColumn.lt-md', 'gdColumn.lt-lg', 'gdColumn.lt-xl', + 'gdColumn.gt-xs', 'gdColumn.gt-sm', 'gdColumn.gt-md', 'gdColumn.gt-lg' +]; +const selector = ` + [gdColumn], + [gdColumn.xs], [gdColumn.sm], [gdColumn.md], [gdColumn.lg], [gdColumn.xl], + [gdColumn.lt-sm], [gdColumn.lt-md], [gdColumn.lt-lg], [gdColumn.lt-xl], + [gdColumn.gt-xs], [gdColumn.gt-sm], [gdColumn.gt-md], [gdColumn.gt-lg] +`; - protected _buildCSS(value: string = '') { - return {'grid-column': value}; - } +/** + * 'grid-column' CSS Grid styling directive + * Configures the name or position of an element within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-26 + */ +@Directive({selector, inputs}) +export class DefaultGridColumnDirective extends GridColumnDirective { + protected inputs = inputs; } diff --git a/src/lib/grid/columns/columns.ts b/src/lib/grid/columns/columns.ts index 17bbaacec..6a6935006 100644 --- a/src/lib/grid/columns/columns.ts +++ b/src/lib/grid/columns/columns.ts @@ -5,118 +5,98 @@ * 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 {Directive, ElementRef, Input, Injectable, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + MediaMarshaller, + BaseDirective2, + StyleBuilder, + StyleDefinition, + StyleUtils, +} from '@angular/flex-layout/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -const CACHE_KEY = 'columns'; const DEFAULT_VALUE = 'none'; const AUTO_SPECIFIER = '!'; -/** - * 'grid-template-columns' CSS Grid styling directive - * Configures the sizing for the columns in the grid - * Syntax: [auto] - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-13 - */ -@Directive({selector: ` - [gdColumns], - [gdColumns.xs], [gdColumns.sm], [gdColumns.md], [gdColumns.lg], [gdColumns.xl], - [gdColumns.lt-sm], [gdColumns.lt-md], [gdColumns.lt-lg], [gdColumns.lt-xl], - [gdColumns.gt-xs], [gdColumns.gt-sm], [gdColumns.gt-md], [gdColumns.gt-lg] -`}) -export class GridColumnsDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdColumns') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdColumns.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdColumns.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdColumns.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdColumns.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdColumns.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdColumns.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdColumns.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdColumns.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdColumns.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdColumns.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdColumns.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdColumns.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdColumns.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - @Input('gdInline') set inline(val: string) { this._cacheInput('inline', coerceBooleanProperty(val)); }; +export interface GridColumnsParent { + inline: boolean; +} - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); - } +@Injectable({providedIn: 'root'}) +export class GridColumnsStyleBuilder extends StyleBuilder { + buildStyles(input: string, parent: GridColumnsParent) { + input = input || DEFAULT_VALUE; + let auto = false; + if (input.endsWith(AUTO_SPECIFIER)) { + input = input.substring(0, input.indexOf(AUTO_SPECIFIER)); + auto = true; + } - // ********************************************* - // Lifecycle Methods - // ********************************************* + const css = { + 'display': parent.inline ? 'inline-grid' : 'grid', + 'grid-auto-columns': '', + 'grid-template-columns': '', + }; + const key = (auto ? 'grid-auto-columns' : 'grid-template-columns'); + css[key] = input; - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } + return css; } +} - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); +export class GridColumnsDirective extends BaseDirective2 { + protected DIRECTIVE_KEY = 'grid-columns'; + + @Input('gdInline') + get inline(): boolean { return this._inline; } + set inline(val: boolean) { this._inline = coerceBooleanProperty(val); } + protected _inline = false; + + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridColumnsStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); } // ********************************************* // Protected methods // ********************************************* - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this._applyStyleToElement(this._buildCSS(value)); + protected updateWithValue(value: string) { + this.styleCache = this.inline ? columnsInlineCache : columnsCache; + this.addStyles(value, {inline: this.inline}); } +} +const columnsCache: Map = new Map(); +const columnsInlineCache: Map = new Map(); - protected _buildCSS(value: string = '') { - let auto = false; - if (value.endsWith(AUTO_SPECIFIER)) { - value = value.substring(0, value.indexOf(AUTO_SPECIFIER)); - auto = true; - } +const inputs = [ + 'gdColumns', + 'gdColumns.xs', 'gdColumns.sm', 'gdColumns.md', 'gdColumns.lg', 'gdColumns.xl', + 'gdColumns.lt-sm', 'gdColumns.lt-md', 'gdColumns.lt-lg', 'gdColumns.lt-xl', + 'gdColumns.gt-xs', 'gdColumns.gt-sm', 'gdColumns.gt-md', 'gdColumns.gt-lg' +]; - let css = { - 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', - 'grid-auto-columns': '', - 'grid-template-columns': '', - }; - const key = (auto ? 'grid-auto-columns' : 'grid-template-columns'); - css[key] = value; +const selector = ` + [gdColumns], + [gdColumns.xs], [gdColumns.sm], [gdColumns.md], [gdColumns.lg], [gdColumns.xl], + [gdColumns.lt-sm], [gdColumns.lt-md], [gdColumns.lt-lg], [gdColumns.lt-xl], + [gdColumns.gt-xs], [gdColumns.gt-sm], [gdColumns.gt-md], [gdColumns.gt-lg] +`; - return css; - } +/** + * 'grid-template-columns' CSS Grid styling directive + * Configures the sizing for the columns in the grid + * Syntax: [auto] + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-13 + */ +@Directive({selector, inputs}) +export class DefaultGridColumnsDirective extends GridColumnsDirective { + protected inputs = inputs; } diff --git a/src/lib/grid/gap/gap.ts b/src/lib/grid/gap/gap.ts index 4a5b27167..2ff7fbd52 100644 --- a/src/lib/grid/gap/gap.ts +++ b/src/lib/grid/gap/gap.ts @@ -5,106 +5,85 @@ * 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 {Directive, ElementRef, Input, Optional, Injectable} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + BaseDirective2, + StyleUtils, + MediaMarshaller, + StyleBuilder, + StyleDefinition, +} from '@angular/flex-layout/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -const CACHE_KEY = 'gap'; const DEFAULT_VALUE = '0'; -/** - * 'grid-gap' CSS Grid styling directive - * Configures the gap between items in the grid - * Syntax: [] - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-17 - */ -@Directive({selector: ` - [gdGap], - [gdGap.xs], [gdGap.sm], [gdGap.md], [gdGap.lg], [gdGap.xl], - [gdGap.lt-sm], [gdGap.lt-md], [gdGap.lt-lg], [gdGap.lt-xl], - [gdGap.gt-xs], [gdGap.gt-sm], [gdGap.gt-md], [gdGap.gt-lg] -`}) -export class GridGapDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdGap') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdGap.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdGap.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdGap.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdGap.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdGap.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdGap.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdGap.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdGap.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdGap.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdGap.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdGap.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdGap.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdGap.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - @Input('gdInline') set inline(val: string) { this._cacheInput('inline', coerceBooleanProperty(val)); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); - } - - // ********************************************* - // Lifecycle Methods - // ********************************************* +export interface GridGapParent { + inline: boolean; +} - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } +@Injectable({providedIn: 'root'}) +export class GridGapStyleBuilder extends StyleBuilder { + buildStyles(input: string, parent: GridGapParent) { + return { + 'display': parent.inline ? 'inline-grid' : 'grid', + 'grid-gap': input || DEFAULT_VALUE + }; } +} - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); +export class GridGapDirective extends BaseDirective2 { + protected DIRECTIVE_KEY = 'grid-gap'; + + @Input('gdInline') + get inline(): boolean { return this._inline; } + set inline(val: boolean) { this._inline = coerceBooleanProperty(val); } + protected _inline = false; + + constructor(protected elRef: ElementRef, + protected styleUtils: StyleUtils, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridGapStyleBuilder, + protected marshal: MediaMarshaller) { + super(elRef, styleBuilder, styleUtils, marshal); + this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); } // ********************************************* // Protected methods // ********************************************* - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this._applyStyleToElement(this._buildCSS(value)); + protected updateWithValue(value: string) { + this.styleCache = this.inline ? gapInlineCache : gapCache; + this.addStyles(value, {inline: this.inline}); } +} +const gapCache: Map = new Map(); +const gapInlineCache: Map = new Map(); - protected _buildCSS(value: string = '') { - return { - 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', - 'grid-gap': value - }; - } +const inputs = [ + 'gdGap', + 'gdGap.xs', 'gdGap.sm', 'gdGap.md', 'gdGap.lg', 'gdGap.xl', + 'gdGap.lt-sm', 'gdGap.lt-md', 'gdGap.lt-lg', 'gdGap.lt-xl', + 'gdGap.gt-xs', 'gdGap.gt-sm', 'gdGap.gt-md', 'gdGap.gt-lg' +]; + +const selector = ` + [gdGap], + [gdGap.xs], [gdGap.sm], [gdGap.md], [gdGap.lg], [gdGap.xl], + [gdGap.lt-sm], [gdGap.lt-md], [gdGap.lt-lg], [gdGap.lt-xl], + [gdGap.gt-xs], [gdGap.gt-sm], [gdGap.gt-md], [gdGap.gt-lg] +`; + +/** + * 'grid-gap' CSS Grid styling directive + * Configures the gap between items in the grid + * Syntax: [] + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-17 + */ +@Directive({selector, inputs}) +export class DefaultGridGapDirective extends GridGapDirective { + protected inputs = inputs; } diff --git a/src/lib/grid/grid-align/grid-align.ts b/src/lib/grid/grid-align/grid-align.ts index c33f31439..ec608aaa0 100644 --- a/src/lib/grid/grid-align/grid-align.ts +++ b/src/lib/grid/grid-align/grid-align.ts @@ -5,142 +5,112 @@ * 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 {Directive, ElementRef, Injectable, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnChanges, - OnDestroy, - OnInit, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + MediaMarshaller, + BaseDirective2, + StyleBuilder, + StyleDefinition, + StyleUtils, +} from '@angular/flex-layout/core'; -const CACHE_KEY = 'align'; const ROW_DEFAULT = 'stretch'; const COL_DEFAULT = 'stretch'; -/** - * 'align' CSS Grid styling directive for grid children - * Defines positioning of child elements along row and column axis in a grid container - * Optional values: {row-axis} values or {row-axis column-axis} value pairs - * - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#prop-justify-self - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#prop-align-self - */ -@Directive({selector: ` - [gdGridAlign], - [gdGridAlign.xs], [gdGridAlign.sm], [gdGridAlign.md], [gdGridAlign.lg],[gdGridAlign.xl], - [gdGridAlign.lt-sm], [gdGridAlign.lt-md], [gdGridAlign.lt-lg], [gdGridAlign.lt-xl], - [gdGridAlign.gt-xs], [gdGridAlign.gt-sm], [gdGridAlign.gt-md], [gdGridAlign.gt-lg] -`}) -export class GridAlignDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdGridAlign') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdGridAlign.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdGridAlign.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdGridAlign.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdGridAlign.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdGridAlign.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdGridAlign.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdGridAlign.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdGridAlign.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdGridAlign.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdGridAlign.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdGridAlign.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdGridAlign.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdGridAlign.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); +@Injectable({providedIn: 'root'}) +export class GridAlignStyleBuilder extends StyleBuilder { + buildStyles(input: string) { + return buildCss(input || ROW_DEFAULT); } +} - // ********************************************* - // Lifecycle Methods - // ********************************************* - - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } - } +export class GridAlignDirective extends BaseDirective2 { - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); + protected DIRECTIVE_KEY = 'grid-align'; - this._listenForMediaQueryChanges(CACHE_KEY, ROW_DEFAULT, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridAlignStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.addStyles.bind(this)); } - // ********************************************* - // Protected methods - // ********************************************* + protected styleCache = alignCache; +} - /** - * - */ - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || ROW_DEFAULT; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } +const alignCache: Map = new Map(); - this._applyStyleToElement(this._buildCSS(value)); - } +const inputs = [ + 'gdGridAlign', + 'gdGridAlign.xs', 'gdGridAlign.sm', 'gdGridAlign.md', 'gdGridAlign.lg', 'gdGridAlign.xl', + 'gdGridAlign.lt-sm', 'gdGridAlign.lt-md', 'gdGridAlign.lt-lg', 'gdGridAlign.lt-xl', + 'gdGridAlign.gt-xs', 'gdGridAlign.gt-sm', 'gdGridAlign.gt-md', 'gdGridAlign.gt-lg' +]; - protected _buildCSS(align: string = '') { - let css: {[key: string]: string} = {}, [rowAxis, columnAxis] = align.split(' '); +const selector = ` + [gdGridAlign], + [gdGridAlign.xs], [gdGridAlign.sm], [gdGridAlign.md], [gdGridAlign.lg],[gdGridAlign.xl], + [gdGridAlign.lt-sm], [gdGridAlign.lt-md], [gdGridAlign.lt-lg], [gdGridAlign.lt-xl], + [gdGridAlign.gt-xs], [gdGridAlign.gt-sm], [gdGridAlign.gt-md], [gdGridAlign.gt-lg] +`; - // Row axis - switch (rowAxis) { - case 'end': - css['justify-self'] = 'end'; - break; - case 'center': - css['justify-self'] = 'center'; - break; - case 'stretch': - css['justify-self'] = 'stretch'; - break; - case 'start': - css['justify-self'] = 'start'; - break; - default: - css['justify-self'] = ROW_DEFAULT; // default row axis - break; - } +/** + * 'align' CSS Grid styling directive for grid children + * Defines positioning of child elements along row and column axis in a grid container + * Optional values: {row-axis} values or {row-axis column-axis} value pairs + * + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#prop-justify-self + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#prop-align-self + */ +@Directive({selector, inputs}) +export class DefaultGridAlignDirective extends GridAlignDirective { + protected inputs = inputs; +} - // Column axis - switch (columnAxis) { - case 'end': - css['align-self'] = 'end'; - break; - case 'center': - css['align-self'] = 'center'; - break; - case 'stretch': - css['align-self'] = 'stretch'; - break; - case 'start': - css['align-self'] = 'start'; - break; - default: - css['align-self'] = COL_DEFAULT; // default column axis - break; - } +function buildCss(align: string = '') { + const css: {[key: string]: string} = {}, [rowAxis, columnAxis] = align.split(' '); + + // Row axis + switch (rowAxis) { + case 'end': + css['justify-self'] = 'end'; + break; + case 'center': + css['justify-self'] = 'center'; + break; + case 'stretch': + css['justify-self'] = 'stretch'; + break; + case 'start': + css['justify-self'] = 'start'; + break; + default: + css['justify-self'] = ROW_DEFAULT; // default row axis + break; + } - return css; + // Column axis + switch (columnAxis) { + case 'end': + css['align-self'] = 'end'; + break; + case 'center': + css['align-self'] = 'center'; + break; + case 'stretch': + css['align-self'] = 'stretch'; + break; + case 'start': + css['align-self'] = 'start'; + break; + default: + css['align-self'] = COL_DEFAULT; // default column axis + break; } + + return css; } diff --git a/src/lib/grid/module.ts b/src/lib/grid/module.ts index 6bcf949e5..ae40949b4 100644 --- a/src/lib/grid/module.ts +++ b/src/lib/grid/module.ts @@ -8,31 +8,31 @@ import {NgModule} from '@angular/core'; import {CoreModule} from '@angular/flex-layout/core'; -import {GridAlignDirective} from './grid-align/grid-align'; -import {GridAlignColumnsDirective} from './align-columns/align-columns'; -import {GridAlignRowsDirective} from './align-rows/align-rows'; -import {GridAreaDirective} from './area/area'; -import {GridAreasDirective} from './areas/areas'; -import {GridAutoDirective} from './auto/auto'; -import {GridColumnDirective} from './column/column'; -import {GridColumnsDirective} from './columns/columns'; -import {GridGapDirective} from './gap/gap'; -import {GridRowDirective} from './row/row'; -import {GridRowsDirective} from './rows/rows'; +import {DefaultGridAlignDirective} from './grid-align/grid-align'; +import {DefaultGridAlignColumnsDirective} from './align-columns/align-columns'; +import {DefaultGridAlignRowsDirective} from './align-rows/align-rows'; +import {DefaultGridAreaDirective} from './area/area'; +import {DefaultGridAreasDirective} from './areas/areas'; +import {DefaultGridAutoDirective} from './auto/auto'; +import {DefaultGridColumnDirective} from './column/column'; +import {DefaultGridColumnsDirective} from './columns/columns'; +import {DefaultGridGapDirective} from './gap/gap'; +import {DefaultGridRowDirective} from './row/row'; +import {DefaultGridRowsDirective} from './rows/rows'; const ALL_DIRECTIVES = [ - GridAlignDirective, - GridAlignColumnsDirective, - GridAlignRowsDirective, - GridAreaDirective, - GridAreasDirective, - GridAutoDirective, - GridColumnDirective, - GridColumnsDirective, - GridGapDirective, - GridRowDirective, - GridRowsDirective, + DefaultGridAlignDirective, + DefaultGridAlignColumnsDirective, + DefaultGridAlignRowsDirective, + DefaultGridAreaDirective, + DefaultGridAreasDirective, + DefaultGridAutoDirective, + DefaultGridColumnDirective, + DefaultGridColumnsDirective, + DefaultGridGapDirective, + DefaultGridRowDirective, + DefaultGridRowsDirective, ]; /** diff --git a/src/lib/grid/row/row.ts b/src/lib/grid/row/row.ts index 664009ccc..3a6e651ce 100644 --- a/src/lib/grid/row/row.ts +++ b/src/lib/grid/row/row.ts @@ -5,99 +5,63 @@ * 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 {Directive, ElementRef, Optional, Injectable} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + BaseDirective2, + StyleUtils, + MediaMarshaller, + StyleBuilder, + StyleDefinition, +} from '@angular/flex-layout/core'; -const CACHE_KEY = 'row'; const DEFAULT_VALUE = 'auto'; -/** - * 'grid-row' CSS Grid styling directive - * Configures the name or position of an element within the grid - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-26 - */ -@Directive({selector: ` - [gdRow], - [gdRow.xs], [gdRow.sm], [gdRow.md], [gdRow.lg], [gdRow.xl], - [gdRow.lt-sm], [gdRow.lt-md], [gdRow.lt-lg], [gdRow.lt-xl], - [gdRow.gt-xs], [gdRow.gt-sm], [gdRow.gt-md], [gdRow.gt-lg] -`}) -export class GridRowDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdRow') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdRow.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdRow.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdRow.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdRow.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdRow.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdRow.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdRow.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdRow.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdRow.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdRow.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdRow.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdRow.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdRow.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); - } - - // ********************************************* - // Lifecycle Methods - // ********************************************* - - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } +@Injectable({providedIn: 'root'}) +export class GridRowStyleBuilder extends StyleBuilder { + buildStyles(input: string) { + return {'grid-row': input || DEFAULT_VALUE}; } +} - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); +export class GridRowDirective extends BaseDirective2 { + protected DIRECTIVE_KEY = 'grid-row'; - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridRowStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.addStyles.bind(this)); } - // ********************************************* - // Protected methods - // ********************************************* + protected styleCache = rowCache; +} - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } +const rowCache: Map = new Map(); - this._applyStyleToElement(this._buildCSS(value)); - } +const inputs = [ + 'gdRow', + 'gdRow.xs', 'gdRow.sm', 'gdRow.md', 'gdRow.lg', 'gdRow.xl', + 'gdRow.lt-sm', 'gdRow.lt-md', 'gdRow.lt-lg', 'gdRow.lt-xl', + 'gdRow.gt-xs', 'gdRow.gt-sm', 'gdRow.gt-md', 'gdRow.gt-lg' +]; +const selector = ` + [gdRow], + [gdRow.xs], [gdRow.sm], [gdRow.md], [gdRow.lg], [gdRow.xl], + [gdRow.lt-sm], [gdRow.lt-md], [gdRow.lt-lg], [gdRow.lt-xl], + [gdRow.gt-xs], [gdRow.gt-sm], [gdRow.gt-md], [gdRow.gt-lg] +`; - protected _buildCSS(value: string = '') { - return {'grid-row': value}; - } +/** + * 'grid-row' CSS Grid styling directive + * Configures the name or position of an element within the grid + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-26 + */ +@Directive({selector, inputs}) +export class DefaultGridRowDirective extends GridRowDirective { + protected inputs = inputs; } diff --git a/src/lib/grid/rows/rows.ts b/src/lib/grid/rows/rows.ts index ce69fc560..89ec55a1b 100644 --- a/src/lib/grid/rows/rows.ts +++ b/src/lib/grid/rows/rows.ts @@ -5,118 +5,98 @@ * 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 {Directive, ElementRef, Input, Injectable, Optional} from '@angular/core'; import { - Directive, - ElementRef, - Input, - OnInit, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; + MediaMarshaller, + BaseDirective2, + StyleBuilder, + StyleDefinition, + StyleUtils, +} from '@angular/flex-layout/core'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -const CACHE_KEY = 'rows'; const DEFAULT_VALUE = 'none'; const AUTO_SPECIFIER = '!'; -/** - * 'grid-template-rows' CSS Grid styling directive - * Configures the sizing for the rows in the grid - * Syntax: [auto] - * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-13 - */ -@Directive({selector: ` - [gdRows], - [gdRows.xs], [gdRows.sm], [gdRows.md], [gdRows.lg], [gdRows.xl], - [gdRows.lt-sm], [gdRows.lt-md], [gdRows.lt-lg], [gdRows.lt-xl], - [gdRows.gt-xs], [gdRows.gt-sm], [gdRows.gt-md], [gdRows.gt-lg] -`}) -export class GridRowsDirective extends BaseDirective implements OnInit, OnChanges, OnDestroy { - - /* tslint:disable */ - @Input('gdRows') set align(val: string) { this._cacheInput(`${CACHE_KEY}`, val); } - @Input('gdRows.xs') set alignXs(val: string) { this._cacheInput(`${CACHE_KEY}Xs`, val); } - @Input('gdRows.sm') set alignSm(val: string) { this._cacheInput(`${CACHE_KEY}Sm`, val); }; - @Input('gdRows.md') set alignMd(val: string) { this._cacheInput(`${CACHE_KEY}Md`, val); }; - @Input('gdRows.lg') set alignLg(val: string) { this._cacheInput(`${CACHE_KEY}Lg`, val); }; - @Input('gdRows.xl') set alignXl(val: string) { this._cacheInput(`${CACHE_KEY}Xl`, val); }; - - @Input('gdRows.gt-xs') set alignGtXs(val: string) { this._cacheInput(`${CACHE_KEY}GtXs`, val); }; - @Input('gdRows.gt-sm') set alignGtSm(val: string) { this._cacheInput(`${CACHE_KEY}GtSm`, val); }; - @Input('gdRows.gt-md') set alignGtMd(val: string) { this._cacheInput(`${CACHE_KEY}GtMd`, val); }; - @Input('gdRows.gt-lg') set alignGtLg(val: string) { this._cacheInput(`${CACHE_KEY}GtLg`, val); }; - - @Input('gdRows.lt-sm') set alignLtSm(val: string) { this._cacheInput(`${CACHE_KEY}LtSm`, val); }; - @Input('gdRows.lt-md') set alignLtMd(val: string) { this._cacheInput(`${CACHE_KEY}LtMd`, val); }; - @Input('gdRows.lt-lg') set alignLtLg(val: string) { this._cacheInput(`${CACHE_KEY}LtLg`, val); }; - @Input('gdRows.lt-xl') set alignLtXl(val: string) { this._cacheInput(`${CACHE_KEY}LtXl`, val); }; - - @Input('gdInline') set inline(val: string) { this._cacheInput('inline', coerceBooleanProperty(val)); }; +export interface GridRowsParent { + inline: boolean; +} - /* tslint:enable */ - constructor(monitor: MediaMonitor, - elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); - } +@Injectable({providedIn: 'root'}) +export class GridRowsStyleBuilder extends StyleBuilder { + buildStyles(input: string, parent: GridRowsParent) { + input = input || DEFAULT_VALUE; + let auto = false; + if (input.endsWith(AUTO_SPECIFIER)) { + input = input.substring(0, input.indexOf(AUTO_SPECIFIER)); + auto = true; + } - // ********************************************* - // Lifecycle Methods - // ********************************************* + const css = { + 'display': parent.inline ? 'inline-grid' : 'grid', + 'grid-auto-rows': '', + 'grid-template-rows': '', + }; + const key = (auto ? 'grid-auto-rows' : 'grid-template-rows'); + css[key] = input; - /** - * For @Input changes on the current mq activation property, see onMediaQueryChanges() - */ - ngOnChanges(changes: SimpleChanges) { - if (changes[CACHE_KEY] != null || this._mqActivation) { - this._updateWithValue(); - } + return css; } +} - /** - * After the initial onChanges, build an mqActivation object that bridges - * mql change events to onMediaQueryChange handlers - */ - ngOnInit() { - super.ngOnInit(); - - this._listenForMediaQueryChanges(CACHE_KEY, DEFAULT_VALUE, (changes: MediaChange) => { - this._updateWithValue(changes.value); - }); - this._updateWithValue(); +export class GridRowsDirective extends BaseDirective2 { + protected DIRECTIVE_KEY = 'grid-rows'; + + @Input('gdInline') + get inline(): boolean { return this._inline; } + set inline(val: boolean) { this._inline = coerceBooleanProperty(val); } + protected _inline = false; + + constructor(protected elementRef: ElementRef, + // NOTE: not actually optional, but we need to force DI without a + // constructor call + @Optional() protected styleBuilder: GridRowsStyleBuilder, + protected styler: StyleUtils, + protected marshal: MediaMarshaller) { + super(elementRef, styleBuilder, styler, marshal); + this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY, + this.updateWithValue.bind(this)); } // ********************************************* // Protected methods // ********************************************* - protected _updateWithValue(value?: string) { - value = value || this._queryInput(CACHE_KEY) || DEFAULT_VALUE; - if (this._mqActivation) { - value = this._mqActivation.activatedInput; - } - - this._applyStyleToElement(this._buildCSS(value)); + protected updateWithValue(value: string) { + this.styleCache = this.inline ? rowsInlineCache : rowsCache; + this.addStyles(value, {inline: this.inline}); } +} +const rowsCache: Map = new Map(); +const rowsInlineCache: Map = new Map(); - protected _buildCSS(value: string = '') { - let auto = false; - if (value.endsWith(AUTO_SPECIFIER)) { - value = value.substring(0, value.indexOf(AUTO_SPECIFIER)); - auto = true; - } +const inputs = [ + 'gdRows', + 'gdRows.xs', 'gdRows.sm', 'gdRows.md', 'gdRows.lg', 'gdRows.xl', + 'gdRows.lt-sm', 'gdRows.lt-md', 'gdRows.lt-lg', 'gdRows.lt-xl', + 'gdRows.gt-xs', 'gdRows.gt-sm', 'gdRows.gt-md', 'gdRows.gt-lg' +]; - let css = { - 'display': this._queryInput('inline') ? 'inline-grid' : 'grid', - 'grid-auto-rows': '', - 'grid-template-rows': '', - }; - const key = (auto ? 'grid-auto-rows' : 'grid-template-rows'); - css[key] = value; +const selector = ` + [gdRows], + [gdRows.xs], [gdRows.sm], [gdRows.md], [gdRows.lg], [gdRows.xl], + [gdRows.lt-sm], [gdRows.lt-md], [gdRows.lt-lg], [gdRows.lt-xl], + [gdRows.gt-xs], [gdRows.gt-sm], [gdRows.gt-md], [gdRows.gt-lg] +`; - return css; - } +/** + * 'grid-template-rows' CSS Grid styling directive + * Configures the sizing for the rows in the grid + * Syntax: [auto] + * @see https://css-tricks.com/snippets/css/complete-guide-grid/#article-header-id-13 + */ +@Directive({selector, inputs}) +export class DefaultGridRowsDirective extends GridRowsDirective { + protected inputs = inputs; } diff --git a/src/lib/server/server-provider.ts b/src/lib/server/server-provider.ts index cf1d6e30b..9c709a11a 100644 --- a/src/lib/server/server-provider.ts +++ b/src/lib/server/server-provider.ts @@ -15,7 +15,8 @@ import { BreakPoint, MatchMedia, StylesheetMap, - ServerMatchMedia + ServerMatchMedia, + prioritySort, } from '@angular/flex-layout/core'; @@ -24,7 +25,7 @@ import { * retrieve the associated stylings from the virtual stylesheet * @param serverSheet the virtual stylesheet that stores styles for each * element - * @param matchMedia the service to activate/deactive breakpoints + * @param matchMedia the service to activate/deactivate breakpoints * @param breakpoints the registered breakpoints to activate/deactivate */ export function generateStaticFlexLayoutStyles(serverSheet: StylesheetMap, @@ -41,6 +42,7 @@ export function generateStaticFlexLayoutStyles(serverSheet: StylesheetMap, const defaultStyles = new Map(serverSheet.stylesheet); let styleText = generateCss(defaultStyles, 'all', classMap); + breakpoints.sort(prioritySort); breakpoints.reverse(); breakpoints.forEach((bp, i) => { serverSheet.clearStyles(); diff --git a/yarn.lock b/yarn.lock index 5083b64df..2cefe6a2d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,32 +3,32 @@ "@angular/animations@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.1.1.tgz#8fecbd19417364946a9ea40c8fdf32462110232f" - integrity sha512-iTNxhPPraCZsE4rgM23lguT1kDV4mfYAr+Bsi5J0+v9ZJA+VaKvi6eRW8ZGrx4/rDz6hzTnBn1jgPppHFbsOcw== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.1.2.tgz#876598802a2722b97d7aa9ea092d3aadc05c1fa8" + integrity sha512-zCLzPpifD4V9C35+DG75yHiAxZrWmk7n7dudxchKXf/YpgzV1M43lTSxna6YZgMLIXRjilfjfh6jqOOP+PctoQ== dependencies: tslib "^1.9.0" "@angular/cdk@^7.0.3": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-7.1.0.tgz#7e5c3e3631947ef91413a83997ec4edec92cdd1c" - integrity sha512-dY740pKcIRtKr6n6NomrgqfdEj988urTZ9I/bfJjxF5fdhnSjyhEvDlB55EHsrF+bTTZbZXRmv7AwOQ9GJnD9w== + version "7.1.1" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-7.1.1.tgz#6916b2303af5e0a56a9b64ff4d54642cf191c54f" + integrity sha512-woW9lWDBKRuxZipMzWofrAY7YpuZd4vf/J1YPjmAqV7U94MaDFyizRLyFolbTZVYo8ggh9U3SQAWRrEBvJNsjg== dependencies: tslib "^1.7.1" optionalDependencies: parse5 "^5.0.0" "@angular/common@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.1.1.tgz#f78f884614ef81ab2fd648f1aa3e83aae370a6c8" - integrity sha512-SngekFx9v39sjgi9pON0Wehxpu+NdUk7OEebw4Fa8dKqTgydTkuhmnNH+9WQe264asoeCt51oufPRjIqMLNohA== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.1.2.tgz#506e48b18dd8c9dd8c97e61585ad0647079e6c05" + integrity sha512-Ss9OilnbKpfkkwa1spUUAzgtGgd76j+Cgp1ecBBaueBoHyDZcSwD3Ioe5/91mjGF8i/MmpoBtEmk569fwmb7iQ== dependencies: tslib "^1.9.0" "@angular/compiler-cli@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-7.1.1.tgz#c5f6225fb72b56f42fa78c332fdee9755c64604e" - integrity sha512-4NXlkDhOEQgaP3Agigqw93CvXJvsfnXa0xiglq9e/wjL+6XbtM9WcDb5lfRQz41N9RSkO3pEHGvKMweKZGgogA== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-7.1.2.tgz#8bdf883b6e529ccd8111f1adc92c3323fa11d093" + integrity sha512-u686o7eOPxSokE3l+lpSMs+sGRTLiGBXGsTuNR891XPN8+E5ep7NHgimeLizVXlbwIYZiNtcQ9zRbhEsMI2ErQ== dependencies: canonical-path "1.0.0" chokidar "^1.4.2" @@ -43,67 +43,67 @@ yargs "9.0.1" "@angular/compiler@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.1.1.tgz#4efbcad27ab43d4cd36d936a8df2e073f6d02d0a" - integrity sha512-oJvBe8XZ+DXF/W/DxWBTbBcixJTuPeZWdkcZIGWhJoQP7K5GnGnj8ffP9Lp6Dh4TKv85awtC6OfIKhbHxa650Q== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.1.2.tgz#def5a55616ef805963288b9d9402d87411d93e7a" + integrity sha512-ua6Wh+c5XzxAeJT6guwAFYnwa1XzJpncppUrceRXIS9VAn9X7ApxRr45DvbVeYwXBb1iNdHWtZFm1koFVQpydA== dependencies: tslib "^1.9.0" "@angular/core@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.1.1.tgz#9748b0103cd86226554e1ccbd0f43dd8c46f1ed1" - integrity sha512-Osig5SRgDRQ+Hec/liN7nq/BCJieB+4/pqRh9rFbOXezb2ptgRZqdXOXN8P17i4AwPVf308Mh55V0niJ5Eu3Rw== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.1.2.tgz#08f90d8e6ecb26c10cc21e2e2e4bdca5d0b7d7a5" + integrity sha512-k3hKz6oj5KAaU/R034flxa73MWoR1SBBZPbpqK5zncIYbZMxvUQDgD3O7SNdQfI9G534SzdJk3AqJNEDTFUyYA== dependencies: tslib "^1.9.0" "@angular/forms@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.1.1.tgz#d16ef10a901c007062fd19144cd77917ef55ee24" - integrity sha512-yCWuPjpu23Wc3XUw7v/ACNn/e249oT0bYlM8aaMQ1F5OwrmmC4NJC12Rpl9Ihza61RIHIKzNcHVEgzc7WhcSag== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-7.1.2.tgz#17a427e27f496e41677be05c5d8033899db9e942" + integrity sha512-L7LtvjcZUf4DjeDKQnxm+AzC9VkmR3I+hnezyvkLT7oUHcHEpYgNtiLmNM4Ir7ZI3zuaSMmHlEBlnDn0YJlcvA== dependencies: tslib "^1.9.0" "@angular/http@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/http/-/http-7.1.1.tgz#f19f17ad42e7f3cdabcf1250ca757640d0f02219" - integrity sha512-pRk+c/kz9aJ8te5xzCxlPLpFnwB0d/E9YkOo3/ydaXF9vZw13RTzk00YyzJ41PDzJf8oPDdXtueTQ+vtJ7Srtw== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/http/-/http-7.1.2.tgz#928fd412c39a79fec3e67b01601d428c9cc8d43d" + integrity sha512-8wscCWG+Cd+/IKniYrBViMFWFZFNh8eEkmUAucPInwmcSFyY//ZLWd2WJLEqbclAGT7kOkTOdUjJ6eMnnWAFuw== dependencies: tslib "^1.9.0" "@angular/material@^7.0.3": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@angular/material/-/material-7.1.0.tgz#441f2d03a97a1fa35546b61dcde86db34f0f5efc" - integrity sha512-bgotNpSfGLjNZ1AcTyhs6XS7trF4I7UHwQmfa0l8y3Gf9plwErPDfQe2XqnayRyG9nTwHj9f1lQ45X5mr3/0/A== + version "7.1.1" + resolved "https://registry.yarnpkg.com/@angular/material/-/material-7.1.1.tgz#147b7565262e04d504c8dab7317632967a3db02a" + integrity sha512-wjYAWsdWpb8/BgoIfoUomnycoljU00avJ3hRIgPNnEpZhB7zqiBA8tCitzDS4NK8dKBJjM9WRAOj6yl6x3+9wA== dependencies: tslib "^1.7.1" "@angular/platform-browser-dynamic@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.1.1.tgz#6945298446173338782f437a996226110cda0d3e" - integrity sha512-ZIu48Vn4S6gjD7CMbGlKGaPQ8v9rYkWzlNYi4vTYzgiqKKNC3hqLsVESU3mSvr5oeQBxSIBidTdHSyafHFrA2w== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.1.2.tgz#1eb7a08f011e911817c9cdfaa560523cedba959a" + integrity sha512-DoQ+840d3YSC34NnCVD+NlQOyes56+Re9V62ZViXKSwsWtpqgsYBiUW5yYHCO8bruS+Kn+BGTCK/w7/KEM60tg== dependencies: tslib "^1.9.0" "@angular/platform-browser@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.1.1.tgz#a6bd408f656dc43ee5a2d8af3dfaa786c7c1dfca" - integrity sha512-I6OPjecynGJSbPtzu0gvEgSmIR6X6/xEAhg4L9PycW1ryjzptTC9klWRTWIqsIBqMxhVnY44uKLeRNrDwMOwyA== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.1.2.tgz#7f1fa3b59473b1ddaaa658bf064cf512f27c6578" + integrity sha512-cxFCqOXfLznHNI3dfnKcSCokbuSrxSLlXdE4uqoZliTRQIC9/ccrxVdx4UbJjtSgWFaNG1ocxH0rckgcUEG/kg== dependencies: tslib "^1.9.0" "@angular/platform-server@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-7.1.1.tgz#adecd03e586d2f62fee7417e68897067e59aa686" - integrity sha512-wG9Xk8R6rWkWIfDQ31IwrL7gAu0gRx/vJW2We1kz1hHKOqnCB22qOTEaFY0hPfKFlwvwcZknm4KFXAizA8ub4A== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-7.1.2.tgz#2ffac676e752125a9d70d01af309e6105f7cd903" + integrity sha512-w7cIzWP9tc2xAdo7r81W3UNDyYO8y37lz8G+o9QyFRj4/bdwsVyqmpwKM/NHlsCt/Gce0gNjgPAlhU9gug780Q== dependencies: domino "^2.1.0" tslib "^1.9.0" xhr2 "^0.1.4" "@angular/router@^7.0.3": - version "7.1.1" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-7.1.1.tgz#80a4cdffc03a529b73485c2ad63a30ec435364ea" - integrity sha512-jbnqEq/1iDBkeH8Vn13hauGPTzhwllWM+MLfmdNGTiMzGRx4pmkWa57seDOeBF/GNYBL9JjkWTCrkKFAc2FJKw== + version "7.1.2" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-7.1.2.tgz#3a322f6ee912d309e5f65eb10c7db6cf376d82c8" + integrity sha512-Lht4hcbx2hAtUEcJ1YG4Q63bukKrDHxqSnELMYi1/G5y5vH8LWPQX7aoEcOJeaQWQTKroQBAIeprol/h9vkvoQ== dependencies: tslib "^1.9.0" @@ -115,17 +115,17 @@ "@babel/highlight" "^7.0.0" "@babel/core@^7.1.2": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.1.6.tgz#3733cbee4317429bc87c62b29cf8587dba7baeb3" - integrity sha512-Hz6PJT6e44iUNpAn8AoyAs6B3bl60g7MJQaI0rZEar6ECzh6+srYO1xlIdssio34mPaUtAb1y+XlkkSJzok3yw== + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.2.0.tgz#a4dd3814901998e93340f0086e9867fefa163ada" + integrity sha512-7pvAdC4B+iKjFFp9Ztj0QgBndJ++qaMeonT185wAqUnhipw8idm9Rv1UMyBuKtYjfl6ORNkgEgcsYLfHX/GpLw== dependencies: "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.1.6" - "@babel/helpers" "^7.1.5" - "@babel/parser" "^7.1.6" + "@babel/generator" "^7.2.0" + "@babel/helpers" "^7.2.0" + "@babel/parser" "^7.2.0" "@babel/template" "^7.1.2" "@babel/traverse" "^7.1.6" - "@babel/types" "^7.1.6" + "@babel/types" "^7.2.0" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -134,12 +134,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.1.6": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.1.6.tgz#001303cf87a5b9d093494a4bf251d7b5d03d3999" - integrity sha512-brwPBtVvdYdGxtenbQgfCdDPmtkmUBZPjUoK5SXJEBuHaA5BCubh9ly65fzXz7R6o5rA76Rs22ES8Z+HCc0YIQ== +"@babel/generator@^7.1.6", "@babel/generator@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.0.tgz#eaf3821fa0301d9d4aef88e63d4bcc19b73ba16c" + integrity sha512-BA75MVfRlFQG2EZgFYIwyT1r6xSkwfP2bdkY/kLZusEYWiJs4xCowab/alaEaT0wSvmVuXGqiefeBlP+7V1yKg== dependencies: - "@babel/types" "^7.1.6" + "@babel/types" "^7.2.0" jsesc "^2.5.1" lodash "^4.17.10" source-map "^0.5.0" @@ -168,14 +168,14 @@ dependencies: "@babel/types" "^7.0.0" -"@babel/helpers@^7.1.5": - version "7.1.5" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.1.5.tgz#68bfc1895d685f2b8f1995e788dbfe1f6ccb1996" - integrity sha512-2jkcdL02ywNBry1YNFAH/fViq4fXG0vdckHqeJk+75fpQ2OH+Az6076tX/M0835zA45E0Cqa6pV5Kiv9YOqjEg== +"@babel/helpers@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.2.0.tgz#8335f3140f3144270dc63c4732a4f8b0a50b7a21" + integrity sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A== dependencies: "@babel/template" "^7.1.2" "@babel/traverse" "^7.1.5" - "@babel/types" "^7.1.5" + "@babel/types" "^7.2.0" "@babel/highlight@^7.0.0": version "7.0.0" @@ -186,10 +186,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.2", "@babel/parser@^7.1.6": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.6.tgz#16e97aca1ec1062324a01c5a6a7d0df8dd189854" - integrity sha512-dWP6LJm9nKT6ALaa+bnL247GHHMWir3vSlZ2+IHgHgktZQx0L3Uvq2uAWcuzIe+fujRsYWBW2q622C5UvGK9iQ== +"@babel/parser@^7.0.0", "@babel/parser@^7.1.2", "@babel/parser@^7.1.6", "@babel/parser@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.0.tgz#02d01dbc330b6cbf36b76ac93c50752c69027065" + integrity sha512-M74+GvK4hn1eejD9lZ7967qAwvqTZayQa3g10ag4s9uewgR7TKjeaT0YMyoq+gVfKYABiWZ4MQD701/t5e1Jhg== "@babel/template@^7.1.0", "@babel/template@^7.1.2": version "7.1.2" @@ -215,10 +215,10 @@ globals "^11.1.0" lodash "^4.17.10" -"@babel/types@^7.0.0", "@babel/types@^7.1.2", "@babel/types@^7.1.5", "@babel/types@^7.1.6": - version "7.1.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.6.tgz#0adb330c3a281348a190263aceb540e10f04bcce" - integrity sha512-DMiUzlY9DSjVsOylJssxLHSgj6tWM9PRFJOGW/RaOglVOK9nzTxoOMfTfRQXGUCUQ/HmlG2efwC+XqUEJ5ay4w== +"@babel/types@^7.0.0", "@babel/types@^7.1.2", "@babel/types@^7.1.6", "@babel/types@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.0.tgz#7941c5b2d8060e06f9601d6be7c223eef906d5d8" + integrity sha512-b4v7dyfApuKDvmPb+O488UlGuR1WbwMXFsO/cyqMrnfvRAChZKJAYeeglWTjUO1b9UghKKgepAQM5tsvBJca6A== dependencies: esutils "^2.0.2" lodash "^4.17.10" @@ -240,17 +240,17 @@ tslib "1.9.0" xmlhttprequest "1.8.0" -"@firebase/auth-types@0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.3.4.tgz#253b1b2d9b520a0b945d4617c8418f0f19a4159f" - integrity sha512-0r3gSQk9jw5orFHCTUIgao0zan6dHt2J0BO3t/uEzbod+uwqvUn/gh+yg+kK6HX92Fg8E7y030KX4Bw/aXt0Ew== +"@firebase/auth-types@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.5.0.tgz#5620308993d524e5b4f0d084372ad7e7dc7dee55" + integrity sha512-DSjtsIjTy5RSiWyGHqiGkLcDgEgFEf2aD2O0t/0+lHmAzxUGrJFO5+IkPNV6i0ffmtiJaXQDJ7z7q4OdypDBCg== -"@firebase/auth@0.7.9": - version "0.7.9" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.7.9.tgz#f915f8eeb6ee16b827518f48a0fa529803d39e4e" - integrity sha512-m8e2KZ/WvToTMovoBI5K3N0Ku8Aqt6083oqzQODUYHjf94KsAfGdoQEaJono7T7vK4o7E5xpqFgFldOM5LdgiQ== +"@firebase/auth@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.9.0.tgz#c815f36c9b0e494c50a15b93ce0d958f4eed0f29" + integrity sha512-pNxfxFr4/tJluGfmPYUiuy2Tq/ZSRniJlWP4fj1mg+a9r+KevheeISZdWEZwJMosieXeCg+BzY0tvFD9j5ZrDw== dependencies: - "@firebase/auth-types" "0.3.4" + "@firebase/auth-types" "0.5.0" "@firebase/database-types@0.3.2": version "0.3.2" @@ -268,20 +268,20 @@ faye-websocket "0.11.1" tslib "1.9.0" -"@firebase/firestore-types@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-0.7.0.tgz#bded7892868cf6b189a121d0f0cec468f1609995" - integrity sha512-jyKRcKnSh3CSEPL4xGOZNoOXEiv7YmFK/JEcdd/4cAH17/Xo+Pk67gk1E648LRKh6QPghgNvzNTY5R10mKbQNw== +"@firebase/firestore-types@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-0.8.0.tgz#a40b2ed03eda9c189af145acdff408e562976345" + integrity sha512-FdLy2TbZ6aAeT9eDmVMPAFsUqhjN2e+jcdVpl0Pz1W6ElRWWyr30hgTY7xIqIKpMs1iT6IF/8w9CKvI6fPbKxA== -"@firebase/firestore@0.8.8": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-0.8.8.tgz#17d7fccf7afb4b27d4c55e608f03fff55bad163a" - integrity sha512-8OxRQZnvkXucZNxJh0OYkRoCt0+v260ScQvSIfTPjNO26Ahxc0ULSiox6US6lHH8U1ecTEilVYroknzwV/moJw== +"@firebase/firestore@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-0.9.0.tgz#37a6698b1805b07181455df562f7e98192d12546" + integrity sha512-ub9BXce75D7t2yQxqopJhqE5YrsdePLg7wP8aMLR2Twe+O89IZqOhdbE0VALjplD2rAWXkOLqf6zYsppu2hh4g== dependencies: - "@firebase/firestore-types" "0.7.0" + "@firebase/firestore-types" "0.8.0" "@firebase/logger" "0.1.2" "@firebase/webchannel-wrapper" "0.2.11" - grpc "1.16.0" + grpc "1.16.1" tslib "1.9.0" "@firebase/functions-types@0.2.1": @@ -667,9 +667,9 @@ integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= "@types/node@*", "@types/node@^10.1.0": - version "10.12.10" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.10.tgz#4fa76e6598b7de3f0cb6ec3abacc4f59e5b3a2ce" - integrity sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w== + version "10.12.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.12.tgz#e15a9d034d9210f00320ef718a50c4a799417c47" + integrity sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A== "@types/node@^6.0.46": version "6.14.2" @@ -1248,15 +1248,15 @@ atob@^2.1.1: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^9.0.0: - version "9.3.1" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.3.1.tgz#71b622174de2b783d5fd99f9ad617b7a3c78443e" - integrity sha512-DY9gOh8z3tnCbJ13JIWaeQsoYncTGdsrgCceBaQSIL4nvdrLxgbRSBPevg2XbX7u4QCSfLheSJEEIUUSlkbx6Q== + version "9.4.2" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.4.2.tgz#0234d20900684fc4bfb67926493deb68384067f5" + integrity sha512-tYQYJvZvqlJCzF+BLC//uAcdT/Yy4ik9bwZRXr/EehUJ/bjjpTthsWTy8dpowdoIE1sLCDf1ch4Eb2cOSzZC9w== dependencies: - browserslist "^4.3.3" - caniuse-lite "^1.0.30000898" + browserslist "^4.3.5" + caniuse-lite "^1.0.30000914" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.5" + postcss "^7.0.6" postcss-value-parser "^3.3.1" aws-sign2@~0.7.0: @@ -1379,9 +1379,9 @@ better-assert@~1.0.0: callsite "1.0.0" big-integer@^1.6.17: - version "1.6.36" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.36.tgz#78631076265d4ae3555c04f85e7d9d2f3a071a36" - integrity sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg== + version "1.6.40" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.40.tgz#02e4cd4d6e266c4d9ece2469c05cb6439149fc78" + integrity sha512-CjhtJp0BViLzP1ZkEnoywjgtFQXS2pomKjAJtIISTCnuHILkLcAXLdFLG/nxsHc4s9kJfc+82Xpg8WNyhfACzQ== binary-extensions@^1.0.0: version "1.12.0" @@ -1512,14 +1512,14 @@ braces@^2.3.0, braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" -browserslist@^4.3.3: - version "4.3.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.4.tgz#4477b737db6a1b07077275b24791e680d4300425" - integrity sha512-u5iz+ijIMUlmV8blX82VGFrB9ecnUg5qEt55CMZ/YJEhha+d8qpBfOFuutJ6F/VKRXjZoD33b6uvarpPxcl3RA== +browserslist@^4.3.5: + version "4.3.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.5.tgz#1a917678acc07b55606748ea1adf9846ea8920f7" + integrity sha512-z9ZhGc3d9e/sJ9dIx5NFXkKoaiQTnrvrMsN3R1fGb1tkWWNSz12UewJn9TNxGo1l7J23h0MRaPmk7jfeTZYs1w== dependencies: - caniuse-lite "^1.0.30000899" - electron-to-chromium "^1.3.82" - node-releases "^1.0.1" + caniuse-lite "^1.0.30000912" + electron-to-chromium "^1.3.86" + node-releases "^1.0.5" browserstack@1.5.0: version "1.5.0" @@ -1742,10 +1742,10 @@ camelcase@^4.0.0, camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= -caniuse-lite@^1.0.30000898, caniuse-lite@^1.0.30000899: - version "1.0.30000912" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000912.tgz#08e650d4090a9c0ab06bfd2b46b7d3ad6dcaea28" - integrity sha512-M3zAtV36U+xw5mMROlTXpAHClmPAor6GPKAMD5Yi7glCB5sbMPFtnQ3rGpk4XqPdUrrTIaVYSJZxREZWNy8QJg== +caniuse-lite@^1.0.30000912, caniuse-lite@^1.0.30000914: + version "1.0.30000916" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000916.tgz#3428d3f529f0a7b2bfaaec65e796037bdd433aab" + integrity sha512-D6J9jloPm2MPkg0PXcODLMQAJKkeixKO9xhqTUMvtd44MtTYMyyDXPQ2Lk9IgBq5FH0frwiPa/N/w8ncQf7kIQ== canonical-path@0.0.2, canonical-path@~0.0.2: version "0.0.2" @@ -2517,9 +2517,9 @@ core-js@2.5.5: integrity sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs= core-js@^2.0.0, core-js@^2.2.0, core-js@^2.5.7: - version "2.5.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" - integrity sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw== + version "2.6.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.0.tgz#1e30793e9ee5782b307e37ffa22da0eacddd84d4" + integrity sha512-kLRC6ncVpuEW/1kwrOXYX6KQASCVtrh1gQr/UiaVgFlf9WE5Vp+lNe5+h3LuMr5PAucWnnEXwH0nQHRH/gpGtw== core-js@~2.3.0: version "2.3.0" @@ -3046,15 +3046,10 @@ dom-storage@2.1.0: resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz#00fb868bc9201357ea243c7bcfd3304c1e34ea39" integrity sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q== -domelementtype@1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.2.1.tgz#578558ef23befac043a1abb0db07635509393479" - integrity sha512-SQVCLFS2E7G5CRCMdn6K9bIhRj1bS6QBWZfF0TUPh4V/BbqrQ619IdSS3/izn0FZ+9l+uODzaZjb08fjOfablA== - -domelementtype@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" - integrity sha1-sXrtguirWeUt2cGbF1bg/BhyBMI= +domelementtype@1, domelementtype@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== domelementtype@~1.1.1: version "1.1.3" @@ -3069,9 +3064,9 @@ domhandler@^2.3.0: domelementtype "1" domino@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.0.tgz#653ba7d331441113b42e40ba05f24253ec86e02e" - integrity sha512-xINSODvrnuQcm3eXJN4IkBR+JxqLrJN8Ge4fd00y1b7HsY0A4huKN5BflSS/oo8quBWmocTfWdFvrw2H8TjGqQ== + version "2.1.1" + resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.1.tgz#cd5c639940db72bb7cde1cdb5beea466a4113136" + integrity sha512-fqoTi6oQ881wYRENIEmz78hKVoc3X9HqVpklo419yxzebys6dtU5c83iVh3UYvvexPFdAuwlDYCsUM9//CrMMg== domutils@^1.5.1: version "1.7.0" @@ -3121,11 +3116,6 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexer@^0.1.1, duplexer@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" - integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= - duplexify@^3.2.0, duplexify@^3.5.0, duplexify@^3.6.0: version "3.6.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.1.tgz#b1a7a29c4abfd639585efaecce80d666b1e34125" @@ -3169,10 +3159,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.82: - version "1.3.85" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.85.tgz#5c46f790aa96445cabc57eb9d17346b1e46476fe" - integrity sha512-kWSDVVF9t3mft2OHVZy4K85X2beP6c6mFm3teFS/mLSDJpQwuFIWHrULCX+w6H1E55ZYmFRlT+ATAFRwhrYzsw== +electron-to-chromium@^1.3.86: + version "1.3.88" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.88.tgz#f36ab32634f49ef2b0fdc1e82e2d1cc17feb29e7" + integrity sha512-UPV4NuQMKeUh1S0OWRvwg0PI8ASHN9kBC8yDTk1ROXLC85W5GnhTRu/MZu3Teqx3JjlQYuckuHYXSUSgtb3J+A== empower-core@^1.2.0: version "1.2.0" @@ -3455,19 +3445,6 @@ event-emitter@^0.3.5, event-emitter@~0.3.5: d "1" es5-ext "~0.10.14" -event-stream@^3.3.4: - version "3.3.5" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.5.tgz#e5dd8989543630d94c6cf4d657120341fa31636b" - integrity sha512-vyibDcu5JL20Me1fP734QBH/kenBGLZap2n0+XXM7mvuUPzJ20Ydqj1aKcIeMdri1p+PU+4yAKugjN8KCVst+g== - dependencies: - duplexer "^0.1.1" - from "^0.1.7" - map-stream "0.0.7" - pause-stream "^0.0.11" - split "^1.0.1" - stream-combiner "^0.2.2" - through "^2.3.8" - eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" @@ -3657,12 +3634,13 @@ eyes@0.1.x: integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= fancy-log@^1.1.0, fancy-log@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.2.tgz#f41125e3d84f2e7d89a43d06d958c8f78be16be1" - integrity sha1-9BEl49hPLn2JpD0G2VjI94vha+E= + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== dependencies: ansi-gray "^0.1.1" color-support "^1.1.3" + parse-node-version "^1.0.0" time-stamp "^1.0.0" fast-deep-equal@^2.0.1: @@ -3930,14 +3908,14 @@ firebase@2.x.x: faye-websocket ">=0.6.0" firebase@^5.5.8: - version "5.5.9" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-5.5.9.tgz#1e20172d7c7dfafdc75a18378439e0493bc12753" - integrity sha512-IFABX9++5Bq7S00zYGdkdnqikq67cJuub26iyap4qNPnc05qXxx/5waomMIyEvfH74K7ywOaVWEy0E1BFNKk7g== + version "5.7.0" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-5.7.0.tgz#4e43e3f6521cfb4c1b4caf1b269a402c83713dd4" + integrity sha512-8sQYXCUbUuzpyZS+XpBogstQE/7lvX6tPWtrUH0Cf4mH7flH/Sue5GUvZzwBpkgKOrMpI+clzuoYm/ffGi/TjQ== dependencies: "@firebase/app" "0.3.5" - "@firebase/auth" "0.7.9" + "@firebase/auth" "0.9.0" "@firebase/database" "0.3.7" - "@firebase/firestore" "0.8.8" + "@firebase/firestore" "0.9.0" "@firebase/functions" "0.3.3" "@firebase/messaging" "0.3.7" "@firebase/polyfill" "0.3.3" @@ -3973,6 +3951,11 @@ flat-cache@^1.2.1: rimraf "~2.6.2" write "^0.2.1" +flatted@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" + integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== + flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" @@ -4048,11 +4031,6 @@ from2@^2.1.1: inherits "^2.0.1" readable-stream "^2.0.0" -from@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= - fs-access@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" @@ -4592,12 +4570,12 @@ google-p12-pem@^0.1.0: node-forge "^0.7.1" google-p12-pem@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-1.0.2.tgz#c8a3843504012283a0dbffc7430b7c753ecd4b07" - integrity sha512-+EuKr4CLlGsnXx4XIJIVkcKYrsa2xkAmCvxRhX2HsazJzUBAJ35wARGeApHUn4nNfPD03Vl057FskNr20VaCyg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-1.0.3.tgz#3d8acc140573339a5bca7b2f6a4b206bbea6d8d7" + integrity sha512-KGnAiMMWaJp4j4tYVvAjfP3wCKZRLv9M1Nir2wRRNWUYO7j1aX8O9Qgz+a8/EQ5rAvuo4SIu79n6SIdkNl7Msg== dependencies: - node-forge "^0.7.4" - pify "^3.0.0" + node-forge "^0.7.5" + pify "^4.0.0" google-proto-files@^0.16.0, google-proto-files@^0.16.1: version "0.16.1" @@ -4697,17 +4675,7 @@ graphviz@^0.0.8: dependencies: temp "~0.4.0" -grpc@1.16.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.16.0.tgz#cdf56d6ede2f1b6ab5224ad467cb22e1b3ec36fc" - integrity sha512-+p8YRIng7Gihkn2jycAXwXdA9aQ10SikRrcHY+/r3W1Z1Pr9NFIbLcmBZPoaTbzzLDv/ysqwqFEZriAdd8tveQ== - dependencies: - lodash "^4.17.5" - nan "^2.0.0" - node-pre-gyp "^0.10.0" - protobufjs "^5.0.3" - -grpc@^1.12.2: +grpc@1.16.1, grpc@^1.12.2: version "1.16.1" resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.16.1.tgz#533316f38cea68111ef577728c3f4e8e9e554543" integrity sha512-7uHN1Nd3UqfvwgQ6f5U3+EZb/0iuHJ9mbPH+ydaTkszJsUi3nwdz6DuSh0eJwYVXXn6Gojv2khiQAadMongGKg== @@ -4782,15 +4750,15 @@ gulp-cli@^2.0.1: yargs "^7.1.0" gulp-connect@^5.6.1: - version "5.6.1" - resolved "https://registry.yarnpkg.com/gulp-connect/-/gulp-connect-5.6.1.tgz#ba25bc7b1ec17e2fdae3a7368b510bf9edaa8991" - integrity sha512-FknHeA6smZhiRNrK/3UVH8BHLCFHS7WZdE7Y2VbZHtxye1UTIa5ZY0Cnst6O9n3kL8z7y43QI+acx3nUtJoiHw== + version "5.7.0" + resolved "https://registry.yarnpkg.com/gulp-connect/-/gulp-connect-5.7.0.tgz#7e925f5e4c34ebfedf9f318576966e8fe8840d5a" + integrity sha512-8tRcC6wgXMLakpPw9M7GRJIhxkYdgZsXwn7n56BA2bQYGLR9NOPhMzx7js+qYDy6vhNkbApGKURjAw1FjY4pNA== dependencies: ansi-colors "^2.0.5" connect "^3.6.6" connect-livereload "^0.6.0" - event-stream "^3.3.4" fancy-log "^1.3.2" + map-stream "^0.0.7" send "^0.16.2" serve-index "^1.9.1" serve-static "^1.13.2" @@ -6209,9 +6177,9 @@ karma-sourcemap-loader@^0.3.7: graceful-fs "^4.1.2" karma@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/karma/-/karma-3.1.1.tgz#94c8edd20fb9597ccde343326da009737fb0423a" - integrity sha512-NetT3wPCQMNB36uiL9LLyhrOt8SQwrEKt0xD3+KpTCfm0VxVyUJdPL5oTq2Ic5ouemgL/Iz4wqXEbF3zea9kQQ== + version "3.1.3" + resolved "https://registry.yarnpkg.com/karma/-/karma-3.1.3.tgz#6e251648e3aff900927bc1126dbcbcb92d3edd61" + integrity sha512-JU4FYUtFEGsLZd6ZJzLrivcPj0TkteBiIRDcXWFsltPMGgZMDtby/MIzNOzgyZv/9dahs9vHpSxerC/ZfeX9Qw== dependencies: bluebird "^3.3.0" body-parser "^1.16.1" @@ -6223,11 +6191,12 @@ karma@^3.1.1: di "^0.0.1" dom-serialize "^2.2.0" expand-braces "^0.1.1" + flatted "^2.0.0" glob "^7.1.1" graceful-fs "^4.1.2" http-proxy "^1.13.0" isbinaryfile "^3.0.0" - lodash "^4.17.4" + lodash "^4.17.5" log4js "^3.0.0" mime "^2.3.1" minimatch "^3.0.2" @@ -6239,7 +6208,7 @@ karma@^3.1.1: socket.io "2.1.1" source-map "^0.6.1" tmp "0.0.33" - useragent "2.2.1" + useragent "2.3.0" keyv@3.0.0: version "3.0.0" @@ -6800,18 +6769,13 @@ lru-cache@2: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= -lru-cache@2.2.x: - version "2.2.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" - integrity sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0= - -lru-cache@^4.0.1, lru-cache@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.4.tgz#51cc46e8e6d9530771c857e24ccc720ecdbcc031" - integrity sha512-EPstzZ23znHUVLKj+lcXO1KvZkrlw+ZirdwvOmnAnA/1PB4ggyXJ77LRkCqkff+ShQ+cqoxCxLQOh4cKITO5iA== +lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.1.3: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== dependencies: pseudomap "^1.0.2" - yallist "^3.0.2" + yallist "^2.1.2" lru-queue@0.1: version "0.1.0" @@ -6886,7 +6850,7 @@ map-obj@^2.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= -map-stream@0.0.7: +map-stream@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg= @@ -7186,9 +7150,9 @@ minipass@^2.2.1, minipass@^2.3.4: yallist "^3.0.0" minizlib@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.1.tgz#6734acc045a46e61d596a43bb9d9cd326e19cc42" - integrity sha512-TrfjCjk4jLhcJyGMYymBH6oTXcWjYbUAXTHDbtnWHjZC25h0cdajHuPE1zxb4DVmu8crfh+HwH/WMuyLG0nHBg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" + integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== dependencies: minipass "^2.2.1" @@ -7380,7 +7344,7 @@ node-forge@0.7.4: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.4.tgz#8e6e9f563a1e32213aa7508cded22aa791dbf986" integrity sha512-8Df0906+tq/omxuCZD6PqhPaQDYuyJ1d+VITgxoIA8zvQd1ru+nMJcDChHH324MWitIgbVkAkQoGEEVJNpn/PA== -node-forge@^0.7.1, node-forge@^0.7.4: +node-forge@^0.7.1, node-forge@^0.7.5: version "0.7.6" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== @@ -7440,17 +7404,17 @@ node-pre-gyp@^0.12.0: semver "^5.3.0" tar "^4" -node-releases@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.5.tgz#a641adcc968b039a27345d92ef10b093e5cbd41d" - integrity sha512-Ky7q0BO1BBkG/rQz6PkEZ59rwo+aSfhczHP1wwq8IowoVdN/FpiP7qp0XW0P2+BVCWe5fQUBozdbVd54q1RbCQ== +node-releases@^1.0.5: + version "1.1.0" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.0.tgz#be7464fa8d877808237520fd49436d5e79191c3d" + integrity sha512-+qV91QMDBvARuPxUEfI/mRF/BY+UAkTIn3pvmvM2iOLIRvv6RNYklFXBgrkky6P1wXUqQW1P3qKlWxxy4JZbfg== dependencies: semver "^5.3.0" node-sass@^4.8.3, node-sass@^4.9.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.10.0.tgz#dcc2b364c0913630945ccbf7a2bbf1f926effca4" - integrity sha512-fDQJfXszw6vek63Fe/ldkYXmRYK/QS6NbvM3i5oEo9ntPDy4XX7BcKZyTKv+/kSSxRtXXc7l+MSwEmYc0CSy6Q== + version "4.11.0" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" + integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -7943,6 +7907,11 @@ parse-ms@^2.0.0: resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.0.0.tgz#7b3640295100caf3fa0100ccceb56635b62f9d62" integrity sha512-AddiXFSLLCqj+tCRJ9MrUtHZB4DWojO3tk0NVZ+g5MaMQHF2+p2ktqxuoXyPFLljz/aUK0Nfhd/uGWnhXVXEyA== +parse-node-version@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.0.tgz#33d9aa8920dcc3c0d33658ec18ce237009a56d53" + integrity sha512-02GTVHD1u0nWc20n2G7WX/PgdhNFG04j5fi1OkaJzPWLTcf6vh6229Lta1wTmXG/7Dg42tCssgkccVt7qvd8Kg== + parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -8081,13 +8050,6 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" -pause-stream@^0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= - dependencies: - through "~2.3" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -8153,9 +8115,9 @@ pluralize@^7.0.0: integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== portfinder@^1.0.13: - version "1.0.19" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.19.tgz#07e87914a55242dcda5b833d42f018d6875b595f" - integrity sha512-23aeQKW9KgHe6citUrG3r9HjeX6vls0h713TAa+CwTKZwNIr/pD2ApaxYF4Um3ZZyq4ar+Siv3+fhoHaIwSOSw== + version "1.0.20" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" + integrity sha512-Yxe4mTyDzTd59PZJY4ojZR8F+E5e97iq2ZOHPz3HDgSvYC5siNad2tLooQ5y5QHyQhc3xVqvyk/eNA3wuoa7Sw== dependencies: async "^1.5.2" debug "^2.2.0" @@ -8272,7 +8234,7 @@ postcss-values-parser@^1.5.0: indexes-of "^1.0.1" uniq "^1.0.1" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.3, postcss@^7.0.5: +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.2, postcss@^7.0.3, postcss@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.6.tgz#6dcaa1e999cdd4a255dcd7d4d9547f4ca010cdc2" integrity sha512-Nq/rNjnHFcKgCDDZYO0lNsl6YWe6U7tTy+ESN+PnLxebL8uBtYX59HZqvrj7YLK5UCyll2hqDsJOo3ndzEW8Ug== @@ -8452,9 +8414,9 @@ process-nextick-args@~1.0.6: integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= progress@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.1.tgz#c9242169342b1c29d275889c95734621b1952e31" - integrity sha512-OE+a6vzqazc+K6LxJrX5UPyKFvGnL5CYmq2jFGNIBWHpc4QyE49/YOumcrpQFJpfejmvRtbJzgO1zPmMCqlbBg== + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== promise-polyfill@7.1.2: version "7.1.2" @@ -9700,9 +9662,9 @@ sparkles@^1.0.0: integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== spdx-correct@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.2.tgz#19bb409e91b47b1ad54159243f7312a858db3c2e" - integrity sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ== + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" @@ -9764,7 +9726,7 @@ split2@^2.0.0: dependencies: through2 "^2.0.2" -split@^1.0.0, split@^1.0.1: +split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== @@ -9831,14 +9793,6 @@ stdout-stream@^1.4.0: dependencies: readable-stream "^2.0.1" -stream-combiner@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" - integrity sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg= - dependencies: - duplexer "~0.1.1" - through "~2.3.4" - stream-consume@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.1.tgz#d3bdb598c2bd0ae82b8cac7ac50b1107a7996c48" @@ -10330,7 +10284,7 @@ through2@^2.0.0, through2@^2.0.1, through2@^2.0.2, through2@^2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.4: +through@2, "through@>=2.2.7 <3", through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -10658,16 +10612,21 @@ typescript-eslint-parser@^18.0.0: lodash.unescape "4.0.1" semver "5.5.0" -typescript@^3.0.3, typescript@~3.1.1: - version "3.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" - integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== +typescript@^3.0.3: + version "3.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.2.2.tgz#fe8101c46aa123f8353523ebdcf5730c2ae493e5" + integrity sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg== typescript@~2.7.1: version "2.7.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.7.2.tgz#2d615a1ef4aee4f574425cdff7026edf81919836" integrity sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw== +typescript@~3.1.1: + version "3.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" + integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== + uglify-js@3.4.x, uglify-js@^3.1.4: version "3.4.9" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" @@ -10960,12 +10919,12 @@ user-home@^2.0.0: dependencies: os-homedir "^1.0.0" -useragent@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.2.1.tgz#cf593ef4f2d175875e8bb658ea92e18a4fd06d8e" - integrity sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4= +useragent@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" + integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== dependencies: - lru-cache "2.2.x" + lru-cache "4.1.x" tmp "0.0.x" util-deprecate@^1.0.1, util-deprecate@~1.0.1: @@ -11468,6 +11427,11 @@ y18n@^3.2.0, y18n@^3.2.1: resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + yallist@^3.0.0, yallist@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"