Flex-layout: Expose `fxFlex` directive class to access via ContentChildren

Created on 25 Apr 2017  路  12Comments  路  Source: angular/flex-layout

Requirement

I need to access fxFlex via ContentChildren from a adjacent directive to dynamically modify widths.

Use Case

I want to create a responsive split component that leverages flex-layout for the layout.

use

Code

Usage

<div fxLayout="row" ngxSplit="row">
  <div fxFlex="30%" ngxSplitArea>
    Left
  </div>
  <div fxFlex="5px">
    <a href="#" ngxSplitHandle>...</a>
  </div>
  <div fxFlex="70%" ngxSplitArea>
    Right
  </div>
</div>

split.directive.ts

import { 
  Directive, Input, ChangeDetectionStrategy, ContentChild, 
  ContentChildren, AfterContentInit, QueryList
} from '@angular/core';
import { SplitAreaDirective } from './split-area.directive';
import { SplitHandleDirective } from './split-handle.directive';

@Directive({
  selector: '[ngxSplit]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'ngx-split'
  }
})
export class SplitDirective implements AfterContentInit {

  /*tslint:disable*/
  @Input('ngxSplit') 
  direction: string = 'vertial';
  /*tslint:enable*/

  @ContentChild(SplitHandleDirective) handle: SplitHandleDirective;
  @ContentChildren(SplitAreaDirective) areas: QueryList<SplitAreaDirective>;

  ngAfterContentInit(): void {
    console.log('ha', this.handle, this.areas); //<-- Need it here
    this.handle.drag.subscribe(pos => this.onDrag(pos));
  }

  onDrag({ x, y }): void {
    this.areas.forEach(area => {
      console.log('area', area); // TODO: Resize here
    });
  }

}

split-area.directive.ts

import { Directive, Input, ChangeDetectionStrategy, ContentChildren, QueryList } from '@angular/core';

@Directive({
  selector: '[ngxSplitArea]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'ngx-split-area'
  }
})
export class SplitAreaDirective {

  @ContentChildren('[fxFlex]') layouts: QueryList<any>;

}

split-handle.directive.ts

import { Directive, ElementRef, Output, ChangeDetectionStrategy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/switchMap';

@Directive({
  selector: '[ngxSplitHandle]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'ngx-split-handle'
  }
})
export class SplitHandleDirective {

  @Output() drag: Observable<{ x: number, y: number }>;

  constructor(ref: ElementRef) {
    const getMouseEventPosition = (event: MouseEvent) => ({ x: event.clientX, y: event.clientY });

    const mousedown$ = Observable.fromEvent(ref.nativeElement, 'mousedown').map(getMouseEventPosition);
    const mousemove$ = Observable.fromEvent(document, 'mousemove').map(getMouseEventPosition);
    const mouseup$ = Observable.fromEvent(document, 'mouseup');

    this.drag = mousedown$.switchMap(mousedown =>
        mousemove$.map(mousemove => ({
          x: mousemove.x - mousedown.x,
          y: mousemove.y - mousedown.y
        }))
        .takeUntil(mouseup$)
      );
  }

}
enhancement has pr

Most helpful comment

Noticed some odd artifacts in the demo for #266:

  • dragging the splitter icon by mouse does not correlate to the cursor's actual position, it seems to be offset by a few pixels.
  • an extraneous outer scrollbar around the two panes in the second column allows scrolling to the right which reveals dark, empty space.

All 12 comments

@amcdnl - very cool feature and much-needed for layout adjustments. 馃憤

This idea (and your sample) presents several challenges. Your fxFlex usages are

  • statically bound to a specific value
  • not responsive: no mediaQuery suffices used. (this is reduces complexity here)

Issue #1: Manually updating fxFlex values

When you drag, you will want to update the fxFlex value; so the value is managed and styles are applied. If you set the @Input property <flex directive instance>::fxFlex(val), you may need to manually trigger change detection for the value to be applied.

This may be an issue...

Issue #2: ngxSplitArea

  • The ngxSplitArea needs access to its fxFlex directive on the same host element.

    • Use @Optional() @Self() protected _flex: FlexDirective in the constructor to inject

  • The ngxSplitArea needs access to its content children fxFlex instances...

    • @ContentChildren(FlexDirective) fxFlexChildren: QueryList<FlexDirective>;

    • Why is this needed ?

DragHandle

Your drag handle may need to be positioned absolute for you to freely drag...

@ThomasBurleson Thanks for the feedback!

I tried to simplify the demo as much as possible, is there not a way to update the values internally that I defined statically?

I don't see FlexDirective being exported from flex-layout. I can import it from import { FlexDirective } from '@angular/flex-layout/flexbox/api/flex'; though, I'm not sure if that what you were referring to.

@amcdnl - So you just found a problem. FlexDirective (nor any other directive) is NOT exported for use in scenarios like above. I will fix that asap!

Meanwhile, a work around to the static values is to use databindings:

<div fxLayout="row" ngxSplit="row">
  <div [fxFlex]="columnWidthLeft" ngxSplitArea>
    Left
  </div>
  <div fxFlex="5px">
    <a href="#" ngxSplitHandle>...</a>
  </div>
  <div [fxFlex]="columnWidthRight" ngxSplitArea>
    Right
  </div>
</div>

Then - as the drag handle moves - you could update the columnWidth<xxx> properties.

Note: you should assign string values like 32px, etc. If you assign only the value, flex-layout assumes percentages.

@amcdnl - Another trick is too NOT specify the width of one of your columns. Let it expand to fill the available horizontal space:

