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

Commit 4600672

Browse files
CaerusKaruThomasBurleson
authored andcommitted
feat(core): add memoization to style generation (#888)
Enable directives to memoize generated inline-style values.
1 parent 220344d commit 4600672

File tree

15 files changed

+219
-118
lines changed

15 files changed

+219
-118
lines changed

src/lib/core/base/base.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,22 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
109109
return this._elementRef.nativeElement;
110110
}
111111

112+
/** Add styles to the element using predefined style builder */
112113
protected addStyles(input: string, parent?: Object) {
113-
const styles: StyleDefinition = this._styleBuilder!.buildStyles(input, parent);
114-
this._applyStyleToElement(styles);
114+
const builder = this._styleBuilder!;
115+
const useCache = builder.shouldCache;
116+
117+
let genStyles: StyleDefinition | undefined = this._styleCache.get(input);
118+
119+
if (!genStyles || !useCache) {
120+
genStyles = builder.buildStyles(input, parent);
121+
if (useCache) {
122+
this._styleCache.set(input, genStyles);
123+
}
124+
}
125+
126+
this._applyStyleToElement(genStyles);
127+
builder.sideEffect(input, genStyles, parent);
115128
}
116129

117130
/** Access the current value (if any) of the @Input property */
@@ -246,4 +259,7 @@ export abstract class BaseDirective implements OnDestroy, OnChanges {
246259
* getComputedStyle() during ngOnInit().
247260
*/
248261
protected _hasInitialized = false;
262+
263+
/** Cache map for style computation */
264+
protected _styleCache: Map<string, StyleDefinition> = new Map();
249265
}

src/lib/core/style-builder/style-builder.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,22 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {Injectable} from '@angular/core';
98
import {StyleDefinition} from '../style-utils/style-utils';
109

11-
@Injectable()
10+
/** A class that encapsulates CSS style generation for common directives */
1211
export abstract class StyleBuilder {
12+
13+
/** Whether to cache the generated output styles */
14+
shouldCache = true;
15+
16+
/** Build the styles given an input string and configuration object from a host */
1317
abstract buildStyles(input: string, parent?: Object): StyleDefinition;
18+
19+
/**
20+
* Run a side effect computation given the input string and the computed styles
21+
* from the build task and the host configuration object
22+
* NOTE: This should be a no-op unless an algorithm is provided in a subclass
23+
*/
24+
sideEffect(_input: string, _styles: StyleDefinition, _parent?: Object) {
25+
}
1426
}

src/lib/flex/flex-align/flex-align.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -21,28 +21,28 @@ import {
2121
MediaMonitor,
2222
StyleBuilder,
2323
StyleDefinition,
24-
StyleUtils
24+
StyleUtils,
2525
} from '@angular/flex-layout/core';
2626

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

3232
// Cross-axis
3333
switch (input) {
3434
case 'start':
35-
css['align-self'] = 'flex-start';
35+
styles['align-self'] = 'flex-start';
3636
break;
3737
case 'end':
38-
css['align-self'] = 'flex-end';
38+
styles['align-self'] = 'flex-end';
3939
break;
4040
default:
41-
css['align-self'] = input;
41+
styles['align-self'] = input;
4242
break;
4343
}
4444

45-
return css;
45+
return styles;
4646
}
4747
}
4848

@@ -125,4 +125,8 @@ export class FlexAlignDirective extends BaseDirective implements OnInit, OnChang
125125

126126
this.addStyles(value && (value + '') || '');
127127
}
128+
129+
protected _styleCache = flexAlignCache;
128130
}
131+
132+
const flexAlignCache: Map<string, StyleDefinition> = new Map();

src/lib/flex/flex-fill/flex-fill.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ const FLEX_FILL_CSS = {
2323
};
2424

