question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

feat: add api for resizing flexes

See original GitHub issue

Requirement

I would like the ability to resize a flex given a delta.

Use Case

In the implementation of components like splitters and grids, you can leverage the power of flex layout for width distribution but you often need the ability to resize those areas.

API

On the fxFlex directive something like resizeByDelta(deltaPx: number): return actual change in px and it would automatically measure the existing size, calculate the pixel factor and subtract that from the existing layout.

It should be important to keep this abstract so you can apply different distribution strategies. For example:

Ability to distribute the delta evenly:

const [resizedFlex, ...allFlex] = this.findFlexes();
const newDelta = resizedFlex.resizeByDelta(delta);
const equalDelta = allFlex.length / newDelta;
allFlex.forEach(f => f.resizeByDelta(delta));

You could even pick and choose based on attributes such as canResize in your findFlexes function.

Ability to distribute the width to only the next/previous flex:

const [resizedFlex, nextFlex] = this.findFlexes();
const newDelta = resizedFlex.resizeByDelta(delta);
nextFlex.resizeByDelta(newDelta);

In the process of writing the code, there is also some methods that should be exposed:

  • _queryInput
  • _updateStyle
  • _cacheInput --> Hopefully some API could call this automatically on the update fn.

Code

Here is a directive I have worked up to handle this. You might notice much of this code had to be extracted from the flex core code since it wasn’t exposed in a way that could facilitate re-use here.

import { Directive, Self, Input, ElementRef, EventEmitter, Output, Inject, forwardRef } from '@angular/core';
import { FlexDirective, validateBasis } from '@angular/flex-layout';

@Directive({
  selector: '[flexResize]'
})
export class FlexResizeDirective {

  @Input() minBasis: string;
  @Input() maxBasis: string;
  @Input() fxFlex: string;

  @Output() flexResize = new EventEmitter();

  get fxFlexFill(): boolean {
    return this.fxFlex === '';
  }

  constructor(
    public elementRef: ElementRef,
    @Inject(forwardRef(() => FlexDirective)) public flexDirective: FlexDirective
  ) { }

  /**
   * Get the flex parts from the flex directive.
   */
  getFlexParts() {
    const flex = this.flexDirective as any;
    const basis = flex._queryInput('flex') || '1 1 1e-9px';
    return validateBasis(
        String(basis).replace(';', ''),
        (flex as any)._queryInput('grow'),
        (flex as any)._queryInput('shrink')
      );
  }

  /**
   * Get the flex inputs from the flex directive.
   */
  getInputFlexParts() {
    const flex = this.flexDirective as any;
    const basis = this.fxFlex || '1 1 1e-9px';
    return validateBasis(
        String(basis).replace(';', ''),
        (flex as any)._queryInput('grow'),
        (flex as any)._queryInput('shrink')
      );
  }

  /**
   * Update the style based on a new flex basis.
   */
  updateStyle(flexBasis?: string|number) {
    const flex = this.flexDirective as any;

    if (typeof flexBasis === 'undefined') {
      flexBasis = flex._queryInput('flex') || '';
    }

    if (typeof flexBasis === 'number') {
      flexBasis = this.isPercent() ? `${flexBasis}%` : `${flexBasis}px`;
    }

    const grow = flex._queryInput('grow');
    const shrink = flex._queryInput('shrink');

    if (flexBasis.indexOf(' ') < 0) {
      flexBasis = [grow, shrink, flexBasis].join(' ');
    }

    flex._cacheInput('flex', flexBasis);
    flex._updateStyle(flexBasis);
    this.flexResize.emit(flexBasis);
  }

  /**
   * Determine if the input is a percent or not.
   */
  isPercent(basis?: string): boolean {
    if (!basis) {
      const flex = this.flexDirective as any;
      basis = flex._queryInput('flex') || '1 1 1e-9px';
    }

    const hasCalc = String(basis).indexOf('calc') > -1;
    return String(basis).indexOf('%') > -1 && !hasCalc;
  }

  /**
   * Determines if the basis is a percent or exact.
   */
  isBasisPecent(basis: string): boolean {
    const hasCalc = String(basis).indexOf('calc') > -1;
    return String(basis).indexOf('%') > -1 && !hasCalc;
  }

  /**
   * Converts the basis to a number.
   */
  toValue(basis: string) {
    if (typeof basis === 'string') {
      return parseFloat(basis.replace('%', '').replace('px', ''));
    }
    return basis;
  }

  /**
   * Get the min/max percent of the basis.
   */
  getMinMaxPct(minBasis, maxBasis, grow, shrink, baseBasisPct, basisToPx) {
    // minimum and maximum basis determined by max/min inputs
    let minBasisPct = this.toValue(minBasis) / (this.isBasisPecent(minBasis) ? 1 : basisToPx);
    let maxBasisPct = this.toValue(maxBasis) / (this.isBasisPecent(maxBasis) ? 1 : basisToPx);

    // minimum and maximum basis determined by flex inputs
    minBasisPct = Math.max(minBasisPct || 0, shrink === '0' ? baseBasisPct : 0);
    maxBasisPct = Math.min(maxBasisPct || 100, grow === '0' ? baseBasisPct : 100);

    return [minBasisPct, maxBasisPct];
  }