```html
<div fxLayout="row" ngxSplit="row">
  <div [fxFlex]="columnWidth" ngxSplitArea>
    Left
  </div>
  <div fxFlex="5px">
    <a href="#" ngxSplitHandle>...</a>
  </div>
  <div fxFlex ngxSplitArea>
    Right
  </div>
</div>

Heres what I ended up with, I'd love to hear what you think of the API usage w/ flex as its kinda hacky ATM but most important it WORKS!

split.directive.ts

import { SplitAreaDirective } from './split-area.directive';
import { SplitHandleDirective } from './split-handle.directive';

@Directive({
  selector: '[ngxSplit]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'ngx-split'
  }
})
export class SplitDirective implements AfterContentInit {

  /*tslint:disable*/
  @Input('ngxSplit') 
  direction: string = 'row';
  /*tslint:enable*/

  @ContentChild(SplitHandleDirective) handle: SplitHandleDirective;
  @ContentChildren(SplitAreaDirective) areas: QueryList<SplitAreaDirective>;

  constructor(private elementRef: ElementRef) { }

  ngAfterContentInit(): void {
    this.handle.drag.subscribe(pos => this.onDrag(pos));
  }

  onDrag({ x, y }): void {
    const parentWidth = this.elementRef.nativeElement.clientWidth;
    const delta = this.direction === 'row' ? x : y;

    this.areas.forEach((area, i) => {
      // get the cur flex
      const flex = (area.flex as any);
      const flexPerc = flex._inputMap.flex;

      // get the % in px
      const areaCur = parseFloat(flexPerc);
      const areaPx = parentWidth * (areaCur / 100);

      // determine which dir and calc the diff
      let areaDiff;
      if(i === 0) {
        areaDiff = areaPx + delta;
      } else {
        areaDiff = areaPx - delta;
      }

      // convert the px to %
      let newAreaPx = (areaDiff / parentWidth) * 100;
      newAreaPx = Math.max(newAreaPx, 0);
      newAreaPx = Math.min(newAreaPx, 100);

      // update flexlayout
      flex._inputMap.flex = newAreaPx + '%';
      flex._updateStyle();
    });
  }

}

split-handle.directive.ts

import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/takeUntil';
import 'rxjs/add/operator/switchMap';

@Directive({
  selector: '[ngxSplitHandle]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'ngx-split-handle'
  }
})
export class SplitHandleDirective {

  @Output() drag: Observable<{ x: number, y: number }>;

  constructor(ref: ElementRef) {
    const getMouseEventPosition = (event: MouseEvent) => ({ x: event.movementX, y: event.movementY });

    const mousedown$ = Observable.fromEvent(ref.nativeElement, 'mousedown').map(getMouseEventPosition);
    const mousemove$ = Observable.fromEvent(document, 'mousemove').map(getMouseEventPosition);
    const mouseup$ = Observable.fromEvent(document, 'mouseup');

    this.drag = mousedown$
      .switchMap(mousedown =>
        mousemove$.map(mousemove => ({
          x: mousemove.x,
          y: mousemove.y
        }))
        .takeUntil(mouseup$)
      );
  }

}

split-area.directive.ts

import { FlexDirective } from '@angular/flex-layout/flexbox/api/flex';

@Directive({
  selector: '[ngxSplitArea]',
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    class: 'ngx-split-area'
  }
})
export class SplitAreaDirective {
  constructor(@Optional() @Self() public flex: FlexDirective) { }
}

Usage

<div fxLayout="row" ngxSplit="row">
  <div fxFlex="30%" ngxSplitArea>
    Left
  </div>
  <div fxFlex="15px" class="handle-area">
    <button class="handle-row" ngxSplitHandle>...</button>
  </div>
  <div fxFlex="70%" ngxSplitArea>
    <div fxLayout="column" fxFlexFill ngxSplit="column">
      <div fxFlex="50%" ngxSplitArea>
        Top
      </div>
      <div fxFlex="15px" class="handle-area">
        <button class="handle-column" ngxSplitHandle>...</button>
      </div>
      <div fxFlex="50%" ngxSplitArea>
        Bottom
      </div>
    </div>
  </div>
</div>

In particular the portion here:

flex._inputMap.flex = newAreaPx + '%';
flex._updateStyle();

is where we can probably come up with something better.

Here is a demo of column and row splitters - ngx-ui

Yes. This is a hack indeed. But a nice one.
Let's see if we can fix the library so you do not need to hacketize it!

@amcdnl thx you saved me a lot of time, had today the same idea cause fxFlex is very useful and no split component i've found is using it :)

at the moment this is enough for me, but i think i'll update it a bit so we can work with "." values too like "fxFlex.xs".

This is what i need at the end:
-Desktop- resizable row
|__|__||

-Tablet- resizable column
|__|
|__|

-Mobile- resize off
|__|
|__|

However, nice done :+1:

@amcdnl - After some thought, I think the following new method is best:

flexInstance.setActivatedValue( newAreaPx );

This internally identifies the current activated input key, sets the value and immediately calls updateStyle().

If the value does not have a px suffix, then a percentage value will be presumed.

Note that this does not set the value for all inputs (or other activated breakpoints).

@ThomasBurleson - What about a api to get the current flex property?

This change is not working in IE11, can you please suggest the changes for making it working in IE11 and other IE browsers

Noticed some odd artifacts in the demo for #266:

  • dragging the splitter icon by mouse does not correlate to the cursor's actual position, it seems to be offset by a few pixels.
  • an extraneous outer scrollbar around the two panes in the second column allows scrolling to the right which reveals dark, empty space.

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings