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

Commit f0473e9

Browse files
ThomasBurlesonmmalerba
authored andcommitted
fix(fxFlexOffset): use parent flow direction for margin property (#369)
`fxFlexOffset` assigns inline `margin-left` style; assuming default flow directions == 'row'. If the parent flow direction == 'column', then a `margin-top` should be used. Also added a subscription to an optional parent element LayoutDirective; which will trigger the FlexOffsetDirective to update the inline styles to match the current flow direction. > Note: if `fxFlexOffset` is used **without** a parent flexbox styling (set via css or directive), then a `display:flex; flex-direction:row;` will be auto assigned to the fxFlexOffset host element's parent. Fixes #328.
1 parent 37a0b85 commit f0473e9

File tree

6 files changed

+242
-13
lines changed

6 files changed

+242
-13
lines changed

src/lib/flexbox/api/base.ts

+7
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,13 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
7373
// Accessor Methods
7474
// *********************************************
7575

76+
/**
77+
* Access to host element's parent DOM node
78+
*/
79+
protected get parentElement(): any {
80+
return this._elementRef.nativeElement.parentNode;
81+
}
82+
7683
/**
7784
* Access the current value (if any) of the @Input property.
7885
*/
+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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 {Component} from '@angular/core';
9+
import {CommonModule} from '@angular/common';
10+
import {ComponentFixture, TestBed, inject} from '@angular/core/testing';
11+
12+
import {DEFAULT_BREAKPOINTS_PROVIDER} from '../../media-query/breakpoints/break-points-provider';
13+
import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-registry';
14+
import {MockMatchMedia} from '../../media-query/mock/mock-match-media';
15+
import {MatchMedia} from '../../media-query/match-media';
16+
import {FlexLayoutModule} from '../../module';
17+
18+
import {customMatchers, expect} from '../../utils/testing/custom-matchers';
19+
import {_dom as _} from '../../utils/testing/dom-tools';
20+
21+
import {
22+
makeExpectDOMFrom,
23+
makeCreateTestComponent,
24+
queryFor
25+
} from '../../utils/testing/helpers';
26+
27+
describe('flex directive', () => {
28+
let fixture: ComponentFixture<any>;
29+
let matchMedia: MockMatchMedia;
30+
let expectDOMFrom = makeExpectDOMFrom(() => TestFlexComponent);
31+
let componentWithTemplate = (template: string) => {
32+
fixture = makeCreateTestComponent(() => TestFlexComponent)(template);
33+
34+
inject([MatchMedia], (_matchMedia: MockMatchMedia) => {
35+
matchMedia = _matchMedia;
36+
})();
37+
};
38+
39+
beforeEach(() => {
40+
jasmine.addMatchers(customMatchers);
41+
42+
// Configure testbed to prepare services
43+
TestBed.configureTestingModule({
44+
imports: [CommonModule, FlexLayoutModule],
45+
declarations: [TestFlexComponent],
46+
providers: [
47+
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
48+
{provide: MatchMedia, useClass: MockMatchMedia}
49+
]
50+
});
51+
});
52+
53+
describe('with static features', () => {
54+
55+
it('should add correct styles for default `fxFlexOffset` usage', () => {
56+
componentWithTemplate(`<div fxFlexOffset='32px' fxFlex></div>`);
57+
fixture.detectChanges();
58+
59+
let dom = fixture.debugElement.children[0].nativeElement;
60+
let isBox = _.hasStyle(dom, 'margin-left', '32px');
61+
let hasFlex = _.hasStyle(dom, 'flex', '1 1 1e-09px') || // IE
62+
_.hasStyle(dom, 'flex', '1 1 1e-9px') || // Chrome
63+
_.hasStyle(dom, 'flex', '1 1 0.000000001px') || // Safari
64+
_.hasStyle(dom, 'flex', '1 1 0px');
65+
66+
expect(isBox).toBeTruthy();
67+
expect(hasFlex).toBe(true);
68+
});
69+
70+
71+
it('should work with percentage values', () => {
72+
expectDOMFrom(`<div fxFlexOffset='17' fxFlex='37'></div>`).toHaveCssStyle({
73+
'flex': '1 1 100%',
74+
'box-sizing': 'border-box',
75+
'margin-left': '17%'
76+
});
77+
});
78+
79+
it('should work fxLayout parents', () => {
80+
componentWithTemplate(`
81+
<div fxLayout='column' class='test'>
82+
<div fxFlex='30px' fxFlexOffset='17px'> </div>
83+
</div>
84+
`);
85+
fixture.detectChanges();
86+
let parent = queryFor(fixture, '.test')[0].nativeElement;
87+
let element = queryFor(fixture, '[fxFlex]')[0].nativeElement;
88+
89+
// parent flex-direction found with 'column' with child height styles
90+
expect(parent).toHaveCssStyle({'flex-direction': 'column', 'display': 'flex'});
91+
expect(element).toHaveCssStyle({'margin-top': '17px'});
92+
});
93+
94+
it('should CSS stylesheet and not inject flex-direction on parent', () => {
95+
componentWithTemplate(`
96+
<style>
97+
.test { flex-direction:column; display: flex; }
98+
</style>
99+
<div class='test'>
100+
<div fxFlexOffset='41px' fxFlex='30px'></div>
101+
</div>
102+
`);
103+
104+
fixture.detectChanges();
105+
let parent = queryFor(fixture, '.test')[0].nativeElement;
106+
let element = queryFor(fixture, '[fxFlex]')[0].nativeElement;
107+
108+
// parent flex-direction found with 'column' with child height styles
109+
expect(parent).toHaveCssStyle({'flex-direction': 'column', 'display': 'flex'});
110+
expect(element).toHaveCssStyle({'margin-top': '41px'});
111+
});
112+
113+
it('should work with styled-parent flex directions', () => {
114+
componentWithTemplate(`
115+
<div fxLayout='row'>
116+
<div style='flex-direction:column' class='parent'>
117+
<div fxFlex='60px' fxFlexOffset='21'> </div>
118+
</div>
119+
</div>
120+
`);
121+
fixture.detectChanges();
122+
let element = queryFor(fixture, '[fxFlex]')[0].nativeElement;
123+
let parent = queryFor(fixture, '.parent')[0].nativeElement;
124+
125+
// parent flex-direction found with 'column'; set child with height styles
126+
expect(element).toHaveCssStyle({'margin-top': '21%'});
127+
expect(parent).toHaveCssStyle({'flex-direction': 'column'});
128+
});
129+
130+
it('should ignore fxLayout settings on same element', () => {
131+
expectDOMFrom(`
132+
<div fxLayout='column' fxFlex='37%' fxFlexOffset='52px' >
133+
</div>
134+
`)
135+
.not.toHaveCssStyle({
136+
'flex-direction': 'row',
137+
'flex': '1 1 100%',
138+
'margin-left': '52px',
139+
});
140+
});
141+
142+
});
143+
144+
});
145+
146+
147+
// *****************************************************************
148+
// Template Component
149+
// *****************************************************************
150+
151+
@Component({
152+
selector: 'test-component-shell',
153+
template: `<span>PlaceHolder Template HTML</span>`
154+
})
155+
export class TestFlexComponent {
156+
public direction = 'column';
157+
}
158+
159+

src/lib/flexbox/api/flex-offset.ts

+61-3
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ import {
1212
OnInit,
1313
OnChanges,
1414
OnDestroy,
15+
Optional,
1516
Renderer,
1617
SimpleChanges,
18+
SkipSelf
1719
} from '@angular/core';
1820

21+
import {Subscription} from 'rxjs/Subscription';
1922

2023
import {BaseFxDirective} from './base';
2124
import {MediaChange} from '../../media-query/media-change';
2225
import {MediaMonitor} from '../../media-query/media-monitor';
23-
26+
import {LayoutDirective} from './layout';
27+
import {isFlowHorizontal} from '../../utils/layout-validator';
2428

2529
/**
2630
* 'flex-offset' flexbox styling directive
@@ -53,8 +57,14 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh
5357
@Input('fxFlexOffset.gt-lg') set offsetGtLg(val) { this._cacheInput('offsetGtLg', val); };
5458

5559
/* tslint:enable */
56-
constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer) {
60+
constructor(monitor: MediaMonitor,
61+
elRef: ElementRef,
62+
renderer: Renderer,
63+
@Optional() @SkipSelf() protected _container: LayoutDirective ) {
5764
super(monitor, elRef, renderer);
65+
66+
67+
this.watchParentFlow();
5868
}
5969

6070
// *********************************************
@@ -70,6 +80,16 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh
7080
}
7181
}
7282

