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

Commit

Permalink
feat(core): add memoization to style generation (#888)
Browse files Browse the repository at this point in the history
Enable directives to memoize generated inline-style values.
  • Loading branch information
CaerusKaru authored and ThomasBurleson committed Nov 29, 2018
1 parent 220344d commit 4600672
Show file tree
Hide file tree
Showing 15 changed files with 219 additions and 118 deletions.
20 changes: 18 additions & 2 deletions src/lib/core/base/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,22 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
return this._elementRef.nativeElement;
}

/** Add styles to the element using predefined style builder */
protected addStyles(input: string, parent?: Object) {
const styles: StyleDefinition = this._styleBuilder!.buildStyles(input, parent);
this._applyStyleToElement(styles);
const builder = this._styleBuilder!;
const useCache = builder.shouldCache;

let genStyles: StyleDefinition | undefined = this._styleCache.get(input);

if (!genStyles || !useCache) {
genStyles = builder.buildStyles(input, parent);
if (useCache) {
this._styleCache.set(input, genStyles);
}
}

this._applyStyleToElement(genStyles);
builder.sideEffect(input, genStyles, parent);
}

/** Access the current value (if any) of the @Input property */
Expand Down Expand Up @@ -246,4 +259,7 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
* getComputedStyle() during ngOnInit().
*/
protected _hasInitialized = false;

/** Cache map for style computation */
protected _styleCache: Map<string, StyleDefinition> = new Map();
}
16 changes: 14 additions & 2 deletions src/lib/core/style-builder/style-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Injectable} from '@angular/core';
import {StyleDefinition} from '../style-utils/style-utils';

@Injectable()
/** A class that encapsulates CSS style generation for common directives */
export abstract class StyleBuilder {

/** Whether to cache the generated output styles */
shouldCache = true;

/** Build the styles given an input string and configuration object from a host */
abstract buildStyles(input: string, parent?: Object): StyleDefinition;

/**
* Run a side effect computation given the input string and the computed styles
* from the build task and the host configuration object
* NOTE: This should be a no-op unless an algorithm is provided in a subclass
*/
sideEffect(_input: string, _styles: StyleDefinition, _parent?: Object) {
}
}
20 changes: 12 additions & 8 deletions src/lib/flex/flex-align/flex-align.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,28 @@ import {
MediaMonitor,
StyleBuilder,
StyleDefinition,
StyleUtils
StyleUtils,
} from '@angular/flex-layout/core';

@Injectable({providedIn: 'root'})
export class FlexAlignStyleBuilder implements StyleBuilder {
buildStyles(input: string): StyleDefinition {
const css: {[key: string]: string | number} = {};
export class FlexAlignStyleBuilder extends StyleBuilder {
buildStyles(input: string) {
const styles: StyleDefinition = {};

// Cross-axis
switch (input) {
case 'start':
css['align-self'] = 'flex-start';
styles['align-self'] = 'flex-start';
break;
case 'end':
css['align-self'] = 'flex-end';
styles['align-self'] = 'flex-end';
break;
default:
css['align-self'] = input;
styles['align-self'] = input;
break;
}

return css;
return styles;
}
}

Expand Down Expand Up @@ -125,4 +125,8 @@ export class FlexAlignDirective extends BaseDirective implements OnInit, OnChang

this.addStyles(value && (value + '') || '');
}

protected _styleCache = flexAlignCache;
}

const flexAlignCache: Map<string, StyleDefinition> = new Map();
8 changes: 6 additions & 2 deletions src/lib/flex/flex-fill/flex-fill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const FLEX_FILL_CSS = {
};

