/
styling.ts
579 lines (546 loc) Β· 21.5 KB
/
styling.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* 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 {StyleSanitizeFn} from '../../sanitization/style_sanitizer';
import {ProceduralRenderer3, RElement, Renderer3} from './renderer';
import {LView} from './view';
/**
* --------
*
* This file contains the core interfaces for styling in Angular.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* A static-level representation of all style or class bindings/values
* associated with a `TNode`.
*
* The `TStylingContext` unites all template styling bindings (i.e.
* `[class]` and `[style]` bindings) as well as all host-level
* styling bindings (for components and directives) together into
* a single manifest
*
* The styling context is stored on a `TNode` on and there are
* two instances of it: one for classes and another for styles.
*
* ```typescript
* tNode.styles = [ ... a context only for styles ... ];
* tNode.classes = [ ... a context only for classes ... ];
* ```
*
* The styling context is created each time there are one or more
* styling bindings (style or class bindings) present for an element,
* but is only created once per `TNode`.
*
* `tNode.styles` and `tNode.classes` can be an instance of the following:
*
* ```typescript
* tNode.styles = null; // no static styling or styling bindings active
* tNode.styles = StylingMapArray; // only static values present (e.g. `<div style="width:200">`)
* tNode.styles = TStylingContext; // one or more styling bindings present (e.g. `<div
* [style.width]>`)
* ```
*
* Both `tNode.styles` and `tNode.classes` are instantiated when anything
* styling-related is active on an element. They are first created from
* from the any of the element-level instructions (e.g. `element`,
* `elementStart`, `elementHostAttrs`). When any static style/class
* values are encountered they are registered on the `tNode.styles`
* and `tNode.classes` data-structures. By default (when any static
* values are encountered) the `tNode.styles` or `tNode.classes` values
* are instances of a `StylingMapArray`. Only when style/class bindings
* are detected then that styling map is converted into an instance of
* `TStylingContext`.
*
* Due to the fact the the `TStylingContext` is stored on a `TNode`
* this means that all data within the context is static. Instead of
* storing actual styling binding values, the lView binding index values
* are stored within the context. (static nature means it is more compact.)
*
* The code below shows a breakdown of two instances of `TStylingContext`
* (one for `tNode.styles` and another for `tNode.classes`):
*
* ```typescript
* // <div [class.active]="c" // lView binding index = 20
* // [style.width]="x" // lView binding index = 21
* // [style.height]="y"> // lView binding index = 22
* // ...
* // </div>
* tNode.styles = [
* 0, // the context config value (see `TStylingContextConfig`)
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b010, // template guard mask for height
* 0, // host bindings guard mask for height
* 'height', // the property name
* 22, // the binding location for the "y" binding in the lView
* null, // the default value for height
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'width', // the property name
* 21, // the binding location for the "x" binding in the lView
* null, // the default value for width
* ];
*
* tNode.classes = [
* 0, // the context config value (see `TStylingContextConfig`)
* 1, // the total amount of sources present (only `1` b/c there are only template
* bindings)
* [null], // initial values array (an instance of `StylingMapArray`)
*
* 0, // config entry for the property (see `TStylingContextPropConfigFlags`)
* 0b001, // template guard mask for width
* 0, // host bindings guard mask for width
* 'active', // the property name
* 20, // the binding location for the "c" binding in the lView
* null, // the default value for the `active` class
* ];
* ```
*
* Entry value present in an entry (called a tuple) within the
* styling context is as follows:
*
* ```typescript
* context = [
* //...
* configValue,
* templateGuardMask,
* hostBindingsGuardMask,
* propName,
* ...bindingIndices...,
* defaultValue
* //...
* ];
* ```
*
* Below is a breakdown of each value:
*
* - **configValue**:
* Property-specific configuration values. The only config setting
* that is implemented right now is whether or not to sanitize the
* value.
*
* - **templateGuardMask**:
* A numeric value where each bit represents a binding index
* location. Each binding index location is assigned based on
* a local counter value that increments each time an instruction
* is called:
*
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y"> // binding index = 22 (counter index = 1)
* ```
*
* In the example code above, if the `width` value where to change
* then the first bit in the local bit mask value would be flipped
* (and the second bit for when `height`).
*
* If and when there are more than 32 binding sources in the context
* (more than 32 `[style/class]` bindings) then the bit masking will
* overflow and we are left with a situation where a `-1` value will
* represent the bit mask. Due to the way that JavaScript handles
* negative values, when the bit mask is `-1` then all bits within
* that value will be automatically flipped (this is a quick and
* efficient way to flip all bits on the mask when a special kind
* of caching scenario occurs or when there are more than 32 bindings).
*
* - **hostBindingsGuardMask**:
* Another instance of a guard mask that is specific to host bindings.
* This behaves exactly the same way as does the `templateGuardMask`,
* but will not contain any binding information processed in the template.
* The reason why there are two instances of guard masks (one for the
* template and another for host bindings) is because the template bindings
* are processed before host bindings and the state information is not
* carried over into the host bindings code. As soon as host bindings are
* processed for an element the counter and state-based bit mask values are
* set to `0`.
*
* ```
* <div [style.width]="x" // binding index = 21 (counter index = 0)
* [style.height]="y" // binding index = 22 (counter index = 1)
* dir-that-sets-width // binding index = 30 (counter index = 0)
* dir-that-sets-width> // binding index = 31 (counter index = 1)
* ```
*
* - **propName**:
* The CSS property name or class name (e.g `width` or `active`).
*
* - **bindingIndices...**:
* A series of numeric binding values that reflect where in the
* lView to find the style/class values associated with the property.
* Each value is in order in terms of priority (templates are first,
* then directives and then components). When the context is flushed
* and the style/class values are applied to the element (this happens
* inside of the `stylingApply` instruction) then the flushing code
* will keep checking each binding index against the associated lView
* to find the first style/class value that is non-null.
*
* - **defaultValue**:
* This is the default that will always be applied to the element if
* and when all other binding sources return a result that is null.
* Usually this value is `null` but it can also be a static value that
* is intercepted when the tNode is first constructured (e.g.
* `<div style="width:200px">` has a default value of `200px` for
* the `width` property).
*
* Each time a new binding is encountered it is registered into the
* context. The context then is continually updated until the first
* styling apply call has been called (which is automatically scheduled
* to be called once an element exits during change detection). Note that
* each entry in the context is stored in alphabetical order.
*
* Once styling has been flushed for the first time for an element the
* context will set as locked (this prevents bindings from being added
* to the context later on).
*
* # How Styles/Classes are Rendered
* Each time a styling instruction (e.g. `[class.name]`, `[style.prop]`,
* etc...) is executed, the associated `lView` for the view is updated
* at the current binding location. Also, when this happens, a local
* counter value is incremented. If the binding value has changed then
* a local `bitMask` variable is updated with the specific bit based
* on the counter value.
*
* Below is a lightweight example of what happens when a single style
* property is updated (i.e. `<div [style.prop]="val">`):
*
* ```typescript
* function updateStyleProp(prop: string, value: string) {
* const lView = getLView();
* const bindingIndex = BINDING_INDEX++;
*
* // update the local counter value
* const indexForStyle = stylingState.stylesCount++;
* if (lView[bindingIndex] !== value) {
* lView[bindingIndex] = value;
*
* // tell the local state that we have updated a style value
* // by updating the bit mask
* stylingState.bitMaskForStyles |= 1 << indexForStyle;
* }
* }
* ```
*
* Once all the bindings have updated a `bitMask` value will be populated.
* This `bitMask` value is used in the apply algorithm (which is called
* context resolution).
*
* ## The Apply Algorithm (Context Resolution)
* As explained above, each time a binding updates its value, the resulting
* value is stored in the `lView` array. These styling values have yet to
* be flushed to the element.
*
* Once all the styling instructions have been evaluated, then the styling
* context(s) are flushed to the element. When this happens, the context will
* be iterated over (property by property) and each binding source will be
* examined and the first non-null value will be applied to the element.
*
* Let's say that we the following template code:
*
* ```html
* <div [style.width]="w1" dir-that-set-width="w2"></div>
* ```
*
* There are two styling bindings in the code above and they both write
* to the `width` property. When styling is flushed on the element, the
* algorithm will try and figure out which one of these values to write
* to the element.
*
* In order to figure out which value to apply, the following
* binding prioritization is adhered to:
*
* 1. First template-level styling bindings are applied (if present).
* This includes things like `[style.width]` and `[class.active]`.
*
* 2. Second are styling-level host bindings present in directives.
* (if there are sub/super directives present then the sub directives
* are applied first).
*
* 3. Third are styling-level host bindings present in components.
* (if there are sub/super components present then the sub directives
* are applied first).
*
* This means that in the code above the styling binding present in the
* template is applied first and, only if its falsy, then the directive
* styling binding for width will be applied.
*
* ### What about map-based styling bindings?
* Map-based styling bindings are activated when there are one or more
* `[style]` and/or `[class]` bindings present on an element. When this
* code is activated, the apply algorithm will iterate over each map
* entry and apply each styling value to the element with the same
* prioritization rules as above.
*
* For the algorithm to apply styling values efficiently, the
* styling map entries must be applied in sync (property by property)
* with prop-based bindings. (The map-based algorithm is described
* more inside of the `render3/styling/map_based_bindings.ts` file.)
*
* ## Sanitization
* Sanitization is used to prevent invalid style values from being applied to
* the element.
*
* It is enabled in two cases:
*
* 1. The `styleSanitizer(sanitizerFn)` instruction was called (just before any other
* styling instructions are run).
*
* 2. The component/directive `LView` instance has a sanitizer object attached to it
* (this happens when `renderComponent` is executed with a `sanitizer` value or
* if the ngModule contains a sanitizer provider attached to it).
*
* If and when sanitization is active then all property/value entries will be evaluated
* through the active sanitizer before they are applied to the element (or the styling
* debug handler).
*
* If a `Sanitizer` object is used (via the `LView[SANITIZER]` value) then that object
* will be used for every property.
*
* If a `StyleSanitizerFn` function is used (via the `styleSanitizer`) then it will be
* called in two ways:
*
* 1. property validation mode: this will be called early to mark whether a property
* should be sanitized or not at during the flushing stage.
*
* 2. value sanitization mode: this will be called during the flushing stage and will
* run the sanitizer function against the value before applying it to the element.
*
* If sanitization returns an empty value then that empty value will be applied
* to the element.
*/
export interface TStylingContext extends
Array<number|string|number|boolean|null|StylingMapArray|{}> {
/** Configuration data for the context */
[TStylingContextIndex.ConfigPosition]: TStylingConfig;
/** The total amount of sources present in the context */
[TStylingContextIndex.TotalSourcesPosition]: number;
/** Initial value position for static styles */
[TStylingContextIndex.InitialStylingValuePosition]: StylingMapArray;
}
/**
* A series of flags used to configure the config value present within an instance of
* `TStylingContext`.
*/
export const enum TStylingConfig {
/**
* The initial state of the styling context config.
*/
Initial = 0b000000,
/**
* Whether or not there are any directives on this element.
*
* This is used so that certain performance optimizations can
* take place (e.g. direct style/class binding application).
*
* Note that the presence of this flag doesn't guarantee the
* presence of host-level style or class bindings within any
* of the active directives on the element.
*
* Examples include:
* 1. `<div dir-one>`
* 2. `<div dir-one [dir-two]="x">`
* 3. `<comp>`
* 4. `<comp dir-one>`
*/
HasDirectives = 0b000001,
/**
* Whether or not there are prop-based bindings present.
*
* Examples include:
* 1. `<div [style.prop]="x">`
* 2. `<div [class.prop]="x">`
* 3. `@HostBinding('style.prop') x`
* 4. `@HostBinding('class.prop') x`
*/
HasPropBindings = 0b000010,
/**
* Whether or not there are map-based bindings present.
*
* Examples include:
* 1. `<div [style]="x">`
* 2. `<div [class]="x">`
* 3. `@HostBinding('style') x`
* 4. `@HostBinding('class') x`
*/
HasMapBindings = 0b000100,
/**
* Whether or not there are map-based and prop-based bindings present.
*
* Examples include:
* 1. `<div [style]="x" [style.prop]="y">`
* 2. `<div [class]="x" [style.prop]="y">`
* 3. `<div [style]="x" dir-that-sets-some-prop>`
* 4. `<div [class]="x" dir-that-sets-some-class>`
*/
HasPropAndMapBindings = HasPropBindings | HasMapBindings,
/**
* Whether or not there are two or more sources for a single property in the context.
*
* Examples include:
* 1. prop + prop: `<div [style.width]="x" dir-that-sets-width>`
* 2. map + prop: `<div [style]="x" [style.prop]>`
* 3. map + map: `<div [style]="x" dir-that-sets-style>`
*/
HasCollisions = 0b001000,
/**
* Whether or not the context contains initial styling values.
*
* Examples include:
* 1. `<div style="width:200px">`
* 2. `<div class="one two three">`
* 3. `@Directive({ host: { 'style': 'width:200px' } })`
* 4. `@Directive({ host: { 'class': 'one two three' } })`
*/
HasInitialStyling = 0b0010000,
/**
* Whether or not the context contains one or more template bindings.
*
* Examples include:
* 1. `<div [style]="x">`
* 2. `<div [style.width]="x">`
* 3. `<div [class]="x">`
* 4. `<div [class.name]="x">`
*/
HasTemplateBindings = 0b0100000,
/**
* Whether or not the context contains one or more host bindings.
*
* Examples include:
* 1. `@HostBinding('style') x`
* 2. `@HostBinding('style.width') x`
* 3. `@HostBinding('class') x`
* 4. `@HostBinding('class.name') x`
*/
HasHostBindings = 0b1000000,
/** A Mask of all the configurations */
Mask = 0b1111111,
/** Total amount of configuration bits used */
TotalBits = 7,
}
/**
* An index of position and offset values used to navigate the `TStylingContext`.
*/
export const enum TStylingContextIndex {
ConfigPosition = 0,
TotalSourcesPosition = 1,
InitialStylingValuePosition = 2,
ValuesStartPosition = 3,
// each tuple entry in the context
// (config, templateBitGuard, hostBindingBitGuard, prop, ...bindings||default-value)
ConfigOffset = 0,
TemplateBitGuardOffset = 1,
HostBindingsBitGuardOffset = 2,
PropOffset = 3,
BindingsStartOffset = 4
}
/**
* A series of flags used for each property entry within the `TStylingContext`.
*/
export const enum TStylingContextPropConfigFlags {
Default = 0b0,
SanitizationRequired = 0b1,
TotalBits = 1,
Mask = 0b1,
}
/**
* A function used to apply or remove styling from an element for a given property.
*/
export interface ApplyStylingFn {
(renderer: Renderer3|ProceduralRenderer3|null, element: RElement, prop: string, value: any,
bindingIndex?: number|null): void;
}
/**
* Runtime data type that is used to store binding data referenced from the `TStylingContext`.
*
* Because `LView` is just an array with data, there is no reason to
* special case `LView` everywhere in the styling algorithm. By allowing
* this data type to be an array that contains various scalar data types,
* an instance of `LView` doesn't need to be constructed for tests.
*/
export type LStylingData = LView | (string | number | boolean | null)[];
/**
* Array-based representation of a key/value array.
*
* The format of the array is "property", "value", "property2",
* "value2", etc...
*
* The first value in the array is reserved to store the instance
* of the key/value array that was used to populate the property/
* value entries that take place in the remainder of the array.
*/
export interface StylingMapArray extends Array<{}|string|number|null|undefined> {
/**
* The last raw value used to generate the entries in the map.
*/
[StylingMapArrayIndex.RawValuePosition]: {}|string|number|null|undefined;
}
/**
* An index of position and offset points for any data stored within a `StylingMapArray` instance.
*/
export const enum StylingMapArrayIndex {
/** Where the values start in the array */
ValuesStartPosition = 1,
/** The location of the raw key/value map instance used last to populate the array entries */
RawValuePosition = 0,
/** The size of each property/value entry */
TupleSize = 2,
/** The offset for the property entry in the tuple */
PropOffset = 0,
/** The offset for the value entry in the tuple */
ValueOffset = 1,
}
/**
* Used to apply/traverse across all map-based styling entries up to the provided `targetProp`
* value.
*
* When called, each of the map-based `StylingMapArray` entries (which are stored in
* the provided `LStylingData` array) will be iterated over. Depending on the provided
* `mode` value, each prop/value entry may be applied or skipped over.
*
* If `targetProp` value is provided the iteration code will stop once it reaches
* the property (if found). Otherwise if the target property is not encountered then
* it will stop once it reaches the next value that appears alphabetically after it.
*
* If a `defaultValue` is provided then it will be applied to the element only if the
* `targetProp` property value is encountered and the value associated with the target
* property is `null`. The reason why the `defaultValue` is needed is to avoid having the
* algorithm apply a `null` value and then apply a default value afterwards (this would
* end up being two style property writes).
*
* @returns whether or not the target property was reached and its value was
* applied to the element.
*/
export interface SyncStylingMapsFn {
(context: TStylingContext, renderer: Renderer3|ProceduralRenderer3|null, element: RElement,
data: LStylingData, sourceIndex: number, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn|null, mode: StylingMapsSyncMode, targetProp?: string|null,
defaultValue?: boolean|string|null): boolean;
}
/**
* Used to direct how map-based values are applied/traversed when styling is flushed.
*/
export const enum StylingMapsSyncMode {
/** Only traverse values (no prop/value styling entries get applied) */
TraverseValues = 0b000,
/** Apply every prop/value styling entry to the element */
ApplyAllValues = 0b001,
/** Only apply the target prop/value entry */
ApplyTargetProp = 0b010,
/** Skip applying the target prop/value entry */
SkipTargetProp = 0b100,
/** Iterate over inner maps map values in the context */
RecurseInnerMaps = 0b1000,
/** Only check to see if a value was set somewhere in each map */
CheckValuesOnly = 0b10000,
}