  /**
   * Resize the split areq by basis pixels.
   */
  resizeAreaBy(delta: number, basisToPx: number) {
    const flex = this.flexDirective as FlexDirective;

    if (this.fxFlexFill) {
      // area is fxFlexFill, distribute delta right
      return delta;
    }

    const [grow, shrink, basis] = this.getFlexParts();
    const isPercent = this.isBasisPecent(basis);
    const basisValue = this.toValue(basis);

    // get baseBasis in percent
    const baseBasis = this.getInputFlexParts()[2];
    const baseBasisPct = this.toValue(baseBasis) / (this.isBasisPecent(baseBasis) ? basisToPx : 1);

    // get basis in px and %
    const basisPx = isPercent ? basisValue * basisToPx : basisValue;
    const basisPct = basisPx / basisToPx;

    // determine which dir and calc the diff
    let newBasisPx = basisPx + delta;
    let newBasisPct = newBasisPx / basisToPx;

    const [minBasisPct, maxBasisPct] =
      this.getMinMaxPct(this.minBasis, this.maxBasis, grow, shrink, baseBasisPct, basisToPx);

    // obey max and min
    newBasisPct = Math.max(newBasisPct, minBasisPct);
    newBasisPct = Math.min(newBasisPct, maxBasisPct);

    // calculate new basis on px
    newBasisPx = newBasisPct * basisToPx;

    // update flexlayout
    this.updateStyle(isPercent ? newBasisPct : newBasisPx);

    // return actual change in px
    return newBasisPx - basisPx;
  }

}

Example

Here is a example Stackblitz using a earlier version of the above code https://virtual-grid-jguqpn.stackblitz.io/. In this use case, you can see the grid resize function listens for the resize event and calls this resize directive like:

import { Directive, AfterContentInit, ContentChildren, QueryList, ElementRef } from '@angular/core';
import { GridResizeHandleComponent } from './grid-resize-handle.component';
import { GridHeaderCellComponent } from '../cell/grid-header-cell.component';

@Directive({
  selector: '[dfGridResize]'
})
export class GridResizeDirective implements AfterContentInit {

  @ContentChildren(GridHeaderCellComponent) cells: QueryList<GridHeaderCellComponent>;

  private _previousX: number;

  constructor(private _elementRef: ElementRef) {}

  ngAfterContentInit() {
    this.cells.forEach((cell: GridHeaderCellComponent) =>
      cell.resizeHandle.resizing.subscribe(ev => this.onResize(ev, cell)));
  }

  /**
   * A resize handle resized was called. This function will:
   * - Calculate the delta to resize the current column
   * - Determine the delta to distribute to the other columns
   * - Determine the direction to add/subtract the other columns
   */
  onResize(event: MouseEvent, cell: GridHeaderCellComponent) {
    const direction = this.determineDirection(event);
    if (direction) {
      const basisToPx = this._elementRef.nativeElement.clientWidth / 100;
      const areas = this.findColumnsToResize(cell);
      let delta = event.movementX;

      const [first, next] = areas;
      if (next) {
        delta = first.flexResizeDirective.resizeAreaBy(delta, basisToPx);

        let dividedDelta = Math.abs(delta);
        if (direction === 'right') {
          dividedDelta = dividedDelta * -1;
        }

        next.flexResizeDirective.resizeAreaBy(dividedDelta, basisToPx);
      }
    }
  }

  /**
   * Determine the direction of the drag.
   */
  determineDirection(event): 'left' | 'right' {
    let direction;

    if (event.pageX < this._previousX) {
      direction = 'left';
    } else if (event.pageX > this._previousX) {
      direction = 'right';
    }

    this._previousX = event.pageX;

    return direction;
  }

  /**
   * Find the columns we are going to resize.
   */
  findColumnsToResize(cell: GridHeaderCellComponent): GridHeaderCellComponent[] {
    const copies = this.cells.toArray();
    const areas = this.cells.toArray();
    let match = false;

    for (const copy of copies) {
      if (!match && copy !== cell) {
        const index = areas.indexOf(copy);
        areas.splice(index, 1);
        continue;
      }

      if (copy === cell) {
        match = true;
      } else if (!cell.cellTemplate.resizable) {
        const index = areas.indexOf(copy);
        areas.splice(index, 1);
      }
    }

    return areas;
  }

}

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:8 (7 by maintainers)

github_iconTop GitHub Comments

1reaction
amcdnlcommented, May 18, 2018

Use case considerations:

  • User opens page with grid on device width of 3k px
  • User resizes one of the columns from 30% to around 50% ( lets say this is 180px)
  • The size is stored in localstorage
  • User revisits with screensize of 1.5k px

The delta would need to be relative so that the 180px delta responds nicely on the new screen width. Also if the user were to resize that column to make it slightly smaller on the screen size it wouldn’t clobber the existing delta.

Overall I think that solution will work, just things to consider.

1reaction
CaerusKarucommented, May 18, 2018

Ok then let’s discuss more offline, maybe over Slack. And maybe we should add credit for you on that one 😄

Read more comments on GitHub >

github_iconTop Results From Across the Web

A Complete Guide to Flexbox | CSS-Tricks
This complete guide explains everything about flexbox, focusing on all the different possible properties for the parent element (the flex ...
Read more >
Processing API - Filestack Docs
The resizing feature comprises two main functions: manipulating the width and height of an image and changing the fit and alignment of the...
Read more >
Resize - ApacheFlex API Reference
The Resize effect changes the width, height, or both dimensions of a component over a specified time interval. If you specify only two...
Read more >
react-rnd - npm
Specifies the scale of the canvas you are dragging or resizing this element on. This allows you to, for example, get the correct...
Read more >
Can I use... Support tables for HTML5, CSS3, etc
"Can I use" provides up-to-date browser support tables for support of front-end web technologies on desktop and mobile web browsers.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found