@Injectable({providedIn: 'root'})
export class FlexFillStyleBuilder implements StyleBuilder {
buildStyles(_input: string): StyleDefinition {
export class FlexFillStyleBuilder extends StyleBuilder {
buildStyles(_input: string) {
return FLEX_FILL_CSS;
}
}
Expand All @@ -47,4 +47,8 @@ export class FlexFillDirective extends BaseDirective {
super(monitor, elRef, styleUtils, styleBuilder);
this.addStyles('');
}

protected _styleCache = flexFillCache;
}

const flexFillCache: Map<string, StyleDefinition> = new Map();
2 changes: 1 addition & 1 deletion src/lib/flex/flex-offset/flex-offset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ describe('flex-offset directive', () => {
});

@Injectable({providedIn: FlexModule})
export class MockFlexOffsetStyleBuilder implements StyleBuilder {
export class MockFlexOffsetStyleBuilder extends StyleBuilder {
buildStyles(_input: string) {
return {'margin-top': '10px'};
}
Expand Down
25 changes: 20 additions & 5 deletions src/lib/flex/flex-offset/flex-offset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,24 @@ import {Subscription} from 'rxjs';
import {Layout, LayoutDirective} from '../layout/layout';
import {isFlowHorizontal} from '../../utils/layout-validator';

interface FlexOffsetParent {
export interface FlexOffsetParent {
layout: string;
isRtl: boolean;
}

@Injectable({providedIn: 'root'})
export class FlexOffsetStyleBuilder implements StyleBuilder {
buildStyles(offset: string, parent: FlexOffsetParent): StyleDefinition {
export class FlexOffsetStyleBuilder extends StyleBuilder {
buildStyles(offset: string, parent: FlexOffsetParent) {
const isPercent = String(offset).indexOf('%') > -1;
const isPx = String(offset).indexOf('px') > -1;
if (!isPx && !isPercent && !isNaN(+offset)) {
offset = offset + '%';
}
const horizontalLayoutKey = parent.isRtl ? 'margin-right' : 'margin-left';

return isFlowHorizontal(parent.layout) ? {[horizontalLayoutKey]: `${offset}`} :
const styles = isFlowHorizontal(parent.layout) ? {[horizontalLayoutKey]: `${offset}`} :
{'margin-top': `${offset}`};

return styles;
}
}

Expand Down Expand Up @@ -185,6 +186,20 @@ export class FlexOffsetDirective extends BaseDirective implements OnInit, OnChan
// The flex-direction of this element's flex container. Defaults to 'row'.
const layout = this._getFlexFlowDirection(this.parentElement, true);
const isRtl = this._directionality.value === 'rtl';
if (layout === 'row' && isRtl) {
this._styleCache = flexOffsetCacheRowRtl;
} else if (layout === 'row' && !isRtl) {
this._styleCache = flexOffsetCacheRowLtr;
} else if (layout === 'column' && isRtl) {
this._styleCache = flexOffsetCacheColumnRtl;
} else if (layout === 'column' && !isRtl) {
this._styleCache = flexOffsetCacheColumnLtr;
}
this.addStyles((value && (value + '') || ''), {layout, isRtl});
}
}

const flexOffsetCacheRowRtl: Map<string, StyleDefinition> = new Map();
const flexOffsetCacheColumnRtl: Map<string, StyleDefinition> = new Map();
const flexOffsetCacheRowLtr: Map<string, StyleDefinition> = new Map();
const flexOffsetCacheColumnLtr: Map<string, StyleDefinition> = new Map();
13 changes: 9 additions & 4 deletions src/lib/flex/flex-order/flex-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import {
MediaMonitor,
StyleBuilder,
StyleDefinition,
StyleUtils
StyleUtils,
} from '@angular/flex-layout/core';

@Injectable({providedIn: 'root'})
export class FlexOrderStyleBuilder implements StyleBuilder {
buildStyles(value: string): StyleDefinition {
export class FlexOrderStyleBuilder extends StyleBuilder {
buildStyles(value: string) {
const val = parseInt(value, 10);
return {order: isNaN(val) ? 0 : val};
const styles = {order: isNaN(val) ? 0 : val};
return styles;
}
}

Expand Down Expand Up @@ -108,4 +109,8 @@ export class FlexOrderDirective extends BaseDirective implements OnInit, OnChang

this.addStyles(value || '');
}

protected _styleCache = flexOrderCache;
}

const flexOrderCache: Map<string, StyleDefinition> = new Map();
11 changes: 8 additions & 3 deletions src/lib/flex/flex/flex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,6 +858,7 @@ describe('flex directive', () => {
});

describe('with column basis zero disabled', () => {
let styleBuilder: FlexStyleBuilder;
beforeEach(() => {
jasmine.addMatchers(customMatchers);

Expand All @@ -875,16 +876,20 @@ describe('flex directive', () => {
});
});

it('should set flex basis to auto', async(() => {
it('should set flex basis to auto', () => {
componentWithTemplate(`
<div fxLayout='column'>
<div fxFlex></div>
</div>
`);
styleBuilder = TestBed.get(FlexStyleBuilder);

// Reset the cache because the layout config is only set at startup
styleBuilder.shouldCache = false;
fixture.detectChanges();
let element = queryFor(fixture, '[fxFlex]')[0];
expectEl(element).toHaveStyle({'flex': '1 1 auto'}, styler);
}));
});
});

describe('with custom builder', () => {
Expand Down Expand Up @@ -926,7 +931,7 @@ describe('flex directive', () => {
});

@Injectable({providedIn: FlexModule})
export class MockFlexStyleBuilder implements StyleBuilder {
export class MockFlexStyleBuilder extends StyleBuilder {
buildStyles(_input: string) {
return {'flex': '1 1 30%'};
}
Expand Down
39 changes: 26 additions & 13 deletions src/lib/flex/flex/flex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,16 @@ export type FlexBasisAlias = 'grow' | 'initial' | 'auto' | 'none' | 'nogrow' | '
interface FlexBuilderParent {
direction: string;
hasWrap: boolean;
useColumnBasisZero: boolean | undefined;
}

@Injectable({providedIn: 'root'})
export class FlexStyleBuilder implements StyleBuilder {
buildStyles(input: string, parent: FlexBuilderParent): StyleDefinition {
let grow: string | number;
let shrink: string | number;
let basis: string | number;
[grow, shrink, basis] = input.split('_');
export class FlexStyleBuilder extends StyleBuilder {
constructor(@Inject(LAYOUT_CONFIG) protected layoutConfig: LayoutConfigOptions) {
super();
}
buildStyles(input: string, parent: FlexBuilderParent) {
let [grow, shrink, ...basisParts]: (string|number)[] = input.split(' ');
let basis = basisParts.join(' ');

// The flex-direction of this element's flex container. Defaults to 'row'.
const direction = (parent.direction.indexOf('column') > -1) ? 'column' : 'row';
Expand Down Expand Up @@ -97,7 +97,7 @@ export class FlexStyleBuilder implements StyleBuilder {
};
switch (basis || '') {
case '':
const useColumnBasisZero = parent.useColumnBasisZero !== false;
const useColumnBasisZero = this.layoutConfig.useColumnBasisZero !== false;
basis = direction === 'row' ? '0%' : (useColumnBasisZero ? '0.000000001px' : 'auto');
break;
case 'initial': // default
Expand Down Expand Up @@ -192,7 +192,7 @@ export class FlexStyleBuilder implements StyleBuilder {
}
}

return extendObject(css, {'box-sizing': 'border-box'});
return extendObject(css, {'box-sizing': 'border-box'}) as StyleDefinition;
}
}

Expand Down Expand Up @@ -310,12 +310,25 @@ export class FlexDirective extends BaseDirective implements OnInit, OnChanges, O
flexBasis = this._mqActivation.activatedInput;
}

let basis = String(flexBasis).replace(';', '');
let parts = validateBasis(basis, this._queryInput('grow'), this._queryInput('shrink'));
const basis = String(flexBasis).replace(';', '');
const parts = validateBasis(basis, this._queryInput('grow'), this._queryInput('shrink'));
const addFlexToParent = this.layoutConfig.addFlexToParent !== false;
const direction = this._getFlexFlowDirection(this.parentElement, addFlexToParent);
const hasWrap = this._layout && this._layout.wrap;
const useColumnBasisZero = this.layoutConfig.useColumnBasisZero;
this.addStyles(parts.join('_'), {direction, hasWrap, useColumnBasisZero});
if (direction === 'row' && hasWrap) {
this._styleCache = flexRowWrapCache;
} else if (direction === 'row' && !hasWrap) {
this._styleCache = flexRowCache;
} else if (direction === 'column' && hasWrap) {
this._styleCache = flexColumnWrapCache;
} else if (direction === 'column' && !hasWrap) {
this._styleCache = flexColumnCache;
}
this.addStyles(parts.join(' '), {direction, hasWrap});
}
}

const flexRowCache: Map<string, StyleDefinition> = new Map();
const flexColumnCache: Map<string, StyleDefinition> = new Map();
const flexRowWrapCache: Map<string, StyleDefinition> = new Map();
const flexColumnWrapCache: Map<string, StyleDefinition> = new Map();
3 changes: 2 additions & 1 deletion src/lib/flex/layout-align/layout-align.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,8 @@ describe('layout-align directive', () => {
});

@Injectable({providedIn: FlexModule})
export class MockLayoutAlignStyleBuilder implements StyleBuilder {
export class MockLayoutAlignStyleBuilder extends StyleBuilder {
shouldCache = false;
buildStyles(_input: string) {
return {'justify-content': 'flex-end'};
}
Expand Down
18 changes: 11 additions & 7 deletions src/lib/flex/layout-align/layout-align.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,22 @@ import {
MediaMonitor,
StyleBuilder,
StyleDefinition,
StyleUtils
StyleUtils,
} from '@angular/flex-layout/core';
import {Subscription} from 'rxjs';

import {extendObject} from '../../utils/object-extend';
import {Layout, LayoutDirective} from '../layout/layout';
import {LAYOUT_VALUES, isFlowHorizontal} from '../../utils/layout-validator';

interface LayoutAlignParent {
export interface LayoutAlignParent {
layout: string;
}

@Injectable({providedIn: 'root'})
export class LayoutAlignStyleBuilder implements StyleBuilder {
buildStyles(align: string, parent: LayoutAlignParent): StyleDefinition {
let css: {[key: string]: string} = {},
[mainAxis, crossAxis] = align.split(' ');
export class LayoutAlignStyleBuilder extends StyleBuilder {
buildStyles(align: string, parent: LayoutAlignParent) {
const css: StyleDefinition = {}, [mainAxis, crossAxis] = align.split(' ');

// Main axis
switch (mainAxis) {
Expand Down Expand Up @@ -105,7 +104,7 @@ export class LayoutAlignStyleBuilder implements StyleBuilder {
!isFlowHorizontal(parent.layout) ? '100%' : null : null,
'max-height': crossAxis === 'stretch' ?
isFlowHorizontal(parent.layout) ? '100%' : null : null,
});
}) as StyleDefinition;
}
}

Expand Down Expand Up @@ -204,6 +203,8 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha
}

const layout = this._layout || 'row';
this._styleCache = layout === 'row' ?
layoutAlignHorizontalCache : layoutAlignVerticalCache;
this.addStyles(value || '', {layout});
}

Expand All @@ -223,3 +224,6 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha
this.addStyles(value, {layout: this._layout || 'row'});
}
}

const layoutAlignHorizontalCache: Map<string, StyleDefinition> = new Map();
const layoutAlignVerticalCache: Map<string, StyleDefinition> = new Map();
2 changes: 1 addition & 1 deletion src/lib/flex/layout-gap/layout-gap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ describe('layout-gap directive', () => {
});

@Injectable({providedIn: FlexModule})
export class MockLayoutGapStyleBuilder implements StyleBuilder {
export class MockLayoutGapStyleBuilder extends StyleBuilder {
buildStyles(_input: string) {
return {'margin-top': '12px'};
}
Expand Down
Loading

0 comments on commit 4600672

Please sign in to comment.