2525
@Injectable({providedIn: 'root'})
26-
export class FlexFillStyleBuilder implements StyleBuilder {
27-
buildStyles(_input: string): StyleDefinition {
26+
export class FlexFillStyleBuilder extends StyleBuilder {
27+
buildStyles(_input: string) {
2828
return FLEX_FILL_CSS;
2929
}
3030
}
@@ -47,4 +47,8 @@ export class FlexFillDirective extends BaseDirective {
4747
super(monitor, elRef, styleUtils, styleBuilder);
4848
this.addStyles('');
4949
}
50+
51+
protected _styleCache = flexFillCache;
5052
}
53+
54+
const flexFillCache: Map<string, StyleDefinition> = new Map();

src/lib/flex/flex-offset/flex-offset.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ describe('flex-offset directive', () => {
223223
});
224224

225225
@Injectable({providedIn: FlexModule})
226-
export class MockFlexOffsetStyleBuilder implements StyleBuilder {
226+
export class MockFlexOffsetStyleBuilder extends StyleBuilder {
227227
buildStyles(_input: string) {
228228
return {'margin-top': '10px'};
229229
}

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

+20-5
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,24 @@ import {Subscription} from 'rxjs';
3131
import {Layout, LayoutDirective} from '../layout/layout';
3232
import {isFlowHorizontal} from '../../utils/layout-validator';
3333

34-
interface FlexOffsetParent {
34+
export interface FlexOffsetParent {
3535
layout: string;
3636
isRtl: boolean;
3737
}
3838

3939
@Injectable({providedIn: 'root'})
40-
export class FlexOffsetStyleBuilder implements StyleBuilder {
41-
buildStyles(offset: string, parent: FlexOffsetParent): StyleDefinition {
40+
export class FlexOffsetStyleBuilder extends StyleBuilder {
41+
buildStyles(offset: string, parent: FlexOffsetParent) {
4242
const isPercent = String(offset).indexOf('%') > -1;
4343
const isPx = String(offset).indexOf('px') > -1;
4444
if (!isPx && !isPercent && !isNaN(+offset)) {
4545
offset = offset + '%';
4646
}
4747
const horizontalLayoutKey = parent.isRtl ? 'margin-right' : 'margin-left';
48-
49-
return isFlowHorizontal(parent.layout) ? {[horizontalLayoutKey]: `${offset}`} :
48+
const styles = isFlowHorizontal(parent.layout) ? {[horizontalLayoutKey]: `${offset}`} :
5049
{'margin-top': `${offset}`};
50+
51+
return styles;
5152
}
5253
}
5354

@@ -185,6 +186,20 @@ export class FlexOffsetDirective extends BaseDirective implements OnInit, OnChan
185186
// The flex-direction of this element's flex container. Defaults to 'row'.
186187
const layout = this._getFlexFlowDirection(this.parentElement, true);
187188
const isRtl = this._directionality.value === 'rtl';
189+
if (layout === 'row' && isRtl) {
190+
this._styleCache = flexOffsetCacheRowRtl;
191+
} else if (layout === 'row' && !isRtl) {
192+
this._styleCache = flexOffsetCacheRowLtr;
193+
} else if (layout === 'column' && isRtl) {
194+
this._styleCache = flexOffsetCacheColumnRtl;
195+
} else if (layout === 'column' && !isRtl) {
196+
this._styleCache = flexOffsetCacheColumnLtr;
197+
}
188198
this.addStyles((value && (value + '') || ''), {layout, isRtl});
189199
}
190200
}
201+
202+
const flexOffsetCacheRowRtl: Map<string, StyleDefinition> = new Map();
203+
const flexOffsetCacheColumnRtl: Map<string, StyleDefinition> = new Map();
204+
const flexOffsetCacheRowLtr: Map<string, StyleDefinition> = new Map();
205+
const flexOffsetCacheColumnLtr: Map<string, StyleDefinition> = new Map();

src/lib/flex/flex-order/flex-order.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,15 @@ import {
2121
MediaMonitor,
2222
StyleBuilder,
2323
StyleDefinition,
24-
StyleUtils
24+
StyleUtils,
2525
} from '@angular/flex-layout/core';
2626

2727
@Injectable({providedIn: 'root'})
28-
export class FlexOrderStyleBuilder implements StyleBuilder {
29-
buildStyles(value: string): StyleDefinition {
28+
export class FlexOrderStyleBuilder extends StyleBuilder {
29+
buildStyles(value: string) {
3030
const val = parseInt(value, 10);
31-
return {order: isNaN(val) ? 0 : val};
31+
const styles = {order: isNaN(val) ? 0 : val};
32+
return styles;
3233
}
3334
}
3435

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

109110
this.addStyles(value || '');
110111
}
112+
113+
protected _styleCache = flexOrderCache;
111114
}
115+
116+
const flexOrderCache: Map<string, StyleDefinition> = new Map();

src/lib/flex/flex/flex.spec.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -858,6 +858,7 @@ describe('flex directive', () => {
858858
});
859859

860860
describe('with column basis zero disabled', () => {
861+
let styleBuilder: FlexStyleBuilder;
861862
beforeEach(() => {
862863
jasmine.addMatchers(customMatchers);
863864

@@ -875,16 +876,20 @@ describe('flex directive', () => {
875876
});
876877
});
877878

878-
it('should set flex basis to auto', async(() => {
879+
it('should set flex basis to auto', () => {
879880
componentWithTemplate(`
880881
<div fxLayout='column'>
881882
<div fxFlex></div>
882883
</div>
883884
`);
885+
styleBuilder = TestBed.get(FlexStyleBuilder);
886+
887+
// Reset the cache because the layout config is only set at startup
888+
styleBuilder.shouldCache = false;
884889
fixture.detectChanges();
885890
let element = queryFor(fixture, '[fxFlex]')[0];
886891
expectEl(element).toHaveStyle({'flex': '1 1 auto'}, styler);
887-
}));
892+
});
888893
});
889894

890895
describe('with custom builder', () => {
@@ -926,7 +931,7 @@ describe('flex directive', () => {
926931
});
927932

928933
@Injectable({providedIn: FlexModule})
929-
export class MockFlexStyleBuilder implements StyleBuilder {
934+
export class MockFlexStyleBuilder extends StyleBuilder {
930935
buildStyles(_input: string) {
931936
return {'flex': '1 1 30%'};
932937
}

src/lib/flex/flex/flex.ts

+26-13
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,16 @@ export type FlexBasisAlias = 'grow' | 'initial' | 'auto' | 'none' | 'nogrow' | '
4141
interface FlexBuilderParent {
4242
direction: string;
4343
hasWrap: boolean;
44-
useColumnBasisZero: boolean | undefined;
4544
}
4645

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

5555
// The flex-direction of this element's flex container. Defaults to 'row'.
5656
const direction = (parent.direction.indexOf('column') > -1) ? 'column' : 'row';
@@ -97,7 +97,7 @@ export class FlexStyleBuilder implements StyleBuilder {
9797
};
9898
switch (basis || '') {
9999
case '':
100-
const useColumnBasisZero = parent.useColumnBasisZero !== false;
100+
const useColumnBasisZero = this.layoutConfig.useColumnBasisZero !== false;
101101
basis = direction === 'row' ? '0%' : (useColumnBasisZero ? '0.000000001px' : 'auto');
102102
break;
103103
case 'initial': // default
@@ -192,7 +192,7 @@ export class FlexStyleBuilder implements StyleBuilder {
192192
}
193193
}
194194

195-
return extendObject(css, {'box-sizing': 'border-box'});
195+
return extendObject(css, {'box-sizing': 'border-box'}) as StyleDefinition;
196196
}
197197
}
198198

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

313-
let basis = String(flexBasis).replace(';', '');
314-
let parts = validateBasis(basis, this._queryInput('grow'), this._queryInput('shrink'));
313+
const basis = String(flexBasis).replace(';', '');
314+
const parts = validateBasis(basis, this._queryInput('grow'), this._queryInput('shrink'));
315315
const addFlexToParent = this.layoutConfig.addFlexToParent !== false;
316316
const direction = this._getFlexFlowDirection(this.parentElement, addFlexToParent);
317317
const hasWrap = this._layout && this._layout.wrap;
318-
const useColumnBasisZero = this.layoutConfig.useColumnBasisZero;
319-
this.addStyles(parts.join('_'), {direction, hasWrap, useColumnBasisZero});
318+
if (direction === 'row' && hasWrap) {
319+
this._styleCache = flexRowWrapCache;
320+
} else if (direction === 'row' && !hasWrap) {
321+
this._styleCache = flexRowCache;
322+
} else if (direction === 'column' && hasWrap) {
323+
this._styleCache = flexColumnWrapCache;
324+
} else if (direction === 'column' && !hasWrap) {
325+
this._styleCache = flexColumnCache;
326+
}
327+
this.addStyles(parts.join(' '), {direction, hasWrap});
320328
}
321329
}
330+
331+
const flexRowCache: Map<string, StyleDefinition> = new Map();
332+
const flexColumnCache: Map<string, StyleDefinition> = new Map();
333+
const flexRowWrapCache: Map<string, StyleDefinition> = new Map();
334+
const flexColumnWrapCache: Map<string, StyleDefinition> = new Map();

src/lib/flex/layout-align/layout-align.spec.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,8 @@ describe('layout-align directive', () => {
452452
});
453453

454454
@Injectable({providedIn: FlexModule})
455-
export class MockLayoutAlignStyleBuilder implements StyleBuilder {
455+
export class MockLayoutAlignStyleBuilder extends StyleBuilder {
456+
shouldCache = false;
456457
buildStyles(_input: string) {
457458
return {'justify-content': 'flex-end'};
458459
}

src/lib/flex/layout-align/layout-align.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,22 @@ import {
2323
MediaMonitor,
2424
StyleBuilder,
2525
StyleDefinition,
26-
StyleUtils
26+
StyleUtils,
2727
} from '@angular/flex-layout/core';
2828
import {Subscription} from 'rxjs';
2929

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

34-
interface LayoutAlignParent {
34+
export interface LayoutAlignParent {
3535
layout: string;
3636
}
3737

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

4443
// Main axis
4544
switch (mainAxis) {
@@ -105,7 +104,7 @@ export class LayoutAlignStyleBuilder implements StyleBuilder {
105104
!isFlowHorizontal(parent.layout) ? '100%' : null : null,
106105
'max-height': crossAxis === 'stretch' ?
107106
isFlowHorizontal(parent.layout) ? '100%' : null : null,
108-
});
107+
}) as StyleDefinition;
109108
}
110109
}
111110

@@ -204,6 +203,8 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha
204203
}
205204

206205
const layout = this._layout || 'row';
206+
this._styleCache = layout === 'row' ?
207+
layoutAlignHorizontalCache : layoutAlignVerticalCache;
207208
this.addStyles(value || '', {layout});
208209
}
209210

@@ -223,3 +224,6 @@ export class LayoutAlignDirective extends BaseDirective implements OnInit, OnCha
223224
this.addStyles(value, {layout: this._layout || 'row'});
224225
}
225226
}
227+
228+
const layoutAlignHorizontalCache: Map<string, StyleDefinition> = new Map();
229+
const layoutAlignVerticalCache: Map<string, StyleDefinition> = new Map();

src/lib/flex/layout-gap/layout-gap.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ describe('layout-gap directive', () => {
434434
});
435435

436436
@Injectable({providedIn: FlexModule})
437-
export class MockLayoutGapStyleBuilder implements StyleBuilder {
437+
export class MockLayoutGapStyleBuilder extends StyleBuilder {
438438
buildStyles(_input: string) {
439439
return {'margin-top': '12px'};
440440
}

0 commit comments

Comments
 (0)