feat: add api for resizing flexes
See original GitHub issueRequirement
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:
- Created 5 years ago
- Comments:8 (7 by maintainers)
Top GitHub Comments
Use case considerations:
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.
Ok then let’s discuss more offline, maybe over Slack. And maybe we should add credit for you on that one 😄