Skip to content

Commit

Permalink
feat(layout): add wrap options support to fxLayout (#207)
Browse files Browse the repository at this point in the history
* feat(fxLayout): add 'wrap' options support to fxLayout

* add support for wrap options as part of fxLayout
* marked fxLayoutWrap as deprecated (but still supported)

* fix(fxFlex): improve support of flex-basis variations

* improve support for fxFlex options and parsing
* add calc() auto-corrections for bad whitespacing
* add basis-validator tools
  • Loading branch information
ThomasBurleson authored and tinayuangao committed Mar 8, 2017
1 parent 7cad395 commit 2340a19
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 50 deletions.
21 changes: 18 additions & 3 deletions src/lib/flexbox/api/flex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
*/
import {Component, OnInit} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentFixture, TestBed, async} from '@angular/core/testing';

import {BreakPointsProvider} from '../../media-query/breakpoints/break-points';
import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry';
import {MockMatchMedia} from '../../media-query/mock/mock-match-media';
import {MatchMedia} from '../../media-query/match-media';
import {FlexLayoutModule} from '../_module';

import {customMatchers, expect } from '../../utils/testing/custom-matchers';
import { _dom as _ } from '../../utils/testing/dom-tools';
import {customMatchers, expect} from '../../utils/testing/custom-matchers';
import {_dom as _} from '../../utils/testing/dom-tools';

import {
makeExpectDOMFrom,
Expand Down Expand Up @@ -118,6 +118,21 @@ describe('flex directive', () => {
}
});

it('should work with calc without internal whitespaces', async(() => {
// @see http://caniuse.com/#feat=calc for IE issues with calc()
if (!isIE) {
let fRef = componentWithTemplate('<div fxFlex="calc(75%-10px)"></div>');
fRef.detectChanges();

setTimeout(() => {
expectNativeEl(fRef).toHaveCssStyle({
'box-sizing': 'border-box',
'flex': '1 1 calc(75% - 10px)' // correct version has whitespace
});
});
}
}));