83+
/**
84+
* Cleanup
85+
*/
86+
ngOnDestroy() {
87+
super.ngOnDestroy();
88+
if (this._layoutWatcher) {
89+
this._layoutWatcher.unsubscribe();
90+
}
91+
}
92+
7393
/**
7494
* After the initial onChanges, build an mqActivation object that bridges
7595
* mql change events to onMediaQueryChange handlers
@@ -84,7 +104,43 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh
84104
// Protected methods
85105
// *********************************************
86106

107+
/** The flex-direction of this element's host container. Defaults to 'row'. */
108+
protected _layout = 'row';
109+
110+
/**
111+
* Subscription to the parent flex container's layout changes.
112+
* Stored so we can unsubscribe when this directive is destroyed.
113+
*/
114+
protected _layoutWatcher: Subscription;
115+
116+
/**
117+
* If parent flow-direction changes, then update the margin property
118+
* used to offset
119+
*/
120+
protected watchParentFlow() {
121+
if (this._container) {
122+
// Subscribe to layout immediate parent direction changes (if any)
123+
this._layoutWatcher = this._container.layout$.subscribe((direction) => {
124+
// `direction` === null if parent container does not have a `fxLayout`
125+
this._onLayoutChange(direction);
126+
});
127+
}
128+
}
87129

