From 9148e8709771c26cb9ac4eda9732191ece1b25fa Mon Sep 17 00:00:00 2001 From: CaerusKaru Date: Tue, 13 Nov 2018 19:03:19 -0600 Subject: [PATCH] feat(core): add ability to override style building (#884) This change decouples the style-generation from the actual directives, meaning that the library is now composed of the following: * Directives that respond to inputs and `matchMedia` events * Style generation providers that return styles when triggered by directives This allows for end-users or library authors to provide their own unique style generation (even by borrowing or extending our existing library code) for any directive. This is entirely non-mandatory for use of the `BaseDirective`, since the `BaseDirective` need not always use a de-coupled style provider to function. The canonical example is the following: ```ts @Injectable() export class CustomStyleBuilder extends StyleBuilder { buildStyles(input: string) { return { 'style1': 'value1', }; } } @NgModule({ ... providers: [ provide: , useClass: CustomStyleBuilder, ], }) export class MyAppModule {} ``` Fixes #689 --- src/lib/core/base/base.ts | 11 +- src/lib/core/public-api.ts | 1 + src/lib/core/style-builder/style-builder.ts | 14 + src/lib/flex/flex-align/flex-align.ts | 57 ++-- src/lib/flex/flex-fill/flex-fill.ts | 25 +- src/lib/flex/flex-offset/flex-offset.spec.ts | 56 +++- src/lib/flex/flex-offset/flex-offset.ts | 44 ++- src/lib/flex/flex-order/flex-order.ts | 30 +- src/lib/flex/flex/flex.spec.ts | 49 ++- src/lib/flex/flex/flex.ts | 290 +++++++++--------- .../flex/layout-align/layout-align.spec.ts | 48 ++- src/lib/flex/layout-align/layout-align.ts | 186 +++++------ src/lib/flex/layout-gap/layout-gap.spec.ts | 52 +++- src/lib/flex/layout-gap/layout-gap.ts | 174 ++++++----- src/lib/flex/layout/layout.spec.ts | 52 +++- src/lib/flex/layout/layout.ts | 40 ++- 16 files changed, 738 insertions(+), 391 deletions(-) create mode 100644 src/lib/core/style-builder/style-builder.ts diff --git a/src/lib/core/base/base.ts b/src/lib/core/base/base.ts index 103073bf0..89e5bbe94 100644 --- a/src/lib/core/base/base.ts +++ b/src/lib/core/base/base.ts @@ -21,6 +21,7 @@ import { import {ResponsiveActivation, KeyOptions} from '../responsive-activation/responsive-activation'; 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. */ export abstract class BaseDirective implements OnDestroy, OnChanges { @@ -58,7 +59,8 @@ export abstract class BaseDirective implements OnDestroy, OnChanges { protected constructor(protected _mediaMonitor: MediaMonitor, protected _elementRef: ElementRef, - protected _styler: StyleUtils) { + protected _styler: StyleUtils, + protected _styleBuilder?: StyleBuilder) { } /** @@ -107,6 +109,11 @@ export abstract class BaseDirective implements OnDestroy, OnChanges { return this._elementRef.nativeElement; } + protected addStyles(input: string, parent?: Object) { + const styles: StyleDefinition = this._styleBuilder!.buildStyles(input, parent); + this._applyStyleToElement(styles); + } + /** Access the current value (if any) of the @Input property */ protected _queryInput(key: string) { return this._inputMap[key]; @@ -206,7 +213,7 @@ export abstract class BaseDirective implements OnDestroy, OnChanges { } /** Special accessor to query for all child 'element' nodes regardless of type, class, etc */ - protected get childrenNodes() { + protected get childrenNodes(): HTMLElement[] { const obj = this.nativeElement.children; const buffer: any[] = []; diff --git a/src/lib/core/public-api.ts b/src/lib/core/public-api.ts index 620eb0371..6d95c817d 100644 --- a/src/lib/core/public-api.ts +++ b/src/lib/core/public-api.ts @@ -20,4 +20,5 @@ export * from './observable-media/index'; export * from './responsive-activation/responsive-activation'; export * from './style-utils/style-utils'; +export * from './style-builder/style-builder'; export * from './basis-validator/basis-validator'; diff --git a/src/lib/core/style-builder/style-builder.ts b/src/lib/core/style-builder/style-builder.ts new file mode 100644 index 000000000..e9b078e14 --- /dev/null +++ b/src/lib/core/style-builder/style-builder.ts @@ -0,0 +1,14 @@ +/** + * @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 {StyleDefinition} from '../style-utils/style-utils'; + +@Injectable() +export abstract class StyleBuilder { + abstract buildStyles(input: string, parent?: Object): StyleDefinition; +} diff --git a/src/lib/flex/flex-align/flex-align.ts b/src/lib/flex/flex-align/flex-align.ts index ec5afa65b..71524a7fb 100644 --- a/src/lib/flex/flex-align/flex-align.ts +++ b/src/lib/flex/flex-align/flex-align.ts @@ -13,9 +13,38 @@ import { OnChanges, OnDestroy, SimpleChanges, + Injectable, } from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import { + BaseDirective, + MediaChange, + MediaMonitor, + StyleBuilder, + StyleDefinition, + StyleUtils +} from '@angular/flex-layout/core'; + +@Injectable({providedIn: 'root'}) +export class FlexAlignStyleBuilder implements StyleBuilder { + buildStyles(input: string): StyleDefinition { + const css: {[key: string]: string | number} = {}; + // Cross-axis + switch (input) { + case 'start': + css['align-self'] = 'flex-start'; + break; + case 'end': + css['align-self'] = 'flex-end'; + break; + default: + css['align-self'] = input; + break; + } + + return css; + } +} /** * 'flex-align' flexbox styling directive @@ -53,8 +82,9 @@ export class FlexAlignDirective extends BaseDirective implements OnInit, OnChang /* tslint:enable */ constructor(monitor: MediaMonitor, elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); + styleUtils: StyleUtils, + styleBuilder: FlexAlignStyleBuilder) { + super(monitor, elRef, styleUtils, styleBuilder); } @@ -93,25 +123,6 @@ export class FlexAlignDirective extends BaseDirective implements OnInit, OnChang value = this._mqActivation.activatedInput; } - this._applyStyleToElement(this._buildCSS(value)); - } - - protected _buildCSS(align: string | number = '') { - let css: {[key: string]: string | number} = {}; - - // Cross-axis - switch (align) { - case 'start': - css['align-self'] = 'flex-start'; - break; - case 'end': - css['align-self'] = 'flex-end'; - break; - default: - css['align-self'] = align; - break; - } - - return css; + this.addStyles(value && (value + '') || ''); } } diff --git a/src/lib/flex/flex-fill/flex-fill.ts b/src/lib/flex/flex-fill/flex-fill.ts index 2b825794f..afdcfc0e4 100644 --- a/src/lib/flex/flex-fill/flex-fill.ts +++ b/src/lib/flex/flex-fill/flex-fill.ts @@ -5,9 +5,14 @@ * 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} from '@angular/core'; -import {BaseDirective, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; - +import {Directive, ElementRef, Injectable} from '@angular/core'; +import { + BaseDirective, + MediaMonitor, + StyleBuilder, + StyleDefinition, + StyleUtils, +} from '@angular/flex-layout/core'; const FLEX_FILL_CSS = { 'margin': 0, @@ -17,6 +22,13 @@ const FLEX_FILL_CSS = { 'min-height': '100%' }; +@Injectable({providedIn: 'root'}) +export class FlexFillStyleBuilder implements StyleBuilder { + buildStyles(_input: string): StyleDefinition { + return FLEX_FILL_CSS; + } +} + /** * 'fxFill' flexbox styling directive * Maximizes width and height of element in a layout container @@ -30,8 +42,9 @@ const FLEX_FILL_CSS = { export class FlexFillDirective extends BaseDirective { constructor(monitor: MediaMonitor, public elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); - this._applyStyleToElement(FLEX_FILL_CSS); + styleUtils: StyleUtils, + styleBuilder: FlexFillStyleBuilder) { + super(monitor, elRef, styleUtils, styleBuilder); + this.addStyles(''); } } diff --git a/src/lib/flex/flex-offset/flex-offset.spec.ts b/src/lib/flex/flex-offset/flex-offset.spec.ts index 30a5da97f..65f34dd27 100644 --- a/src/lib/flex/flex-offset/flex-offset.spec.ts +++ b/src/lib/flex/flex-offset/flex-offset.spec.ts @@ -5,11 +5,16 @@ * 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, PLATFORM_ID} from '@angular/core'; +import {Component, Injectable, PLATFORM_ID} from '@angular/core'; import {CommonModule, isPlatformServer} from '@angular/common'; -import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing'; import {DIR_DOCUMENT} from '@angular/cdk/bidi'; -import {SERVER_TOKEN, StyleUtils} from '@angular/flex-layout/core'; +import { + MockMatchMediaProvider, + SERVER_TOKEN, + StyleBuilder, + StyleUtils, +} from '@angular/flex-layout/core'; import {FlexLayoutModule} from '../../module'; import {customMatchers} from '../../utils/testing/custom-matchers'; @@ -19,6 +24,8 @@ import { expectEl, expectNativeEl, } from '../../utils/testing/helpers'; +import {FlexModule} from '../module'; +import {FlexOffsetStyleBuilder} from './flex-offset'; describe('flex-offset directive', () => { let fixture: ComponentFixture; @@ -177,8 +184,51 @@ describe('flex-offset directive', () => { }); + describe('with custom builder', () => { + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [ + CommonModule, + FlexLayoutModule.withConfig({ + useColumnBasisZero: false, + serverLoaded: true, + }), + ], + declarations: [TestFlexComponent], + providers: [ + MockMatchMediaProvider, + { + provide: FlexOffsetStyleBuilder, + useClass: MockFlexOffsetStyleBuilder, + } + ] + }); + }); + + it('should set flex offset not to input', async(() => { + componentWithTemplate(` +
+
+
+ `); + fixture.detectChanges(); + let element = queryFor(fixture, '[fxFlexOffset]')[0]; + expectEl(element).toHaveStyle({'margin-top': '10px'}, styler); + })); + }); + }); +@Injectable({providedIn: FlexModule}) +export class MockFlexOffsetStyleBuilder implements StyleBuilder { + buildStyles(_input: string) { + return {'margin-top': '10px'}; + } +} + // ***************************************************************** // Template Component diff --git a/src/lib/flex/flex-offset/flex-offset.ts b/src/lib/flex/flex-offset/flex-offset.ts index 78b399262..2c9f3173d 100644 --- a/src/lib/flex/flex-offset/flex-offset.ts +++ b/src/lib/flex/flex-offset/flex-offset.ts @@ -15,12 +15,14 @@ import { Optional, SimpleChanges, SkipSelf, + Injectable, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { BaseDirective, MediaChange, MediaMonitor, + StyleBuilder, StyleDefinition, StyleUtils, } from '@angular/flex-layout/core'; @@ -29,6 +31,26 @@ import {Subscription} from 'rxjs'; import {Layout, LayoutDirective} from '../layout/layout'; import {isFlowHorizontal} from '../../utils/layout-validator'; +interface FlexOffsetParent { + layout: string; + isRtl: boolean; +} + +@Injectable({providedIn: 'root'}) +export class FlexOffsetStyleBuilder implements StyleBuilder { + buildStyles(offset: string, parent: FlexOffsetParent): StyleDefinition { + const isPercent = String(offset).indexOf('%') > -1; + const isPx = String(offset).indexOf('px') > -1; + if (!isPx && !isPercent && !isNaN(+offset)) { + offset = offset + '%'; + } + const horizontalLayoutKey = parent.isRtl ? 'margin-right' : 'margin-left'; + + return isFlowHorizontal(parent.layout) ? {[horizontalLayoutKey]: `${offset}`} : + {'margin-top': `${offset}`}; + } +} + /** * 'flex-offset' flexbox styling directive * Configures the 'margin-left' of the element in a layout container @@ -65,8 +87,9 @@ export class FlexOffsetDirective extends BaseDirective implements OnInit, OnChan elRef: ElementRef, @Optional() @SkipSelf() protected _container: LayoutDirective, private _directionality: Directionality, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); + styleUtils: StyleUtils, + styleBuilder: FlexOffsetStyleBuilder) { + super(monitor, elRef, styleUtils, styleBuilder); this._directionWatcher = this._directionality.change.subscribe(this._updateWithValue.bind(this)); @@ -159,22 +182,9 @@ export class FlexOffsetDirective extends BaseDirective implements OnInit, OnChan value = this._mqActivation.activatedInput; } - this._applyStyleToElement(this._buildCSS(value)); - } - - protected _buildCSS(offset: string|number = ''): StyleDefinition { - let isPercent = String(offset).indexOf('%') > -1; - let isPx = String(offset).indexOf('px') > -1; - if (!isPx && !isPercent && !isNaN(+offset)) { - offset = offset + '%'; - } - // The flex-direction of this element's flex container. Defaults to 'row'. - const isRtl = this._directionality.value === 'rtl'; const layout = this._getFlexFlowDirection(this.parentElement, true); - const horizontalLayoutKey = isRtl ? 'margin-right' : 'margin-left'; - - return isFlowHorizontal(layout) ? {[horizontalLayoutKey]: `${offset}`} : - {'margin-top': `${offset}`}; + const isRtl = this._directionality.value === 'rtl'; + this.addStyles((value && (value + '') || ''), {layout, isRtl}); } } diff --git a/src/lib/flex/flex-order/flex-order.ts b/src/lib/flex/flex-order/flex-order.ts index 0b06b062f..176aff157 100644 --- a/src/lib/flex/flex-order/flex-order.ts +++ b/src/lib/flex/flex-order/flex-order.ts @@ -13,9 +13,24 @@ import { OnChanges, OnDestroy, SimpleChanges, + Injectable, } from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import { + BaseDirective, + MediaChange, + MediaMonitor, + StyleBuilder, + StyleDefinition, + StyleUtils +} from '@angular/flex-layout/core'; +@Injectable({providedIn: 'root'}) +export class FlexOrderStyleBuilder implements StyleBuilder { + buildStyles(value: string): StyleDefinition { + const val = parseInt(value, 10); + return {order: isNaN(val) ? 0 : val}; + } +} /** * 'flex-order' flexbox styling directive @@ -51,8 +66,9 @@ export class FlexOrderDirective extends BaseDirective implements OnInit, OnChang /* tslint:enable */ constructor(monitor: MediaMonitor, elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); + styleUtils: StyleUtils, + styleBuilder: FlexOrderStyleBuilder) { + super(monitor, elRef, styleUtils, styleBuilder); } // ********************************************* @@ -90,12 +106,6 @@ export class FlexOrderDirective extends BaseDirective implements OnInit, OnChang value = this._mqActivation.activatedInput; } - this._applyStyleToElement(this._buildCSS(value)); - } - - - protected _buildCSS(value: string = '') { - const val = parseInt(value, 10); - return {order: isNaN(val) ? 0 : val}; + this.addStyles(value || ''); } } diff --git a/src/lib/flex/flex/flex.spec.ts b/src/lib/flex/flex/flex.spec.ts index b96dc46e2..c85fbd8e7 100644 --- a/src/lib/flex/flex/flex.spec.ts +++ b/src/lib/flex/flex/flex.spec.ts @@ -5,7 +5,7 @@ * 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, PLATFORM_ID, ViewChild} from '@angular/core'; +import {Component, Injectable, PLATFORM_ID, ViewChild} from '@angular/core'; import {CommonModule, isPlatformServer} from '@angular/common'; import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing'; import {Platform} from '@angular/cdk/platform'; @@ -13,11 +13,12 @@ import { MatchMedia, MockMatchMedia, MockMatchMediaProvider, + StyleBuilder, StyleUtils, } from '@angular/flex-layout/core'; import {FlexLayoutModule} from '../../module'; -import {FlexDirective} from './flex'; +import {FlexDirective, FlexStyleBuilder} from './flex'; import {LayoutDirective} from '../layout/layout'; import {customMatchers, expect} from '../../utils/testing/custom-matchers'; import { @@ -26,6 +27,7 @@ import { queryFor, expectEl, } from '../../utils/testing/helpers'; +import {FlexModule} from '../module'; describe('flex directive', () => { @@ -885,8 +887,51 @@ describe('flex directive', () => { })); }); + describe('with custom builder', () => { + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [ + CommonModule, + FlexLayoutModule.withConfig({ + useColumnBasisZero: false, + serverLoaded: true, + }), + ], + declarations: [TestFlexComponent, TestQueryWithFlexComponent], + providers: [ + MockMatchMediaProvider, + { + provide: FlexStyleBuilder, + useClass: MockFlexStyleBuilder, + } + ] + }); + }); + + it('should set flex basis to input', async(() => { + componentWithTemplate(` +
+
+
+ `); + fixture.detectChanges(); + let element = queryFor(fixture, '[fxFlex]')[0]; + expectEl(element).toHaveStyle({'flex': '1 1 30%'}, styler); + })); + }); + }); +@Injectable({providedIn: FlexModule}) +export class MockFlexStyleBuilder implements StyleBuilder { + buildStyles(_input: string) { + return {'flex': '1 1 30%'}; + } +} + // ***************************************************************** // Template Component diff --git a/src/lib/flex/flex/flex.ts b/src/lib/flex/flex/flex.ts index 267eb0c50..c3d9e3182 100644 --- a/src/lib/flex/flex/flex.ts +++ b/src/lib/flex/flex/flex.ts @@ -9,6 +9,7 @@ import { Directive, ElementRef, Inject, + Injectable, Input, OnChanges, OnDestroy, @@ -25,6 +26,8 @@ import { MediaMonitor, StyleUtils, validateBasis, + StyleBuilder, + StyleDefinition, } from '@angular/flex-layout/core'; import {Subscription} from 'rxjs'; @@ -32,150 +35,35 @@ 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; + useColumnBasisZero: boolean | undefined; +} -/** - * 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; - - /** - * 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) { - super(monitor, elRef, styleUtils); - - 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(); - } - } - - /** - * 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); - }); - } - } - - ngOnDestroy() { - super.ngOnDestroy(); - if (this._layoutWatcher) { - this._layoutWatcher.unsubscribe(); - } - } - - - /** - * 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 _updateStyle(value?: string|number) { - let flexBasis = value || this._queryInput('flex') || ''; - if (this._mqActivation) { - flexBasis = this._mqActivation.activatedInput; - } - - let basis = String(flexBasis).replace(';', ''); - let parts = validateBasis(basis, this._queryInput('grow'), this._queryInput('shrink')); - this._applyStyleToElement(this._validateValue.apply(this, parts)); - } +@Injectable({providedIn: 'root'}) +export class FlexStyleBuilder implements StyleBuilder { + buildStyles(input: string, parent: FlexBuilderParent): StyleDefinition { + let grow: string | number; + let shrink: string | number; + let basis: string | number; + [grow, shrink, basis] = input.split('_'); - /** - * Validate the value to be one of the acceptable value options - * Use default fallback of 'row' - */ - protected _validateValue(grow: number|string, - shrink: number|string, - basis: string|number|FlexBasisAlias) { - let addFlexToParent = this.layoutConfig.addFlexToParent !== false; // The flex-direction of this element's flex container. Defaults to 'row'. - let layout = this._getFlexFlowDirection(this.parentElement, addFlexToParent); - let direction = (layout.indexOf('column') > -1) ? 'column' : 'row'; + const direction = (parent.direction.indexOf('column') > -1) ? 'column' : 'row'; - let max = isFlowHorizontal(direction) ? 'max-width' : 'max-height'; - let min = isFlowHorizontal(direction) ? 'min-width' : 'min-height'; + const max = isFlowHorizontal(direction) ? 'max-width' : 'max-height'; + const min = isFlowHorizontal(direction) ? 'min-width' : 'min-height'; - let hasCalc = String(basis).indexOf('calc') > -1; - let usingCalc = hasCalc || (basis == 'auto'); - let isPercent = String(basis).indexOf('%') > -1 && !hasCalc; - let hasUnits = String(basis).indexOf('px') > -1 || String(basis).indexOf('em') > -1 || + const hasCalc = String(basis).indexOf('calc') > -1; + const usingCalc = hasCalc || (basis === 'auto'); + const isPercent = String(basis).indexOf('%') > -1 && !hasCalc; + const hasUnits = String(basis).indexOf('px') > -1 || String(basis).indexOf('em') > -1 || String(basis).indexOf('vw') > -1 || String(basis).indexOf('vh') > -1; - let isPx = String(basis).indexOf('px') > -1 || usingCalc; + const isPx = String(basis).indexOf('px') > -1 || usingCalc; let isValue = (hasCalc || hasUnits); @@ -185,7 +73,7 @@ export class FlexDirective extends BaseDirective implements OnInit, OnChanges, O // make box inflexible when shrink and grow are both zero // should not set a min when the grow is zero // should not set a max when the shrink is zero - let isFixed = !grow && !shrink; + const isFixed = !grow && !shrink; let css: {[key: string]: string | number | null} = {}; @@ -201,7 +89,7 @@ export class FlexDirective extends BaseDirective implements OnInit, OnChanges, O // with 'flex-grow: 1' on the same row. // Use `null` to clear existing styles. - let clearStyles = { + const clearStyles = { 'max-width': null, 'max-height': null, 'min-width': null, @@ -209,7 +97,7 @@ export class FlexDirective extends BaseDirective implements OnInit, OnChanges, O }; switch (basis || '') { case '': - const useColumnBasisZero = this.layoutConfig.useColumnBasisZero !== false; + const useColumnBasisZero = parent.useColumnBasisZero !== false; basis = direction === 'row' ? '0%' : (useColumnBasisZero ? '0.000000001px' : 'auto'); break; case 'initial': // default @@ -297,7 +185,7 @@ export class FlexDirective extends BaseDirective implements OnInit, OnChanges, O } } else { // Fix for issue 660 - if (this._layout && this._layout.wrap) { + if (parent.hasWrap) { css[hasCalc ? 'flex-basis' : 'flex'] = css[max] ? (hasCalc ? css[max] : `${grow} ${shrink} ${css[max]}`) : (hasCalc ? css[min] : `${grow} ${shrink} ${css[min]}`); @@ -307,3 +195,127 @@ export class FlexDirective extends BaseDirective implements OnInit, OnChanges, O return extendObject(css, {'box-sizing': 'border-box'}); } } + +/** + * 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; + + /** + * 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); + + 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(); + } + } + + /** + * 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); + }); + } + } + + ngOnDestroy() { + super.ngOnDestroy(); + if (this._layoutWatcher) { + this._layoutWatcher.unsubscribe(); + } + } + + /** + * 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 _updateStyle(value?: string|number) { + let flexBasis = value || this._queryInput('flex') || ''; + if (this._mqActivation) { + flexBasis = this._mqActivation.activatedInput; + } + + let basis = String(flexBasis).replace(';', ''); + let parts = validateBasis(basis, this._queryInput('grow'), this._queryInput('shrink')); + const addFlexToParent = this.layoutConfig.addFlexToParent !== false; + const direction = this._getFlexFlowDirection(this.parentElement, addFlexToParent); + const hasWrap = this._layout && this._layout.wrap; + const useColumnBasisZero = this.layoutConfig.useColumnBasisZero; + this.addStyles(parts.join('_'), {direction, hasWrap, useColumnBasisZero}); + } +} diff --git a/src/lib/flex/layout-align/layout-align.spec.ts b/src/lib/flex/layout-align/layout-align.spec.ts index 5c0d072b7..b62b2bb2c 100644 --- a/src/lib/flex/layout-align/layout-align.spec.ts +++ b/src/lib/flex/layout-align/layout-align.spec.ts @@ -5,15 +5,16 @@ * 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, OnInit} from '@angular/core'; +import {Component, Injectable, OnInit} from '@angular/core'; import {CommonModule} from '@angular/common'; -import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {ComponentFixture, TestBed, inject, async} from '@angular/core/testing'; import {Platform} from '@angular/cdk/platform'; import { MatchMedia, MockMatchMedia, MockMatchMediaProvider, SERVER_TOKEN, + StyleBuilder, StyleUtils, } from '@angular/flex-layout/core'; @@ -21,6 +22,8 @@ import {FlexLayoutModule} from '../../module'; import {extendObject} from '../../utils/object-extend'; import {customMatchers} from '../../utils/testing/custom-matchers'; import {makeCreateTestComponent, expectNativeEl} from '../../utils/testing/helpers'; +import {FlexModule} from '../module'; +import {LayoutAlignStyleBuilder} from './layout-align'; describe('layout-align directive', () => { let fixture: ComponentFixture; @@ -412,8 +415,49 @@ describe('layout-align directive', () => { }); + describe('with custom builder', () => { + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [ + CommonModule, + FlexLayoutModule.withConfig({ + useColumnBasisZero: false, + serverLoaded: true, + }), + ], + declarations: [], + providers: [ + MockMatchMediaProvider, + { + provide: LayoutAlignStyleBuilder, + useClass: MockLayoutAlignStyleBuilder, + } + ] + }); + }); + + it('should set flex offset not to input', async(() => { + createTestComponent(` +
+
+
+ `); + expectNativeEl(fixture).toHaveStyle({'justify-content': 'flex-end'}, styler); + })); + }); + }); +@Injectable({providedIn: FlexModule}) +export class MockLayoutAlignStyleBuilder implements StyleBuilder { + buildStyles(_input: string) { + return {'justify-content': 'flex-end'}; + } +} + // ***************************************************************** // Template Component diff --git a/src/lib/flex/layout-align/layout-align.ts b/src/lib/flex/layout-align/layout-align.ts index 777c8d3c5..8761f4a4e 100644 --- a/src/lib/flex/layout-align/layout-align.ts +++ b/src/lib/flex/layout-align/layout-align.ts @@ -15,14 +15,100 @@ import { Optional, SimpleChanges, Self, + Injectable, } from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import { + BaseDirective, + MediaChange, + MediaMonitor, + StyleBuilder, + StyleDefinition, + StyleUtils +} from '@angular/flex-layout/core'; import {Subscription} from 'rxjs'; import {extendObject} from '../../utils/object-extend'; import {Layout, LayoutDirective} from '../layout/layout'; import {LAYOUT_VALUES, isFlowHorizontal} from '../../utils/layout-validator'; +interface LayoutAlignParent { + layout: string; +} + +@Injectable({providedIn: 'root'}) +export class LayoutAlignStyleBuilder implements StyleBuilder { + buildStyles(align: string, parent: LayoutAlignParent): StyleDefinition { + let css: {[key: string]: string} = {}, + [mainAxis, crossAxis] = align.split(' '); + + // Main axis + switch (mainAxis) { + case 'center': + css['justify-content'] = 'center'; + break; + case 'space-around': + css['justify-content'] = 'space-around'; + break; + case 'space-between': + css['justify-content'] = 'space-between'; + break; + case 'space-evenly': + css['justify-content'] = 'space-evenly'; + break; + case 'end': + case 'flex-end': + css['justify-content'] = 'flex-end'; + break; + case 'start': + case 'flex-start': + default : + css['justify-content'] = 'flex-start'; // default main axis + break; + } + + // Cross-axis + switch (crossAxis) { + case 'start': + case 'flex-start': + css['align-items'] = css['align-content'] = 'flex-start'; + break; + case 'center': + css['align-items'] = css['align-content'] = 'center'; + break; + case 'end': + case 'flex-end': + css['align-items'] = css['align-content'] = 'flex-end'; + break; + case 'space-between': + css['align-content'] = 'space-between'; + css['align-items'] = 'stretch'; + break; + case 'space-around': + css['align-content'] = 'space-around'; + css['align-items'] = 'stretch'; + break; + case 'baseline': + css['align-content'] = 'stretch'; + css['align-items'] = 'baseline'; + break; + case 'stretch': + default : // 'stretch' + css['align-items'] = css['align-content'] = 'stretch'; // default cross axis + break; + } + + return extendObject(css, { + 'display' : 'flex', + 'flex-direction' : parent.layout, + 'box-sizing' : 'border-box', + 'max-width': crossAxis === 'stretch' ? + !isFlowHorizontal(parent.layout) ? '100%' : null : null, + 'max-height': crossAxis === 'stretch' ? + isFlowHorizontal(parent.layout) ? '100%' : null : null, + }); + } +} + /** * 'layout-align' flexbox styling directive * Defines positioning of child elements along main and cross axis in a layout container @@ -66,8 +152,9 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha monitor: MediaMonitor, elRef: ElementRef, @Optional() @Self() container: LayoutDirective, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); + 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)); @@ -116,8 +203,8 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha value = this._mqActivation.activatedInput; } - this._applyStyleToElement(this._buildCSS(value)); - this._allowStretching(value, !this._layout ? 'row' : this._layout); + const layout = this._layout || 'row'; + this.addStyles(value || '', {layout}); } /** @@ -133,93 +220,6 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha if (this._mqActivation) { value = this._mqActivation.activatedInput; } - this._allowStretching(value, this._layout || 'row'); - } - - protected _buildCSS(align: string = '') { - let css: {[key: string]: string} = {}, - [main_axis, cross_axis] = align.split(' '); // tslint:disable-line:variable-name - - // Main axis - switch (main_axis) { - case 'center': - css['justify-content'] = 'center'; - break; - case 'space-around': - css['justify-content'] = 'space-around'; - break; - case 'space-between': - css['justify-content'] = 'space-between'; - break; - case 'space-evenly': - css['justify-content'] = 'space-evenly'; - break; - case 'end': - case 'flex-end': - css['justify-content'] = 'flex-end'; - break; - case 'start': - case 'flex-start': - default : - css['justify-content'] = 'flex-start'; // default main axis - break; - } - - // Cross-axis - switch (cross_axis) { - case 'start': - case 'flex-start': - css['align-items'] = css['align-content'] = 'flex-start'; - break; - case 'center': - css['align-items'] = css['align-content'] = 'center'; - break; - case 'end': - case 'flex-end': - css['align-items'] = css['align-content'] = 'flex-end'; - break; - case 'space-between': - css['align-content'] = 'space-between'; - css['align-items'] = 'stretch'; - break; - case 'space-around': - css['align-content'] = 'space-around'; - css['align-items'] = 'stretch'; - break; - case 'baseline': - css['align-content'] = 'stretch'; - css['align-items'] = 'baseline'; - break; - case 'stretch': - default : // 'stretch' - css['align-items'] = css['align-content'] = 'stretch'; // default cross axis - break; - } - - return extendObject(css, { - 'display' : 'flex', - 'flex-direction' : this._layout || 'row', - 'box-sizing' : 'border-box', - 'max-width': null, - 'max-height': null, - }); - } - - - /** - * Update container element to 'stretch' as needed... - * NOTE: this is only done if the crossAxis is explicitly set to 'stretch' - */ - protected _allowStretching(align: string = '', layout: string = '') { - let [, cross_axis] = align.split(' '); // tslint:disable-line:variable-name - - if (cross_axis == 'stretch') { - // Use `null` values to remove style - this._applyStyleToElement({ - 'box-sizing': 'border-box', - 'max-width': !isFlowHorizontal(layout) ? '100%' : null, - 'max-height': isFlowHorizontal(layout) ? '100%' : null - }); - } + this.addStyles(value, {layout: this._layout || 'row'}); } } diff --git a/src/lib/flex/layout-gap/layout-gap.spec.ts b/src/lib/flex/layout-gap/layout-gap.spec.ts index fdd929764..24c87c474 100644 --- a/src/lib/flex/layout-gap/layout-gap.spec.ts +++ b/src/lib/flex/layout-gap/layout-gap.spec.ts @@ -5,11 +5,16 @@ * 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, OnInit, PLATFORM_ID} from '@angular/core'; +import {Component, Injectable, OnInit, PLATFORM_ID} from '@angular/core'; import {CommonModule, isPlatformServer} from '@angular/common'; import {TestBed, ComponentFixture, async, inject} from '@angular/core/testing'; import {DIR_DOCUMENT} from '@angular/cdk/bidi'; -import {SERVER_TOKEN, StyleUtils} from '@angular/flex-layout/core'; +import { + MockMatchMediaProvider, + SERVER_TOKEN, + StyleBuilder, + StyleUtils, +} from '@angular/flex-layout/core'; import {FlexLayoutModule} from '../../module'; import {customMatchers, expect} from '../../utils/testing/custom-matchers'; @@ -19,6 +24,8 @@ import { makeCreateTestComponent, queryFor, } from '../../utils/testing/helpers'; +import {FlexModule} from '../module'; +import {LayoutGapStyleBuilder} from './layout-gap'; describe('layout-gap directive', () => { let fixture: ComponentFixture; @@ -390,8 +397,49 @@ describe('layout-gap directive', () => { }); }); + describe('with custom builder', () => { + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [ + CommonModule, + FlexLayoutModule.withConfig({ + useColumnBasisZero: false, + serverLoaded: true, + }), + ], + declarations: [], + providers: [ + MockMatchMediaProvider, + { + provide: LayoutGapStyleBuilder, + useClass: MockLayoutGapStyleBuilder, + } + ] + }); + }); + + it('should set gap not to input', async(() => { + createTestComponent(` +
+
+
+ `); + expectNativeEl(fixture).toHaveStyle({'margin-top': '12px'}, styler); + })); + }); + }); +@Injectable({providedIn: FlexModule}) +export class MockLayoutGapStyleBuilder implements StyleBuilder { + buildStyles(_input: string) { + return {'margin-top': '12px'}; + } +} + // ***************************************************************** // Template Component diff --git a/src/lib/flex/layout-gap/layout-gap.ts b/src/lib/flex/layout-gap/layout-gap.ts index af909231b..53c1bf488 100644 --- a/src/lib/flex/layout-gap/layout-gap.ts +++ b/src/lib/flex/layout-gap/layout-gap.ts @@ -16,12 +16,14 @@ import { Optional, OnDestroy, NgZone, + Injectable, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import { BaseDirective, MediaChange, MediaMonitor, + StyleBuilder, StyleDefinition, StyleUtils } from '@angular/flex-layout/core'; @@ -30,6 +32,73 @@ import {Subscription} from 'rxjs'; import {Layout, LayoutDirective} from '../layout/layout'; import {LAYOUT_VALUES} from '../../utils/layout-validator'; +interface LayoutGapParent { + layout: string; + directionality: string; + items: HTMLElement[]; +} + +const CLEAR_MARGIN_CSS = { + 'margin-left': null, + 'margin-right': null, + 'margin-top': null, + 'margin-bottom': null +}; + +@Injectable({providedIn: 'root'}) +export class LayoutGapStyleBuilder implements StyleBuilder { + constructor(private styler: StyleUtils) {} + + buildStyles(gapValue: string, parent: LayoutGapParent): StyleDefinition { + const items = parent.items; + if (gapValue.endsWith(GRID_SPECIFIER)) { + gapValue = gapValue.substring(0, gapValue.indexOf(GRID_SPECIFIER)); + // For each `element` children, set the padding + const paddingStyles = buildGridPadding(gapValue, parent.directionality); + const marginStyles = buildGridMargin(gapValue, parent.directionality); + this.styler.applyStyleToElements(paddingStyles, items); + + // Add the margin to the host element + return marginStyles; + } else { + const lastItem = items.pop(); + + // For each `element` children EXCEPT the last, + // set the margin right/bottom styles... + this.styler.applyStyleToElements(this._buildCSS(gapValue, parent), items); + + // Clear all gaps for all visible elements + this.styler.applyStyleToElements(CLEAR_MARGIN_CSS, [lastItem!]); + return {}; + } + } + + private _buildCSS(gapValue: string, parent: LayoutGapParent): StyleDefinition { + let key, margins: {[key: string]: string | null} = {...CLEAR_MARGIN_CSS}; + + switch (parent.layout) { + case 'column': + key = 'margin-bottom'; + break; + case 'column-reverse': + key = 'margin-top'; + break; + case 'row': + key = parent.directionality === 'rtl' ? 'margin-left' : 'margin-right'; + break; + case 'row-reverse': + key = parent.directionality === 'rtl' ? 'margin-right' : 'margin-left'; + break; + default : + key = parent.directionality === 'rtl' ? 'margin-left' : 'margin-right'; + break; + } + margins[key] = gapValue; + + return margins; + } +} + /** * 'layout-padding' styling directive * Defines padding of child elements in a layout container @@ -47,7 +116,7 @@ export class LayoutGapDirective extends BaseDirective protected _layout = 'row'; // default flex-direction protected _layoutWatcher?: Subscription; protected _observer?: MutationObserver; - private _directionWatcher: Subscription; + private readonly _directionWatcher: Subscription; /* tslint:disable */ @Input('fxLayoutGap') set gap(val: string) { this._cacheInput('gap', val); } @@ -73,8 +142,9 @@ export class LayoutGapDirective extends BaseDirective @Optional() @Self() container: LayoutDirective, private _zone: NgZone, private _directionality: Directionality, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); + styleUtils: StyleUtils, + styleBuilder: LayoutGapStyleBuilder) { + super(monitor, elRef, styleUtils, styleBuilder); if (container) { // Subscribe to layout direction changes this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); @@ -180,91 +250,37 @@ export class LayoutGapDirective extends BaseDirective }); if (items.length > 0) { - if (gapValue.endsWith(GRID_SPECIFIER)) { - gapValue = gapValue.substring(0, gapValue.indexOf(GRID_SPECIFIER)); - // For each `element` children, set the padding - this._applyStyleToElements(this._buildGridPadding(gapValue), items); - - // Add the margin to the host element - this._applyStyleToElement(this._buildGridMargin(gapValue)); - } else { - const lastItem = items.pop(); - - // For each `element` children EXCEPT the last, - // set the margin right/bottom styles... - this._applyStyleToElements(this._buildCSS(gapValue), items); - - // Clear all gaps for all visible elements - this._applyStyleToElements(this._buildCSS(), [lastItem]); - } + this.addStyles(gapValue, { + directionality: this._directionality.value, + items, + layout: this._layout + }); } } +} - /** - * - */ - private _buildGridPadding(value: string): StyleDefinition { - let paddingTop = '0px', paddingRight = '0px', paddingBottom = value, paddingLeft = '0px'; +const GRID_SPECIFIER = ' grid'; - if (this._directionality.value === 'rtl') { - paddingLeft = value; - } else { - paddingRight = value; - } +function buildGridPadding(value: string, directionality: string): StyleDefinition { + let paddingTop = '0px', paddingRight = '0px', paddingBottom = value, paddingLeft = '0px'; - return {'padding': `${paddingTop} ${paddingRight} ${paddingBottom} ${paddingLeft}`}; + if (directionality === 'rtl') { + paddingLeft = value; + } else { + paddingRight = value; } - /** - * Prepare margin CSS, remove any previous explicitly - * assigned margin assignments - * Note: this will not work with calc values (negative calc values are invalid) - */ - private _buildGridMargin(value: string): StyleDefinition { - let marginTop = '0px', marginRight = '0px', marginBottom = '-' + value, marginLeft = '0px'; + return {'padding': `${paddingTop} ${paddingRight} ${paddingBottom} ${paddingLeft}`}; +} - if (this._directionality.value === 'rtl') { - marginLeft = '-' + value; - } else { - marginRight = '-' + value; - } +function buildGridMargin(value: string, directionality: string): StyleDefinition { + let marginTop = '0px', marginRight = '0px', marginBottom = '-' + value, marginLeft = '0px'; - return {'margin': `${marginTop} ${marginRight} ${marginBottom} ${marginLeft}`}; + if (directionality === 'rtl') { + marginLeft = '-' + value; + } else { + marginRight = '-' + value; } - /** - * Prepare margin CSS, remove any previous explicitly - * assigned margin assignments - */ - private _buildCSS(value: any = null) { - let key, margins: {[key: string]: string | null} = { - 'margin-left': null, - 'margin-right': null, - 'margin-top': null, - 'margin-bottom': null - }; - - switch (this._layout) { - case 'column': - key = 'margin-bottom'; - break; - case 'column-reverse': - key = 'margin-top'; - break; - case 'row': - key = this._directionality.value === 'rtl' ? 'margin-left' : 'margin-right'; - break; - case 'row-reverse': - key = this._directionality.value === 'rtl' ? 'margin-right' : 'margin-left'; - break; - default : - key = this._directionality.value === 'rtl' ? 'margin-left' : 'margin-right'; - break; - } - margins[key] = value; - - return margins; - } + return {'margin': `${marginTop} ${marginRight} ${marginBottom} ${marginLeft}`}; } - -const GRID_SPECIFIER = ' grid'; diff --git a/src/lib/flex/layout/layout.spec.ts b/src/lib/flex/layout/layout.spec.ts index f2df55a28..7cf922e29 100644 --- a/src/lib/flex/layout/layout.spec.ts +++ b/src/lib/flex/layout/layout.spec.ts @@ -5,14 +5,15 @@ * 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, OnInit} from '@angular/core'; +import {Component, Injectable, OnInit} from '@angular/core'; import {CommonModule} from '@angular/common'; -import {ComponentFixture, TestBed, inject} from '@angular/core/testing'; +import {ComponentFixture, TestBed, inject, async} from '@angular/core/testing'; import { MatchMedia, MockMatchMedia, MockMatchMediaProvider, SERVER_TOKEN, + StyleBuilder, StyleUtils, } from '@angular/flex-layout/core'; @@ -20,6 +21,9 @@ import {FlexLayoutModule} from '../../module'; import {customMatchers} from '../../utils/testing/custom-matchers'; import {makeCreateTestComponent, expectNativeEl, expectEl} from '../../utils/testing/helpers'; import {queryFor} from '../../utils/testing/helpers'; +import {FlexModule} from '../module'; +import {Layout, LayoutStyleBuilder} from './layout'; +import {ReplaySubject} from 'rxjs'; describe('layout directive', () => { let fixture: ComponentFixture; @@ -325,8 +329,52 @@ describe('layout directive', () => { }); + describe('with custom builder', () => { + beforeEach(() => { + jasmine.addMatchers(customMatchers); + + // Configure testbed to prepare services + TestBed.configureTestingModule({ + imports: [ + CommonModule, + FlexLayoutModule.withConfig({ + useColumnBasisZero: false, + serverLoaded: true, + }), + ], + providers: [ + MockMatchMediaProvider, + { + provide: LayoutStyleBuilder, + useClass: MockLayoutStyleBuilder, + } + ] + }); + }); + + it('should set layout not to input', async(() => { + createTestComponent(` +
+
+
+ `); + expectNativeEl(fixture).toHaveStyle({'display': 'inline-flex'}, styler); + })); + }); + }); +@Injectable({providedIn: FlexModule}) +export class MockLayoutStyleBuilder implements StyleBuilder { + buildStyles(_input: string, parent: {announcer: ReplaySubject}) { + parent.announcer.next({ + direction: 'column', + wrap: false + }); + return {'display': 'inline-flex'}; + } +} + // ***************************************************************** // Template Component diff --git a/src/lib/flex/layout/layout.ts b/src/lib/flex/layout/layout.ts index 18323beba..161533a27 100644 --- a/src/lib/flex/layout/layout.ts +++ b/src/lib/flex/layout/layout.ts @@ -13,8 +13,16 @@ import { OnChanges, OnDestroy, SimpleChanges, + Injectable, } from '@angular/core'; -import {BaseDirective, MediaChange, MediaMonitor, StyleUtils} from '@angular/flex-layout/core'; +import { + BaseDirective, + MediaChange, + MediaMonitor, + StyleBuilder, + StyleDefinition, + StyleUtils +} from '@angular/flex-layout/core'; import {Observable, ReplaySubject} from 'rxjs'; import {buildLayoutCSS} from '../../utils/layout-validator'; @@ -24,6 +32,22 @@ export type Layout = { wrap: boolean; }; +interface LayoutParent { + announcer: ReplaySubject; +} + +@Injectable({providedIn: 'root'}) +export class LayoutStyleBuilder implements StyleBuilder { + buildStyles(input: string, parent: LayoutParent): StyleDefinition { + const css = buildLayoutCSS(input); + parent.announcer.next({ + direction: css['flex-direction'], + wrap: !!css['flex-wrap'] && css['flex-wrap'] !== 'nowrap' + }); + return css; + } +} + /** * 'layout' flexbox styling directive * Defines the positioning flow direction for the child elements: row or column @@ -72,8 +96,9 @@ export class LayoutDirective extends BaseDirective implements OnInit, OnChanges, constructor(monitor: MediaMonitor, elRef: ElementRef, - styleUtils: StyleUtils) { - super(monitor, elRef, styleUtils); + styleUtils: StyleUtils, + styleBuilder: LayoutStyleBuilder) { + super(monitor, elRef, styleUtils, styleBuilder); this._announcer = new ReplaySubject(1); this.layout$ = this._announcer.asObservable(); } @@ -116,14 +141,7 @@ export class LayoutDirective extends BaseDirective implements OnInit, OnChanges, value = this._mqActivation.activatedInput; } - // Update styles and announce to subscribers the *new* direction - let css = buildLayoutCSS(!!value ? value : ''); - - this._applyStyleToElement(css); - this._announcer.next({ - direction: css['flex-direction'], - wrap: !!css['flex-wrap'] && css['flex-wrap'] !== 'nowrap' - }); + this.addStyles(value || '', {announcer: this._announcer}); } }