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

Commit 2340a19

Browse files
ThomasBurlesontinayuangao
authored andcommitted
feat(layout): add wrap options support to fxLayout (#207)
* 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
1 parent 7cad395 commit 2340a19

File tree

7 files changed

+213
-50
lines changed

7 files changed

+213
-50
lines changed

src/lib/flexbox/api/flex.spec.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
*/
88
import {Component, OnInit} from '@angular/core';
99
import {CommonModule} from '@angular/common';
10-
import {ComponentFixture, TestBed} from '@angular/core/testing';
10+
import {ComponentFixture, TestBed, async} from '@angular/core/testing';
1111

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

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

2121
import {
2222
makeExpectDOMFrom,
@@ -118,6 +118,21 @@ describe('flex directive', () => {
118118
}
119119
});
120120

121+
it('should work with calc without internal whitespaces', async(() => {
122+
// @see http://caniuse.com/#feat=calc for IE issues with calc()
123+
if (!isIE) {
124+
let fRef = componentWithTemplate('<div fxFlex="calc(75%-10px)"></div>');
125+
fRef.detectChanges();
126+
127+
setTimeout(() => {
128+
expectNativeEl(fRef).toHaveCssStyle({
129+
'box-sizing': 'border-box',
130+
'flex': '1 1 calc(75% - 10px)' // correct version has whitespace
131+
});
132+
});
133+
}
134+
}));
135+
121136
it('should work with "auto" values', () => {
122137
expectDOMFrom(`<div fxFlex="auto"></div>`).toHaveCssStyle({
123138
'flex': '1 1 auto'

src/lib/flexbox/api/flex.ts

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {MediaMonitor} from '../../media-query/media-monitor';
2626

2727
import {LayoutDirective} from './layout';
2828
import {LayoutWrapDirective} from './layout-wrap';
29+
import {validateBasis} from '../../utils/basis-validator';
2930

3031

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

181-
this._applyStyleToElement(this._validateValue.apply(this,
182-
this._parseFlexParts(String(flexBasis))));
183-
}
184-
185-
/**
186-
* If the used the short-form `fxFlex="1 0 37%"`, then parse the parts
187-
*/
188-
protected _parseFlexParts(basis: string) {
189-
basis = basis.replace(";", "");
190-
191-
let hasCalc = basis && basis.indexOf("calc") > -1;
192-
let matches = !hasCalc ? basis.split(" ") : this._getPartsWithCalc(basis.trim());
193-
return (matches.length === 3) ? matches : [this._queryInput("grow"),
194-
this._queryInput("shrink"), basis];
195-
}
196-
197-
/**
198-
* Extract more complicated short-hand versions.
199-
* e.g.
200-
* fxFlex="3 3 calc(15em + 20px)"
201-
*/
202-
protected _getPartsWithCalc(value: string) {
203-
let parts = [this._queryInput("grow"), this._queryInput("shrink"), value];
204-
let j = value.indexOf('calc');
205-
206-
if (j > 0) {
207-
parts[2] = value.substring(j);
208-
let matches = value.substr(0, j).trim().split(" ");
209-
if (matches.length == 2) {
210-
parts[0] = matches[0];
211-
parts[1] = matches[1];
212-
}
213-
}
214-
return parts;
182+
let basis = String(flexBasis).replace(";", "");
183+
let parts = validateBasis(basis, this._queryInput("grow"), this._queryInput("shrink"));
184+
this._applyStyleToElement(this._validateValue.apply(this, parts));
215185
}
216186

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

282-
isValue = String(basis).indexOf('px') > -1 ||
283-
String(basis).indexOf('calc') > -1 ||
253+
isValue = hasCalc ||
254+
String(basis).indexOf('px') > -1 ||
284255
String(basis).indexOf('em') > -1 ||
285256
String(basis).indexOf('vw') > -1 ||
286257
String(basis).indexOf('vh') > -1;

src/lib/flexbox/api/layout-wrap.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ import {MediaMonitor} from '../../media-query/media-monitor';
2424
import {LayoutDirective, LAYOUT_VALUES} from './layout';
2525

2626
/**
27+
* @deprecated
28+
* This functionality is now part of the `fxLayout` API
29+
*
2730
* 'layout-wrap' flexbox styling directive
2831
* Defines wrapping of child elements in layout container
2932
* Optional values: reverse, wrap-reverse, none, nowrap, wrap (default)]
33+
*
34+
*
3035
* @see https://css-tricks.com/almanac/properties/f/flex-wrap/
3136
*/
3237
@Directive({selector: `

src/lib/flexbox/api/layout.spec.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ describe('layout directive', () => {
7171
'box-sizing': 'border-box'
7272
});
7373
});
74+
it('should add correct styles for `fxLayout="row wrap"` usage', () => {
75+
expectDOMFrom(`
76+
<div fxLayout="row wrap"></div>
77+
`).toHaveCssStyle({
78+
'display': 'flex',
79+
'flex-direction': 'row',
80+
'box-sizing': 'border-box',
81+
'flex-wrap': "wrap"
82+
});
83+
});
7484
it('should add correct styles for `fxLayout="column"` usage', () => {
7585
expectDOMFrom(`<div fxLayout="column"></div>`).toHaveCssStyle({
7686
'display': 'flex',
@@ -146,7 +156,7 @@ describe('layout directive', () => {
146156
});
147157
});
148158
it('should add responsive styles when configured', () => {
149-
fixture = createTestComponent(`<div fxLayout fxLayout.md="column"></div>`);
159+
fixture = createTestComponent(`<div fxLayout fxLayout.md="column reverse-wrap"></div>`);
150160

151161
expectNativeEl(fixture).toHaveCssStyle({
152162

@@ -159,7 +169,13 @@ describe('layout directive', () => {
159169
expectNativeEl(fixture).toHaveCssStyle({
160170
'display': 'flex',
161171
'flex-direction': 'column',
162-
'box-sizing': 'border-box'
172+
'box-sizing': 'border-box',
173+
'flex-wrap': 'wrap-reverse'
174+
});
175+
176+
activateMediaQuery('lg');
177+
expectNativeEl(fixture).not.toHaveCssStyle({
178+
'flex-wrap': 'reverse-wrap'
163179
});
164180
});
165181
it('should update responsive styles when the active mediaQuery changes', () => {

src/lib/flexbox/api/layout.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange
110110
/**
111111
* Validate the direction value and then update the host's inline flexbox styles
112112
*/
113-
protected _updateWithDirection(direction?: string) {
114-
direction = direction || this._queryInput("layout") || 'row';
113+
protected _updateWithDirection(value?: string) {
114+
value = value || this._queryInput("layout") || 'row';
115115
if (this._mqActivation) {
116-
direction = this._mqActivation.activatedInput;
116+
value = this._mqActivation.activatedInput;
117117
}
118-
direction = this._validateValue(direction);
118+
let [direction, wrap] = this._validateValue(value);
119119

120120
// Update styles and announce to subscribers the *new* direction
121-
this._applyStyleToElement(this._buildCSS(direction));
121+
this._applyStyleToElement(this._buildCSS(direction, wrap));
122122
this._announcer.next(direction);
123123
}
124124

@@ -135,8 +135,13 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange
135135
* laid out and drawn inside that element's specified width and height.
136136
*
137137
*/
138-
protected _buildCSS(value) {
139-
return {'display': 'flex', 'box-sizing': 'border-box', 'flex-direction': value};
138+
protected _buildCSS(direction, wrap = null) {
139+
return {
140+
'display': 'flex',
141+
'box-sizing': 'border-box',
142+
'flex-direction': direction,
143+
'flex-wrap': !!wrap ? wrap : null
144+
};
140145
}
141146

142147
/**
@@ -145,6 +150,38 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange
145150
*/
146151
protected _validateValue(value) {
147152
value = value ? value.toLowerCase() : '';
148-
return LAYOUT_VALUES.find(x => x === value) ? value : LAYOUT_VALUES[0]; // "row"
153+
let [ direction, wrap ] = value.split(" ");
154+
if (!LAYOUT_VALUES.find(x => x === direction)) {
155+
direction = LAYOUT_VALUES[0];
156+
}
157+
return [direction, this._validateWrapValue(wrap)];
158+
149159
}
160+
161+
/**
162+
* Convert layout-wrap="<value>" to expected flex-wrap style
163+
*/
164+
protected _validateWrapValue(value) {
165+
if (!!value) {
166+
switch (value.toLowerCase()) {
167+
case 'reverse':
168+
case 'wrap-reverse':
169+
case 'reverse-wrap':
170+
value = 'wrap-reverse';
171+
break;
172+
173+
case 'no':
174+
case 'none':
175+
case 'nowrap':
176+
value = 'nowrap';
177+
break;
178+
179+
// All other values fallback to "wrap"
180+
default:
181+
value = 'wrap';
182+
break;
183+
}
184+
}
185+
return value;
186+
}
150187
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {validateBasis} from './basis-validator';
9+
10+
11+
describe('validateBasis', () => {
12+
13+
it('should validate single-value basis with default grow/shrink', () => {
14+
let result = validateBasis('37px').join(" ");
15+
expect( result ).toEqual('1 1 37px');
16+
});
17+
18+
it('should validate single-value basis with custom grow and shrink', () => {
19+
let result = validateBasis('37px', "2", "13").join(" ");
20+
expect( result ).toEqual('2 13 37px');
21+
});
22+
23+
it('should validate full `flex` value `2 1 37%`', () => {
24+
let result = validateBasis('2 1 37%').join(" ");
25+
expect( result ).toEqual('2 1 37%');
26+
});
27+
28+
it('should validate with complex value that includes calc()', () => {
29+
let result = validateBasis('3 3 calc(15em + 20px)').join(" ");
30+
expect( result ).toEqual('3 3 calc(15em + 20px)');
31+
});
32+
33+
it('should validate with complex value that includes a bad calc() expression', () => {
34+
let result = validateBasis('3 3 calc(15em +20px)').join(" ");
35+
expect( result ).toEqual('3 3 calc(15em + 20px)');
36+
});
37+
38+
it('should validate with complex value that includes ONLY calc()', () => {
39+
let result = validateBasis('calc(15em + 20px)').join(" ");
40+
expect( result ).toEqual('1 1 calc(15em + 20px)');
41+
});
42+
43+
it('should validate with good calc(x + x) expression()', () => {
44+
let result = validateBasis('calc(15em + 20px)').join(" ");
45+
expect( result ).toEqual('1 1 calc(15em + 20px)');
46+
});
47+
48+
it('should validate with bad calc(x+x) expression()', () => {
49+
let result = validateBasis('calc(15em+20px)').join(" ");
50+
expect( result ).toEqual('1 1 calc(15em + 20px)');
51+
});
52+
53+
it('should validate with bad calc(x-x) expression()', () => {
54+
let result = validateBasis('calc(15em-20px)').join(" ");
55+
expect( result ).toEqual('1 1 calc(15em - 20px)');
56+
});
57+
58+
it('should validate with bad calc(x*x) expression()', () => {
59+
let result = validateBasis('calc(15em*20px)').join(" ");
60+
expect( result ).toEqual('1 1 calc(15em * 20px)');
61+
});
62+
63+
it('should validate with bad calc(x/x) expression()', () => {
64+
let result = validateBasis('calc(15em/20px)').join(" ");
65+
expect( result ).toEqual('1 1 calc(15em / 20px)');
66+
});
67+
});

src/lib/utils/basis-validator.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* The flex API permits 3 or 1 parts of the value:
3+
* - `flex-grow flex-shrink flex-basis`, or
4+
* - `flex-basis`
5+
* Flex-Basis values can be complicated short-hand versions such as:
6+
* - "3 3 calc(15em + 20px)"
7+
* - "calc(15em + 20px)"
8+
* - "calc(15em+20px)"
9+
* - "37px"
10+
* = "43%"
11+
*/
12+
export function validateBasis(basis: string, grow = "1", shrink = "1"): string[] {
13+
let parts = [grow, shrink, basis];
14+
15+
let j = basis.indexOf('calc');
16+
if (j > 0) {
17+
parts[2] = _validateCalcValue(basis.substring(j).trim());
18+
let matches = basis.substr(0, j).trim().split(" ");
19+
if (matches.length == 2) {
20+
parts[0] = matches[0];
21+
parts[1] = matches[1];
22+
}
23+
} else if (j == 0) {
24+
parts[2] = _validateCalcValue(basis.trim());
25+
} else {
26+
let matches = basis.split(" ");
27+
parts = (matches.length === 3) ? matches : [
28+
grow, shrink, basis
29+
];
30+
}
31+
32+
return parts;
33+
}
34+
35+
36+
/**
37+
* Calc expressions require whitespace before & after the operator
38+
* This is a simple, crude whitespace padding solution.
39+
*/
40+
function _validateCalcValue(calc: string): string {
41+
let operators = ["+", "-", "*", "/"];
42+
let findOperator = () => operators.reduce((index, operator) => {
43+
return index || (calc.indexOf(operator) + 1);
44+
}, 0);
45+
46+
if (findOperator() > 0) {
47+
calc = calc.replace(/[\s]/g, "");
48+
let offset = findOperator() - 1;
49+
calc = calc.substr(0, offset) + " " + calc.charAt(offset) + " " + calc.substr(offset + 1);
50+
}
51+
return calc;
52+
}

0 commit comments

Comments
 (0)