it('should work with "auto" values', () => {
expectDOMFrom(`<div fxFlex="auto"></div>`).toHaveCssStyle({
'flex': '1 1 auto'
Expand Down
45 changes: 8 additions & 37 deletions src/lib/flexbox/api/flex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {MediaMonitor} from '../../media-query/media-monitor';

import {LayoutDirective} from './layout';
import {LayoutWrapDirective} from './layout-wrap';
import {validateBasis} from '../../utils/basis-validator';


/** Built-in aliases for different flex-basis values. */
Expand Down Expand Up @@ -178,40 +179,9 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges,
flexBasis = this._mqActivation.activatedInput;
}

this._applyStyleToElement(this._validateValue.apply(this,
this._parseFlexParts(String(flexBasis))));
}

/**
* If the used the short-form `fxFlex="1 0 37%"`, then parse the parts
*/
protected _parseFlexParts(basis: string) {
basis = basis.replace(";", "");

let hasCalc = basis && basis.indexOf("calc") > -1;
let matches = !hasCalc ? basis.split(" ") : this._getPartsWithCalc(basis.trim());
return (matches.length === 3) ? matches : [this._queryInput("grow"),
this._queryInput("shrink"), basis];
}

/**
* Extract more complicated short-hand versions.
* e.g.
* fxFlex="3 3 calc(15em + 20px)"
*/
protected _getPartsWithCalc(value: string) {
let parts = [this._queryInput("grow"), this._queryInput("shrink"), value];
let j = value.indexOf('calc');

if (j > 0) {
parts[2] = value.substring(j);
let matches = value.substr(0, j).trim().split(" ");
if (matches.length == 2) {
parts[0] = matches[0];
parts[1] = matches[1];
}
}
return parts;
let basis = String(flexBasis).replace(";", "");
let parts = validateBasis(basis, this._queryInput("grow"), this._queryInput("shrink"));
this._applyStyleToElement(this._validateValue.apply(this, parts));
}

/**
Expand Down Expand Up @@ -277,10 +247,11 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges,
css = extendObject(clearStyles, {'flex': '0 0 auto'});
break;
default:
let isPercent = String(basis).indexOf('%') > -1;
let hasCalc = String(basis).indexOf('calc') > -1;
let isPercent = String(basis).indexOf('%') > -1 && !hasCalc;

isValue = String(basis).indexOf('px') > -1 ||
String(basis).indexOf('calc') > -1 ||
isValue = hasCalc ||
String(basis).indexOf('px') > -1 ||
String(basis).indexOf('em') > -1 ||
String(basis).indexOf('vw') > -1 ||
String(basis).indexOf('vh') > -1;
Expand Down
5 changes: 5 additions & 0 deletions src/lib/flexbox/api/layout-wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ import {MediaMonitor} from '../../media-query/media-monitor';
import {LayoutDirective, LAYOUT_VALUES} from './layout';

/**
* @deprecated
* This functionality is now part of the `fxLayout` API
*
* 'layout-wrap' flexbox styling directive
* Defines wrapping of child elements in layout container
* Optional values: reverse, wrap-reverse, none, nowrap, wrap (default)]
*
*
* @see https://css-tricks.com/almanac/properties/f/flex-wrap/
*/
@Directive({selector: `
Expand Down
20 changes: 18 additions & 2 deletions src/lib/flexbox/api/layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ describe('layout directive', () => {
'box-sizing': 'border-box'
});
});
it('should add correct styles for `fxLayout="row wrap"` usage', () => {
expectDOMFrom(`
<div fxLayout="row wrap"></div>
`).toHaveCssStyle({
'display': 'flex',
'flex-direction': 'row',
'box-sizing': 'border-box',
'flex-wrap': "wrap"
});
});
it('should add correct styles for `fxLayout="column"` usage', () => {
expectDOMFrom(`<div fxLayout="column"></div>`).toHaveCssStyle({
'display': 'flex',
Expand Down Expand Up @@ -146,7 +156,7 @@ describe('layout directive', () => {
});
});
it('should add responsive styles when configured', () => {
fixture = createTestComponent(`<div fxLayout fxLayout.md="column"></div>`);
fixture = createTestComponent(`<div fxLayout fxLayout.md="column reverse-wrap"></div>`);

expectNativeEl(fixture).toHaveCssStyle({

Expand All @@ -159,7 +169,13 @@ describe('layout directive', () => {
expectNativeEl(fixture).toHaveCssStyle({
'display': 'flex',
'flex-direction': 'column',
'box-sizing': 'border-box'
'box-sizing': 'border-box',
'flex-wrap': 'wrap-reverse'
});

activateMediaQuery('lg');
expectNativeEl(fixture).not.toHaveCssStyle({
'flex-wrap': 'reverse-wrap'
});
});
it('should update responsive styles when the active mediaQuery changes', () => {
Expand Down
53 changes: 45 additions & 8 deletions src/lib/flexbox/api/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,15 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange
/**
* Validate the direction value and then update the host's inline flexbox styles
*/
protected _updateWithDirection(direction?: string) {
direction = direction || this._queryInput("layout") || 'row';
protected _updateWithDirection(value?: string) {
value = value || this._queryInput("layout") || 'row';
if (this._mqActivation) {
direction = this._mqActivation.activatedInput;
value = this._mqActivation.activatedInput;
}
direction = this._validateValue(direction);
let [direction, wrap] = this._validateValue(value);

// Update styles and announce to subscribers the *new* direction
this._applyStyleToElement(this._buildCSS(direction));
this._applyStyleToElement(this._buildCSS(direction, wrap));
this._announcer.next(direction);
}

Expand All @@ -135,8 +135,13 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange
* laid out and drawn inside that element's specified width and height.
*
*/
protected _buildCSS(value) {
return {'display': 'flex', 'box-sizing': 'border-box', 'flex-direction': value};
protected _buildCSS(direction, wrap = null) {
return {
'display': 'flex',
'box-sizing': 'border-box',
'flex-direction': direction,
'flex-wrap': !!wrap ? wrap : null
};
}

/**
Expand All @@ -145,6 +150,38 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange
*/
protected _validateValue(value) {
value = value ? value.toLowerCase() : '';
return LAYOUT_VALUES.find(x => x === value) ? value : LAYOUT_VALUES[0]; // "row"
let [ direction, wrap ] = value.split(" ");
if (!LAYOUT_VALUES.find(x => x === direction)) {
direction = LAYOUT_VALUES[0];
}
return [direction, this._validateWrapValue(wrap)];

}

/**
* Convert layout-wrap="<value>" to expected flex-wrap style
*/
protected _validateWrapValue(value) {
if (!!value) {
switch (value.toLowerCase()) {
case 'reverse':
case 'wrap-reverse':
case 'reverse-wrap':
value = 'wrap-reverse';
break;

case 'no':
case 'none':
case 'nowrap':
value = 'nowrap';
break;

// All other values fallback to "wrap"
default:
value = 'wrap';
break;
}
}
return value;
}
}
67 changes: 67 additions & 0 deletions src/lib/utils/basis-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* @license
* Copyright Google Inc. 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 {validateBasis} from './basis-validator';


describe('validateBasis', () => {

it('should validate single-value basis with default grow/shrink', () => {
let result = validateBasis('37px').join(" ");
expect( result ).toEqual('1 1 37px');
});

it('should validate single-value basis with custom grow and shrink', () => {
let result = validateBasis('37px', "2", "13").join(" ");
expect( result ).toEqual('2 13 37px');
});

it('should validate full `flex` value `2 1 37%`', () => {
let result = validateBasis('2 1 37%').join(" ");
expect( result ).toEqual('2 1 37%');
});

it('should validate with complex value that includes calc()', () => {
let result = validateBasis('3 3 calc(15em + 20px)').join(" ");
expect( result ).toEqual('3 3 calc(15em + 20px)');
});

it('should validate with complex value that includes a bad calc() expression', () => {
let result = validateBasis('3 3 calc(15em +20px)').join(" ");
expect( result ).toEqual('3 3 calc(15em + 20px)');
});

it('should validate with complex value that includes ONLY calc()', () => {
let result = validateBasis('calc(15em + 20px)').join(" ");
expect( result ).toEqual('1 1 calc(15em + 20px)');
});

it('should validate with good calc(x + x) expression()', () => {
let result = validateBasis('calc(15em + 20px)').join(" ");
expect( result ).toEqual('1 1 calc(15em + 20px)');
});

it('should validate with bad calc(x+x) expression()', () => {
let result = validateBasis('calc(15em+20px)').join(" ");
expect( result ).toEqual('1 1 calc(15em + 20px)');
});

it('should validate with bad calc(x-x) expression()', () => {
let result = validateBasis('calc(15em-20px)').join(" ");
expect( result ).toEqual('1 1 calc(15em - 20px)');
});

it('should validate with bad calc(x*x) expression()', () => {
let result = validateBasis('calc(15em*20px)').join(" ");
expect( result ).toEqual('1 1 calc(15em * 20px)');
});

it('should validate with bad calc(x/x) expression()', () => {
let result = validateBasis('calc(15em/20px)').join(" ");
expect( result ).toEqual('1 1 calc(15em / 20px)');
});
});
52 changes: 52 additions & 0 deletions src/lib/utils/basis-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* The flex API permits 3 or 1 parts of the value:
* - `flex-grow flex-shrink flex-basis`, or
* - `flex-basis`
* Flex-Basis values can be complicated short-hand versions such as:
* - "3 3 calc(15em + 20px)"
* - "calc(15em + 20px)"
* - "calc(15em+20px)"
* - "37px"
* = "43%"
*/
export function validateBasis(basis: string, grow = "1", shrink = "1"): string[] {
let parts = [grow, shrink, basis];

let j = basis.indexOf('calc');
if (j > 0) {
parts[2] = _validateCalcValue(basis.substring(j).trim());
let matches = basis.substr(0, j).trim().split(" ");
if (matches.length == 2) {
parts[0] = matches[0];
parts[1] = matches[1];
}
} else if (j == 0) {
parts[2] = _validateCalcValue(basis.trim());
} else {
let matches = basis.split(" ");
parts = (matches.length === 3) ? matches : [
grow, shrink, basis
];
}

return parts;
}


/**
* Calc expressions require whitespace before & after the operator
* This is a simple, crude whitespace padding solution.
*/
function _validateCalcValue(calc: string): string {
let operators = ["+", "-", "*", "/"];
let findOperator = () => operators.reduce((index, operator) => {
return index || (calc.indexOf(operator) + 1);
}, 0);

if (findOperator() > 0) {
calc = calc.replace(/[\s]/g, "");
let offset = findOperator() - 1;
calc = calc.substr(0, offset) + " " + calc.charAt(offset) + " " + calc.substr(offset + 1);
}
return calc;
}

0 comments on commit 2340a19

Please sign in to comment.