/
bindings.ts
1063 lines (979 loc) Β· 44.8 KB
/
bindings.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
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @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 {SafeValue, unwrapSafeValue} from '../../sanitization/bypass';
import {StyleSanitizeFn, StyleSanitizeMode} from '../../sanitization/style_sanitizer';
import {global} from '../../util/global';
import {ProceduralRenderer3, RElement, Renderer3, RendererStyleFlags3, isProceduralRenderer} from '../interfaces/renderer';
import {ApplyStylingFn, LStylingData, StylingMapArray, StylingMapArrayIndex, StylingMapsSyncMode, SyncStylingMapsFn, TStylingConfig, TStylingContext, TStylingContextIndex, TStylingContextPropConfigFlags} from '../interfaces/styling';
import {NO_CHANGE} from '../tokens';
import {DEFAULT_BINDING_INDEX, DEFAULT_BINDING_VALUE, DEFAULT_GUARD_MASK_VALUE, MAP_BASED_ENTRY_PROP_NAME, TEMPLATE_DIRECTIVE_INDEX, concatString, forceStylesAsString, getBindingValue, getConfig, getDefaultValue, getGuardMask, getInitialStylingValue, getMapProp, getMapValue, getProp, getPropValuesStartPosition, getStylingMapArray, getTotalSources, getValue, getValuesCount, hasConfig, hasValueChanged, isContextLocked, isHostStylingActive, isSanitizationRequired, isStylingMapArray, isStylingValueDefined, lockContext, normalizeIntoStylingMap, patchConfig, setDefaultValue, setGuardMask, setMapAsDirty, setValue} from '../util/styling_utils';
import {getStylingState, resetStylingState} from './state';
const VALUE_IS_EXTERNALLY_MODIFIED = {};
/**
* --------
*
* This file contains the core logic for styling in Angular.
*
* All styling bindings (i.e. `[style]`, `[style.prop]`, `[class]` and `[class.name]`)
* will have their values be applied through the logic in this file.
*
* When a binding is encountered (e.g. `<div [style.width]="w">`) then
* the binding data will be populated into a `TStylingContext` data-structure.
* There is only one `TStylingContext` per `TNode` and each element instance
* will update its style/class binding values in concert with the styling
* context.
*
* To learn more about the algorithm see `TStylingContext`.
*
* --------
*/
/**
* The guard/update mask bit index location for map-based bindings.
*
* All map-based bindings (i.e. `[style]` and `[class]` )
*/
const STYLING_INDEX_FOR_MAP_BINDING = 0;
/**
* Visits a class-based binding and updates the new value (if changed).
*
* This function is called each time a class-based styling instruction
* is executed. It's important that it's always called (even if the value
* has not changed) so that the inner counter index value is incremented.
* This way, each instruction is always guaranteed to get the same counter
* state each time it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateClassViaContext(
context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number,
prop: string | null, bindingIndex: number,
value: boolean | string | null | undefined | StylingMapArray | NO_CHANGE,
forceUpdate?: boolean): boolean {
const isMapBased = !prop;
const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.classesIndex++;
const hostBindingsMode = isHostStylingActive(state.sourceIndex);
// even if the initial value is a `NO_CHANGE` value (e.g. interpolation or [ngClass])
// then we still need to register the binding within the context so that the context
// is aware of the binding before it gets locked.
if (!isContextLocked(context, hostBindingsMode) || value !== NO_CHANGE) {
const updated = updateBindingData(
context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate,
false);
if (updated || forceUpdate) {
// We flip the bit in the bitMask to reflect that the binding
// at the `index` slot has changed. This identifies to the flushing
// phase that the bindings for this particular CSS class need to be
// applied again because on or more of the bindings for the CSS
// class have changed.
state.classesBitMask |= 1 << countIndex;
return true;
}
}
return false;
}
/**
* Visits a style-based binding and updates the new value (if changed).
*
* This function is called each time a style-based styling instruction
* is executed. It's important that it's always called (even if the value
* has not changed) so that the inner counter index value is incremented.
* This way, each instruction is always guaranteed to get the same counter
* state each time it's called (which then allows the `TStylingContext`
* and the bit mask values to be in sync).
*/
export function updateStyleViaContext(
context: TStylingContext, data: LStylingData, element: RElement, directiveIndex: number,
prop: string | null, bindingIndex: number,
value: string | number | SafeValue | null | undefined | StylingMapArray | NO_CHANGE,
sanitizer: StyleSanitizeFn | null, forceUpdate?: boolean): boolean {
const isMapBased = !prop;
const state = getStylingState(element, directiveIndex);
const countIndex = isMapBased ? STYLING_INDEX_FOR_MAP_BINDING : state.stylesIndex++;
const hostBindingsMode = isHostStylingActive(state.sourceIndex);
// even if the initial value is a `NO_CHANGE` value (e.g. interpolation or [ngStyle])
// then we still need to register the binding within the context so that the context
// is aware of the binding before it gets locked.
if (!isContextLocked(context, hostBindingsMode) || value !== NO_CHANGE) {
const sanitizationRequired = isMapBased ?
true :
(sanitizer ? sanitizer(prop !, null, StyleSanitizeMode.ValidateProperty) : false);
const updated = updateBindingData(
context, data, countIndex, state.sourceIndex, prop, bindingIndex, value, forceUpdate,
sanitizationRequired);
if (updated || forceUpdate) {
// We flip the bit in the bitMask to reflect that the binding
// at the `index` slot has changed. This identifies to the flushing
// phase that the bindings for this particular property need to be
// applied again because on or more of the bindings for the CSS
// property have changed.
state.stylesBitMask |= 1 << countIndex;
return true;
}
}
return false;
}
/**
* Called each time a binding value has changed within the provided `TStylingContext`.
*
* This function is designed to be called from `updateStyleBinding` and `updateClassBinding`.
* If called during the first update pass, the binding will be registered in the context.
*
* This function will also update binding slot in the provided `LStylingData` with the
* new binding entry (if it has changed).
*
* @returns whether or not the binding value was updated in the `LStylingData`.
*/
function updateBindingData(
context: TStylingContext, data: LStylingData, counterIndex: number, sourceIndex: number,
prop: string | null, bindingIndex: number,
value: string | SafeValue | number | boolean | null | undefined | StylingMapArray,
forceUpdate?: boolean, sanitizationRequired?: boolean): boolean {
const hostBindingsMode = isHostStylingActive(sourceIndex);
if (!isContextLocked(context, hostBindingsMode)) {
// this will only happen during the first update pass of the
// context. The reason why we can't use `tNode.firstTemplatePass`
// here is because its not guaranteed to be true when the first
// update pass is executed (remember that all styling instructions
// are run in the update phase, and, as a result, are no more
// styling instructions that are run in the creation phase).
registerBinding(context, counterIndex, sourceIndex, prop, bindingIndex, sanitizationRequired);
patchConfig(
context,
hostBindingsMode ? TStylingConfig.HasHostBindings : TStylingConfig.HasTemplateBindings);
}
const changed = forceUpdate || hasValueChanged(data[bindingIndex], value);
if (changed) {
setValue(data, bindingIndex, value);
const doSetValuesAsStale = (getConfig(context) & TStylingConfig.HasHostBindings) &&
!hostBindingsMode && (prop ? !value : true);
if (doSetValuesAsStale) {
renderHostBindingsAsStale(context, data, prop);
}
}
return changed;
}
/**
* Iterates over all host-binding values for the given `prop` value in the context and sets their
* corresponding binding values to `null`.
*
* Whenever a template binding changes its value to `null`, all host-binding values should be
* re-applied
* to the element when the host bindings are evaluated. This may not always happen in the event
* that none of the bindings changed within the host bindings code. For this reason this function
* is expected to be called each time a template binding becomes falsy or when a map-based template
* binding changes.
*/
function renderHostBindingsAsStale(
context: TStylingContext, data: LStylingData, prop: string | null): void {
const valuesCount = getValuesCount(context);
if (prop !== null && hasConfig(context, TStylingConfig.HasPropBindings)) {
const itemsPerRow = TStylingContextIndex.BindingsStartOffset + valuesCount;
let i = TStylingContextIndex.ValuesStartPosition;
let found = false;
while (i < context.length) {
if (getProp(context, i) === prop) {
found = true;
break;
}
i += itemsPerRow;
}
if (found) {
const bindingsStart = i + TStylingContextIndex.BindingsStartOffset;
const valuesStart = bindingsStart + 1; // the first column is template bindings
const valuesEnd = bindingsStart + valuesCount - 1;
for (let i = valuesStart; i < valuesEnd; i++) {
const bindingIndex = context[i] as number;
if (bindingIndex !== 0) {
setValue(data, bindingIndex, null);
}
}
}
}
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
const bindingsStart =
TStylingContextIndex.ValuesStartPosition + TStylingContextIndex.BindingsStartOffset;
const valuesStart = bindingsStart + 1; // the first column is template bindings
const valuesEnd = bindingsStart + valuesCount - 1;
for (let i = valuesStart; i < valuesEnd; i++) {
const stylingMap = getValue<StylingMapArray>(data, context[i] as number);
if (stylingMap) {
setMapAsDirty(stylingMap);
}
}
}
}
/**
* Registers the provided binding (prop + bindingIndex) into the context.
*
* It is needed because it will either update or insert a styling property
* into the context at the correct spot.
*
* When called, one of two things will happen:
*
* 1) If the property already exists in the context then it will just add
* the provided `bindingValue` to the end of the binding sources region
* for that particular property.
*
* - If the binding value is a number then it will be added as a new
* binding index source next to the other binding sources for the property.
*
* - Otherwise, if the binding value is a string/boolean/null type then it will
* replace the default value for the property if the default value is `null`.
*
* 2) If the property does not exist then it will be inserted into the context.
* The styling context relies on all properties being stored in alphabetical
* order, so it knows exactly where to store it.
*
* When inserted, a default `null` value is created for the property which exists
* as the default value for the binding. If the bindingValue property is inserted
* and it is either a string, number or null value then that will replace the default
* value.
*
* Note that this function is also used for map-based styling bindings. They are treated
* much the same as prop-based bindings, but, their property name value is set as `[MAP]`.
*/
export function registerBinding(
context: TStylingContext, countId: number, sourceIndex: number, prop: string | null,
bindingValue: number | null | string | boolean, sanitizationRequired?: boolean): void {
let found = false;
prop = prop || MAP_BASED_ENTRY_PROP_NAME;
let totalSources = getTotalSources(context);
// if a new source is detected then a new column needs to be allocated into
// the styling context. The column is basically a new allocation of binding
// sources that will be available to each property.
while (totalSources <= sourceIndex) {
addNewSourceColumn(context);
totalSources++;
}
const isBindingIndexValue = typeof bindingValue === 'number';
const entriesPerRow = TStylingContextIndex.BindingsStartOffset + getValuesCount(context);
let i = TStylingContextIndex.ValuesStartPosition;
// all style/class bindings are sorted by property name
while (i < context.length) {
const p = getProp(context, i);
if (prop <= p) {
if (prop < p) {
allocateNewContextEntry(context, i, prop, sanitizationRequired);
} else if (isBindingIndexValue) {
patchConfig(context, TStylingConfig.HasCollisions);
}
addBindingIntoContext(context, i, bindingValue, countId, sourceIndex);
found = true;
break;
}
i += entriesPerRow;
}
if (!found) {
allocateNewContextEntry(context, context.length, prop, sanitizationRequired);
addBindingIntoContext(context, i, bindingValue, countId, sourceIndex);
}
}
/**
* Inserts a new row into the provided `TStylingContext` and assigns the provided `prop` value as
* the property entry.
*/
function allocateNewContextEntry(
context: TStylingContext, index: number, prop: string, sanitizationRequired?: boolean): void {
const config = sanitizationRequired ? TStylingContextPropConfigFlags.SanitizationRequired :
TStylingContextPropConfigFlags.Default;
context.splice(
index, 0,
config, // 1) config value
DEFAULT_GUARD_MASK_VALUE, // 2) template bit mask
DEFAULT_GUARD_MASK_VALUE, // 3) host bindings bit mask
prop, // 4) prop value (e.g. `width`, `myClass`, etc...)
);
index += 4; // the 4 values above
// 5...) default binding index for the template value
// depending on how many sources already exist in the context,
// multiple default index entries may need to be inserted for
// the new value in the context.
const totalBindingsPerEntry = getTotalSources(context);
for (let i = 0; i < totalBindingsPerEntry; i++) {
context.splice(index, 0, DEFAULT_BINDING_INDEX);
index++;
}
// 6) default binding value for the new entry
context.splice(index, 0, DEFAULT_BINDING_VALUE);
}
/**
* Inserts a new binding value into a styling property tuple in the `TStylingContext`.
*
* A bindingValue is inserted into a context during the first update pass
* of a template or host bindings function. When this occurs, two things
* happen:
*
* - If the bindingValue value is a number then it is treated as a bindingIndex
* value (a index in the `LView`) and it will be inserted next to the other
* binding index entries.
*
* - Otherwise the binding value will update the default value for the property
* and this will only happen if the default value is `null`.
*/
function addBindingIntoContext(
context: TStylingContext, index: number, bindingValue: number | string | boolean | null,
bitIndex: number, sourceIndex: number) {
if (typeof bindingValue === 'number') {
const hostBindingsMode = isHostStylingActive(sourceIndex);
const cellIndex = index + TStylingContextIndex.BindingsStartOffset + sourceIndex;
context[cellIndex] = bindingValue;
const updatedBitMask = getGuardMask(context, index, hostBindingsMode) | (1 << bitIndex);
setGuardMask(context, index, updatedBitMask, hostBindingsMode);
} else if (bindingValue !== null && getDefaultValue(context, index) === null) {
setDefaultValue(context, index, bindingValue);
}
}
/**
* Registers a new column into the provided `TStylingContext`.
*
* If and when a new source is detected then a new column needs to
* be allocated into the styling context. The column is basically
* a new allocation of binding sources that will be available to each
* property.
*
* Each column that exists in the styling context resembles a styling
* source. A styling source an either be the template or one or more
* components or directives all containing styling host bindings.
*/
function addNewSourceColumn(context: TStylingContext): void {
// we use -1 here because we want to insert right before the last value (the default value)
const insertOffset = TStylingContextIndex.BindingsStartOffset + getValuesCount(context) - 1;
let index = TStylingContextIndex.ValuesStartPosition;
while (index < context.length) {
index += insertOffset;
context.splice(index++, 0, DEFAULT_BINDING_INDEX);
// the value was inserted just before the default value, but the
// next entry in the context starts just after it. Therefore++.
index++;
}
context[TStylingContextIndex.TotalSourcesPosition]++;
}
/**
* Applies all pending style and class bindings to the provided element.
*
* This function will attempt to flush styling via the provided `classesContext`
* and `stylesContext` context values. This function is designed to be run from
* the internal `stylingApply` function (which is scheduled to run at the very
* end of change detection for an element if one or more style/class bindings
* were processed) and will rely on any state values that are set from when
* any of the styling bindings executed.
*
* This function is designed to be called twice: one when change detection has
* processed an element within the template bindings (i.e. just as `advance()`
* is called) and when host bindings have been processed. In both cases the
* styles and classes in both contexts will be applied to the element, but the
* algorithm will selectively decide which bindings to run depending on the
* columns in the context. The provided `directiveIndex` value will help the
* algorithm determine which bindings to apply: either the template bindings or
* the host bindings (see `applyStylingToElement` for more information).
*
* Note that once this function is called all temporary styling state data
* (i.e. the `bitMask` and `counter` values for styles and classes will be cleared).
*/
export function flushStyling(
renderer: Renderer3 | ProceduralRenderer3 | null, data: LStylingData,
classesContext: TStylingContext | null, stylesContext: TStylingContext | null,
element: RElement, directiveIndex: number, styleSanitizer: StyleSanitizeFn | null): void {
ngDevMode && ngDevMode.flushStyling++;
const state = getStylingState(element, directiveIndex);
const hostBindingsMode = isHostStylingActive(state.sourceIndex);
if (stylesContext) {
if (!isContextLocked(stylesContext, hostBindingsMode)) {
lockAndFinalizeContext(stylesContext, hostBindingsMode);
}
if (state.stylesBitMask !== 0) {
applyStylingViaContext(
stylesContext, renderer, element, data, state.stylesBitMask, setStyle, styleSanitizer,
hostBindingsMode);
}
}
if (classesContext) {
if (!isContextLocked(classesContext, hostBindingsMode)) {
lockAndFinalizeContext(classesContext, hostBindingsMode);
}
if (state.classesBitMask !== 0) {
applyStylingViaContext(
classesContext, renderer, element, data, state.classesBitMask, setClass, null,
hostBindingsMode);
}
}
resetStylingState();
}
/**
* Locks the context (so no more bindings can be added) and also copies over initial class/style
* values into their binding areas.
*
* There are two main actions that take place in this function:
*
* - Locking the context:
* Locking the context is required so that the style/class instructions know NOT to
* register a binding again after the first update pass has run. If a locking bit was
* not used then it would need to scan over the context each time an instruction is run
* (which is expensive).
*
* - Patching initial values:
* Directives and component host bindings may include static class/style values which are
* bound to the host element. When this happens, the styling context will need to be informed
* so it can use these static styling values as defaults when a matching binding is falsy.
* These initial styling values are read from the initial styling values slot within the
* provided `TStylingContext` (which is an instance of a `StylingMapArray`). This inner map will
* be updated each time a host binding applies its static styling values (via `elementHostAttrs`)
* so these values are only read at this point because this is the very last point before the
* first style/class values are flushed to the element.
*
* Note that the `TStylingContext` styling context contains two locks: one for template bindings
* and another for host bindings. Either one of these locks will be set when styling is applied
* during the template binding flush and/or during the host bindings flush.
*/
function lockAndFinalizeContext(context: TStylingContext, hostBindingsMode: boolean): void {
const initialValues = getStylingMapArray(context) !;
updateInitialStylingOnContext(context, initialValues);
lockContext(context, hostBindingsMode);
}
/**
* Registers all initial styling entries into the provided context.
*
* This function will iterate over all entries in the provided `initialStyling` ar}ray and register
* them as default (initial) values in the provided context. Initial styling values in a context are
* the default values that are to be applied unless overwritten by a binding.
*
* The reason why this function exists and isn't a part of the context construction is because
* host binding is evaluated at a later stage after the element is created. This means that
* if a directive or component contains any initial styling code (i.e. `<div class="foo">`)
* then that initial styling data can only be applied once the styling for that element
* is first applied (at the end of the update phase). Once that happens then the context will
* update itself with the complete initial styling for the element.
*/
function updateInitialStylingOnContext(
context: TStylingContext, initialStyling: StylingMapArray): void {
// `-1` is used here because all initial styling data is not a apart
// of a binding (since it's static)
const COUNT_ID_FOR_STYLING = -1;
let hasInitialStyling = false;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < initialStyling.length;
i += StylingMapArrayIndex.TupleSize) {
const value = getMapValue(initialStyling, i);
if (value) {
const prop = getMapProp(initialStyling, i);
registerBinding(context, COUNT_ID_FOR_STYLING, 0, prop, value, false);
hasInitialStyling = true;
}
}
if (hasInitialStyling) {
patchConfig(context, TStylingConfig.HasInitialStyling);
}
}
/**
* Runs through the provided styling context and applies each value to
* the provided element (via the renderer) if one or more values are present.
*
* This function will iterate over all entries present in the provided
* `TStylingContext` array (both prop-based and map-based bindings).-
*
* Each entry, within the `TStylingContext` array, is stored alphabetically
* and this means that each prop/value entry will be applied in order
* (so long as it is marked dirty in the provided `bitMask` value).
*
* If there are any map-based entries present (which are applied to the
* element via the `[style]` and `[class]` bindings) then those entries
* will be applied as well. However, the code for that is not a part of
* this function. Instead, each time a property is visited, then the
* code below will call an external function called `stylingMapsSyncFn`
* and, if present, it will keep the application of styling values in
* map-based bindings up to sync with the application of prop-based
* bindings.
*
* Visit `styling/map_based_bindings.ts` to learn more about how the
* algorithm works for map-based styling bindings.
*
* Note that this function is not designed to be called in isolation (use
* the `flushStyling` function so that it can call this function for both
* the styles and classes contexts).
*/
export function applyStylingViaContext(
context: TStylingContext, renderer: Renderer3 | ProceduralRenderer3 | null, element: RElement,
bindingData: LStylingData, bitMaskValue: number | boolean, applyStylingFn: ApplyStylingFn,
sanitizer: StyleSanitizeFn | null, hostBindingsMode: boolean): void {
const bitMask = normalizeBitMaskValue(bitMaskValue);
let stylingMapsSyncFn: SyncStylingMapsFn|null = null;
let applyAllValues = false;
if (hasConfig(context, TStylingConfig.HasMapBindings)) {
stylingMapsSyncFn = getStylingMapsSyncFn();
const mapsGuardMask =
getGuardMask(context, TStylingContextIndex.ValuesStartPosition, hostBindingsMode);
applyAllValues = (bitMask & mapsGuardMask) !== 0;
}
const valuesCount = getValuesCount(context);
let totalBindingsToVisit = 1;
let mapsMode =
applyAllValues ? StylingMapsSyncMode.ApplyAllValues : StylingMapsSyncMode.TraverseValues;
if (hostBindingsMode) {
mapsMode |= StylingMapsSyncMode.RecurseInnerMaps;
totalBindingsToVisit = valuesCount - 1;
}
let i = getPropValuesStartPosition(context);
while (i < context.length) {
const guardMask = getGuardMask(context, i, hostBindingsMode);
if (bitMask & guardMask) {
let valueApplied = false;
const prop = getProp(context, i);
const defaultValue = getDefaultValue(context, i);
// Part 1: Visit the `[styling.prop]` value
for (let j = 0; j < totalBindingsToVisit; j++) {
const bindingIndex = getBindingValue(context, i, j) as number;
if (!valueApplied && bindingIndex !== 0) {
const value = getValue(bindingData, bindingIndex);
if (isStylingValueDefined(value)) {
const checkValueOnly = hostBindingsMode && j === 0;
if (!checkValueOnly) {
const finalValue = sanitizer && isSanitizationRequired(context, i) ?
sanitizer(prop, value, StyleSanitizeMode.SanitizeOnly) :
unwrapSafeValue(value);
applyStylingFn(renderer, element, prop, finalValue, bindingIndex);
}
valueApplied = true;
}
}
// Part 2: Visit the `[style]` or `[class]` map-based value
if (stylingMapsSyncFn) {
// determine whether or not to apply the target property or to skip it
let mode = mapsMode | (valueApplied ? StylingMapsSyncMode.SkipTargetProp :
StylingMapsSyncMode.ApplyTargetProp);
// the first column in the context (when `j == 0`) is special-cased for
// template bindings. If and when host bindings are being processed then
// the first column will still be iterated over, but the values will only
// be checked against (not applied). If and when this happens we need to
// notify the map-based syncing code to know not to apply the values it
// comes across in the very first map-based binding (which is also located
// in column zero).
if (hostBindingsMode && j === 0) {
mode |= StylingMapsSyncMode.CheckValuesOnly;
}
const valueAppliedWithinMap = stylingMapsSyncFn(
context, renderer, element, bindingData, j, applyStylingFn, sanitizer, mode, prop,
defaultValue);
valueApplied = valueApplied || valueAppliedWithinMap;
}
}
// Part 3: apply the default value (e.g. `<div style="width:200">` => `200px` gets applied)
// if the value has not yet been applied then a truthy value does not exist in the
// prop-based or map-based bindings code. If and when this happens, just apply the
// default value (even if the default value is `null`).
if (!valueApplied) {
applyStylingFn(renderer, element, prop, defaultValue);
}
}
i += TStylingContextIndex.BindingsStartOffset + valuesCount;
}
// the map-based styling entries may have not applied all their
// values. For this reason, one more call to the sync function
// needs to be issued at the end.
if (stylingMapsSyncFn) {
if (hostBindingsMode) {
mapsMode |= StylingMapsSyncMode.CheckValuesOnly;
}
stylingMapsSyncFn(
context, renderer, element, bindingData, 0, applyStylingFn, sanitizer, mapsMode);
}
}
/**
* Applies the provided styling map to the element directly (without context resolution).
*
* This function is designed to be run from the styling instructions and will be called
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* This function has three different cases that can occur (for each item in the map):
*
* - Case 1: Attempt to apply the current value in the map to the element (if it's `non null`).
*
* - Case 2: If a map value fails to be applied then the algorithm will find a matching entry in
* the initial values present in the context and attempt to apply that.
*
* - Default Case: If the initial value cannot be applied then a default value of `null` will be
* applied (which will remove the style/class value from the element).
*
* See `allowDirectStylingApply` to learn the logic used to determine whether any style/class
* bindings can be directly applied.
*
* @returns whether or not the styling map was applied to the element.
*/
export function applyStylingMapDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, value: {[key: string]: any} | string | null, isClassBased: boolean,
sanitizer?: StyleSanitizeFn | null, forceUpdate?: boolean,
bindingValueContainsInitial?: boolean): void {
const oldValue = getValue(data, bindingIndex);
if (forceUpdate || hasValueChanged(oldValue, value)) {
const config = getConfig(context);
const hasInitial = config & TStylingConfig.HasInitialStyling;
const initialValue =
hasInitial && !bindingValueContainsInitial ? getInitialStylingValue(context) : null;
setValue(data, bindingIndex, value);
// the cached value is the last snapshot of the style or class
// attribute value and is used in the if statement below to
// keep track of internal/external changes.
const cachedValueIndex = bindingIndex + 1;
let cachedValue = getValue(data, cachedValueIndex);
if (cachedValue === NO_CHANGE) {
cachedValue = initialValue;
}
cachedValue = typeof cachedValue !== 'string' ? '' : cachedValue;
// If a class/style value was modified externally then the styling
// fast pass cannot guarantee that the external values are retained.
// When this happens, the algorithm will bail out and not write to
// the style or className attribute directly.
let writeToAttrDirectly = !(config & TStylingConfig.HasPropBindings);
if (writeToAttrDirectly &&
checkIfExternallyModified(element as HTMLElement, cachedValue, isClassBased)) {
writeToAttrDirectly = false;
if (oldValue !== VALUE_IS_EXTERNALLY_MODIFIED) {
// direct styling will reset the attribute entirely each time,
// and, for this reason, if the algorithm decides it cannot
// write to the class/style attributes directly then it must
// reset all the previous style/class values before it starts
// to apply values in the non-direct way.
removeStylingValues(renderer, element, oldValue, isClassBased);
// this will instruct the algorithm not to apply class or style
// values directly anymore.
setValue(data, cachedValueIndex, VALUE_IS_EXTERNALLY_MODIFIED);
}
}
if (writeToAttrDirectly) {
const initialValue =
hasInitial && !bindingValueContainsInitial ? getInitialStylingValue(context) : null;
const valueToApply =
writeStylingValueDirectly(renderer, element, value, isClassBased, initialValue);
setValue(data, cachedValueIndex, valueToApply || null);
} else {
const applyFn = isClassBased ? setClass : setStyle;
const map = normalizeIntoStylingMap(oldValue, value, !isClassBased);
const initialStyles = hasInitial ? getStylingMapArray(context) : null;
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(map, i);
const value = getMapValue(map, i);
// case 1: apply the map value (if it exists)
let applied =
applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
// case 2: apply the initial value (if it exists)
if (!applied && initialStyles) {
applied = findAndApplyMapValue(
renderer, element, applyFn, initialStyles, prop, bindingIndex, sanitizer);
}
// default case: apply `null` to remove the value
if (!applied) {
applyFn(renderer, element, prop, null, bindingIndex);
}
}
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
if (isClassBased) {
state.lastDirectClassMap = map;
} else {
state.lastDirectStyleMap = map;
}
}
}
}
export function writeStylingValueDirectly(
renderer: any, element: RElement, value: {[key: string]: any} | string | null,
isClassBased: boolean, initialValue: string | null): string {
let valueToApply: string;
if (isClassBased) {
valueToApply = typeof value === 'string' ? value : objectToClassName(value);
if (initialValue !== null) {
valueToApply = concatString(initialValue, valueToApply, ' ');
}
setClassName(renderer, element, valueToApply);
} else {
valueToApply = forceStylesAsString(value as{[key: string]: any}, true);
if (initialValue !== null) {
valueToApply = initialValue + ';' + valueToApply;
}
setStyleAttr(renderer, element, valueToApply);
}
return valueToApply;
}
/**
* Applies the provided styling prop/value to the element directly (without context resolution).
*
* This function is designed to be run from the styling instructions and will be called
* automatically. This function is intended to be used for performance reasons in the
* event that there is no need to apply styling via context resolution.
*
* This function has four different cases that can occur:
*
* - Case 1: Apply the provided prop/value (style or class) entry to the element
* (if it is `non null`).
*
* - Case 2: If value does not get applied (because its `null` or `undefined`) then the algorithm
* will check to see if a styling map value was applied to the element as well just
* before this (via `styleMap` or `classMap`). If and when a map is present then the
* algorithm will find the matching property in the map and apply its value.
*
* - Case 3: If a map value fails to be applied then the algorithm will check to see if there
* are any initial values present and attempt to apply a matching value based on
* the target prop.
*
* - Default Case: If a matching initial value cannot be applied then a default value
* of `null` will be applied (which will remove the style/class value
* from the element).
*
* See `allowDirectStylingApply` to learn the logic used to determine whether any style/class
* bindings can be directly applied.
*
* @returns whether or not the prop/value styling was applied to the element.
*/
export function applyStylingValueDirectly(
renderer: any, context: TStylingContext, element: RElement, data: LStylingData,
bindingIndex: number, prop: string, value: any, isClassBased: boolean,
sanitizer?: StyleSanitizeFn | null): boolean {
let applied = false;
if (hasValueChanged(data[bindingIndex], value)) {
setValue(data, bindingIndex, value);
const applyFn = isClassBased ? setClass : setStyle;
// case 1: apply the provided value (if it exists)
applied = applyStylingValue(renderer, element, prop, value, applyFn, bindingIndex, sanitizer);
// case 2: find the matching property in a styling map and apply the detected value
if (!applied && hasConfig(context, TStylingConfig.HasMapBindings)) {
const state = getStylingState(element, TEMPLATE_DIRECTIVE_INDEX);
const map = isClassBased ? state.lastDirectClassMap : state.lastDirectStyleMap;
applied = map ?
findAndApplyMapValue(renderer, element, applyFn, map, prop, bindingIndex, sanitizer) :
false;
}
// case 3: apply the initial value (if it exists)
if (!applied && hasConfig(context, TStylingConfig.HasInitialStyling)) {
const map = getStylingMapArray(context);
applied =
map ? findAndApplyMapValue(renderer, element, applyFn, map, prop, bindingIndex) : false;
}
// default case: apply `null` to remove the value
if (!applied) {
applyFn(renderer, element, prop, null, bindingIndex);
}
}
return applied;
}
function applyStylingValue(
renderer: any, element: RElement, prop: string, value: any, applyFn: ApplyStylingFn,
bindingIndex: number, sanitizer?: StyleSanitizeFn | null): boolean {
let valueToApply: string|null = unwrapSafeValue(value);
if (isStylingValueDefined(valueToApply)) {
valueToApply =
sanitizer ? sanitizer(prop, value, StyleSanitizeMode.ValidateAndSanitize) : valueToApply;
applyFn(renderer, element, prop, valueToApply, bindingIndex);
return true;
}
return false;
}
function findAndApplyMapValue(
renderer: any, element: RElement, applyFn: ApplyStylingFn, map: StylingMapArray, prop: string,
bindingIndex: number, sanitizer?: StyleSanitizeFn | null) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < map.length;
i += StylingMapArrayIndex.TupleSize) {
const p = getMapProp(map, i);
if (p === prop) {
let valueToApply = getMapValue(map, i);
valueToApply = sanitizer ?
sanitizer(prop, valueToApply, StyleSanitizeMode.ValidateAndSanitize) :
valueToApply;
applyFn(renderer, element, prop, valueToApply, bindingIndex);
return true;
}
if (p > prop) {
break;
}
}
return false;
}
function normalizeBitMaskValue(value: number | boolean): number {
// if pass => apply all values (-1 implies that all bits are flipped to true)
if (value === true) return -1;
// if pass => skip all values
if (value === false) return 0;
// return the bit mask value as is
return value;
}
let _activeStylingMapApplyFn: SyncStylingMapsFn|null = null;
export function getStylingMapsSyncFn() {
return _activeStylingMapApplyFn;
}
export function setStylingMapsSyncFn(fn: SyncStylingMapsFn) {
_activeStylingMapApplyFn = fn;
}
/**
* Assigns a style value to a style property for the given element.
*/
export const setStyle: ApplyStylingFn =
(renderer: Renderer3 | null, native: RElement, prop: string, value: string | null) => {
if (renderer !== null) {
// Use `isStylingValueDefined` to account for falsy values that should be bound like 0.
if (isStylingValueDefined(value)) {
// opacity, z-index and flexbox all have number values
// and these need to be converted into strings so that
// they can be assigned properly.
value = value.toString();
ngDevMode && ngDevMode.rendererSetStyle++;
if (isProceduralRenderer(renderer)) {
renderer.setStyle(native, prop, value, RendererStyleFlags3.DashCase);
} else {
// The reason why native style may be `null` is either because
// it's a container element or it's a part of a test
// environment that doesn't have styling. In either
// case it's safe not to apply styling to the element.
const nativeStyle = native.style;
if (nativeStyle != null) {
nativeStyle.setProperty(prop, value);
}
}
} else {
ngDevMode && ngDevMode.rendererRemoveStyle++;
if (isProceduralRenderer(renderer)) {
renderer.removeStyle(native, prop, RendererStyleFlags3.DashCase);
} else {
const nativeStyle = native.style;
if (nativeStyle != null) {
nativeStyle.removeProperty(prop);
}
}
}
}
};
/**
* Adds/removes the provided className value to the provided element.
*/
export const setClass: ApplyStylingFn =
(renderer: Renderer3 | null, native: RElement, className: string, value: any) => {
if (renderer !== null && className !== '') {
if (value) {
ngDevMode && ngDevMode.rendererAddClass++;
if (isProceduralRenderer(renderer)) {
renderer.addClass(native, className);
} else {
// the reason why classList may be `null` is either because
// it's a container element or it's a part of a test
// environment that doesn't have styling. In either
// case it's safe not to apply styling to the element.
const classList = native.classList;
if (classList != null) {
classList.add(className);
}
}
} else {
ngDevMode && ngDevMode.rendererRemoveClass++;
if (isProceduralRenderer(renderer)) {
renderer.removeClass(native, className);
} else {
const classList = native.classList;
if (classList != null) {
classList.remove(className);
}
}
}
}
};
export const setClassName = (renderer: Renderer3 | null, native: RElement, className: string) => {
if (renderer !== null) {
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(native, 'class', className);
} else {
native.className = className;
}
}
};
export const setStyleAttr = (renderer: Renderer3 | null, native: RElement, value: string) => {
if (renderer !== null) {
if (isProceduralRenderer(renderer)) {
renderer.setAttribute(native, 'style', value);
} else {
native.setAttribute('style', value);
}
}
};
/**
* Iterates over all provided styling entries and renders them on the element.
*
* This function is used alongside a `StylingMapArray` entry. This entry is not
* the same as the `TStylingContext` and is only really used when an element contains
* initial styling values (e.g. `<div style="width:200px">`), but no style/class bindings
* are present. If and when that happens then this function will be called to render all
* initial styling values on an element.
*/
export function renderStylingMap(
renderer: Renderer3, element: RElement, stylingValues: TStylingContext | StylingMapArray | null,
isClassBased: boolean): void {
const stylingMapArr = getStylingMapArray(stylingValues);
if (stylingMapArr) {
for (let i = StylingMapArrayIndex.ValuesStartPosition; i < stylingMapArr.length;
i += StylingMapArrayIndex.TupleSize) {
const prop = getMapProp(stylingMapArr, i);
const value = getMapValue(stylingMapArr, i);
if (isClassBased) {
setClass(renderer, element, prop, value, null);
} else {
setStyle(renderer, element, prop, value, null);
}