130+
/**
131+
* Caches the parent container's 'flex-direction' and updates the element's style.
132+
* Used as a handler for layout change events from the parent flex container.
133+
*/
134+
protected _onLayoutChange(direction?: string) {
135+
this._layout = direction || this._layout || 'row';
136+
this._updateWithValue();
137+
}
138+
139+
/**
140+
* Using the current fxFlexOffset value, update the inline CSS
141+
* NOTE: this will assign `margin-left` if the parent flex-direction == 'row',
142+
* otherwise `margin-top` is used for the offset.
143+
*/
88144
protected _updateWithValue(value?: string|number) {
89145
value = value || this._queryInput('offset') || 0;
90146
if (this._mqActivation) {
@@ -101,6 +157,8 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh
101157
offset = offset + '%';
102158
}
103159

104-
return {'margin-left': `${offset}`};
160+
// The flex-direction of this element's flex container. Defaults to 'row'.
161+
let layout = this._getFlowDirection(this.parentElement, true);
162+
return isFlowHorizontal(layout) ? {'margin-left': `${offset}`} : {'margin-top': `${offset}`};
105163
}
106164
}

src/lib/flexbox/api/flex.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {MediaMonitor} from '../../media-query/media-monitor';
2727
import {LayoutDirective} from './layout';
2828
import {LayoutWrapDirective} from './layout-wrap';
2929
import {validateBasis} from '../../utils/basis-validator';
30+
import {isFlowHorizontal} from '../../utils/layout-validator';
3031

3132

3233
/** Built-in aliases for different flex-basis values. */
@@ -237,8 +238,8 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges,
237238
break;
238239
}
239240

240-
let max = (direction === 'row') ? 'max-width' : 'max-height';
241-
let min = (direction === 'row') ? 'min-width' : 'min-height';
241+
let max = isFlowHorizontal(direction) ? 'max-width' : 'max-height';
242+
let min = isFlowHorizontal(direction) ? 'min-width' : 'min-height';
242243

243244
let usingCalc = (String(basis).indexOf('calc') > -1) || (basis == 'auto');
244245
let isPx = String(basis).indexOf('px') > -1 || usingCalc;
@@ -254,7 +255,4 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges,
254255
return extendObject(css, {'box-sizing': 'border-box'});
255256
}
256257

257-
protected get parentElement(): any {
258-
return this._elementRef.nativeElement.parentNode;
259-
}
260258
}

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

+3-4
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ import {MediaChange} from '../../media-query/media-change';
2424
import {MediaMonitor} from '../../media-query/media-monitor';
2525

2626
import {LayoutDirective} from './layout';
27-
import {LAYOUT_VALUES} from '../../utils/layout-validator';
28-
27+
import {LAYOUT_VALUES, isFlowHorizontal} from '../../utils/layout-validator';
2928

3029
/**
3130
* 'layout-align' flexbox styling directive
@@ -204,8 +203,8 @@ export class LayoutAlignDirective extends BaseFxDirective implements OnInit, OnC
204203
// Use `null` values to remove style
205204
this._applyStyleToElement({
206205
'box-sizing': 'border-box',
207-
'max-width': (layout === 'column') ? '100%' : null,
208-
'max-height': (layout === 'row') ? '100%' : null
206+
'max-width': !isFlowHorizontal(layout) ? '100%' : null,
207+
'max-height': isFlowHorizontal(layout) ? '100%' : null
209208
});
210209
}
211210
}

src/lib/utils/layout-validator.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export function buildLayoutCSS(value: string) {
2020
* Validate the value to be one of the acceptable value options
2121
* Use default fallback of 'row'
2222
*/
23-
function validateValue(value: string) {
23+
export function validateValue(value: string) {
2424
value = value ? value.toLowerCase() : '';
2525
let [direction, wrap] = value.split(' ');
2626
if (!LAYOUT_VALUES.find(x => x === direction)) {
@@ -29,6 +29,14 @@ function validateValue(value: string) {
2929
return [direction, validateWrapValue(wrap)];
3030
}
3131

32+
/**
33+
* Determine if the validated, flex-direction value specifies
34+
* a horizontal/row flow.
35+
*/
36+
export function isFlowHorizontal(value: string): boolean {
37+
let [flow, _] = validateValue(value);
38+
return flow.indexOf('row') > -1;
39+
}
3240

3341
/**
3442
* Convert layout-wrap='<value>' to expected flex-wrap style

0 commit comments

Comments
 (0)