# Direct Write & Merge Algorithms Styling Algorithms Style and class bindings in Angular Ivy are designed to resolve styling values across templates, directives and components. In order to make things fast and efficient, Angular will resolve style/class bindings on an element in one of two different modes: - **Direct Write Mode**: style/class bindings are written directly to the `className` and `style.cssText` values - **Merge Mode**: style/class bindings are merged using a `TStylingContext` instance and are applied to an element via `classList.add`/`classList.remove` and `style[prop]` The mode is determined by various factors (more explained in each section below), however, once determined, a config flag is set on the `TNode` which is then used to direct which mode to run: ```typescript= function styleProp(prop: string, value: string|null) { if (tNode.stylesConfig & TStylingConfig.writeStylesDirectly) { // use direct mode algorithm } else { // use merge mode algorithm } } ``` This design doc explains what each of the styling modes are, how they work together and also what the development steps required are so that these algorithms can be implemented into Angular. There is also a development roadmap that explains when each feature will land. # Direct Write Mode Direct write mode works exactly as it sounds: it writes the collected class/style binding data to the `className` and `style.cssText` element properties. The key thing to remember about direct write mode styling is that **Angular 100% owns (no one else is trying to write to those properties) the the styling property** when this mode is active. In order for this mode to be activated, the following requirements must be met: - There is no external access on any of the style/class values on the element (i.e. Angular has fully control over the element) - This is verified by reading from the `className`/`style.cssText` properties and verifying that the expected value is a full match. - There are no duplicate style/class bindings. - Duplicate bindings occur when one or more directives write to the same style/class bindings that another source (template or directive) is also writing to. - Non DOM nodes are being used in the application (e.g. WebWorker render nodes) If at any point Angular detects external access or duplicate bindings then the `TNode` will be flagged and then each of the style/class bindings will fallback to the "merge mode". ## The Algorithm The direct write mode algorithm (when activated) will evaluate each of the style/class bindings one by one and will populate a concatenated string value of each class/style entry. Then, once all bindings have been evaluated, then that concatenated string will be written into the `RNode` using single a `className` / `style.cssText` DOM property write. Let's say for example we have the following template code: ```htmlmixed= <div style="color:red" [style]="'opacity:0.5'" [style.width.px]="200" [style.height.px]="400"> ... </div> ``` Then a final string value of `color:red; opacity:0.5; width:200px; height:400px` will be written to the element (via `style.cssText`) once all bindings have been evaluted. Angular knows that all style/class bindings have been evaluated once the element `exitFn` is fired (which occurs when the `advance()` instruction is run, the template function finishes or when all host bindings are evaluated on a specific element). The algorithm itself works by concatenating a class/style string each time the bindings run. For this to work, the underlying `LView` will now reserve **two** slots of space for each styling instruction: ```typescript= // these are the bindings in the code above lView = [ //... // [style] 'opacity:0.5', // Last value to be used for CD '...', // Concatinated value // [style.width.px] 200, '...', // [style.height.px] 400 '...', //... ] ``` The `...` values represent the intermediate concatenated string value that is bulit up as each of the bindings run. Here's what that looks like for each binding: ```typescript= // these are the bindings in the code above lView = [ //... // [style] 'opacity:0.5', 'color: red; opacity: 0.5', // color comes from the static styles // [style.width.px] 200, 'color: red; opacity: 0.5; width: 200px;', // [style.height.px] 400, 'color: red; opacity: 0.5; width: 200px; height: 400px', //... ] ``` The reason why the intermediate string values are stored for later is because each time the bindings run, the algorithm would have to concatinate and allocate strings just in case the value has changed this would create memory preasure which we are trying to avoid). Now if the intermediate values are stored for later then the algorithm can just use the last concatenated string value from the previous CD run and continue from there. The concatenation algorithm is as follows: ```typescript= // shared global values between instructions let _lastClassIntermediateIndex: number; let _previousClassConcattedValue: string; let _lastStyleIntermediateIndex: number; let _previousStyleConcattedValue: string; function getValue(lView: LView, index: number) { return lView[Math.abs(index)]; } function directSetBinding(lView: LView, bindingIndex: number, value: string, isClassBased: boolean): void { // the intermediate index is the index value that points // to the second slot in the LView next to where the current // `bindingIndex` slot is. The `lastIntermediateIndex` is // the most recent intermediateIndex value that was used in // the previous `directSetBinding()` call for the same element // in the same CD cycle. const lastIntermediateIndex = isClassBased ? _lastClassIntermediateIndex : _lastStyleIntermediateIndex; if (valueChanged(value, getValue(lView, bindingIndex)) || lastIntermediateIndex < 0) { lView[bindingIndex] = value; let concattedValue = lView[Math.abs(lastIntermediateIndex)]; concattedValue = concatString( concattedValue || '', value, isClassBased ? ' ' : ';'); const intermediateIndex = bindingIndex + 1; const previousConcattedValue = getValue(lView, intermediateIndex); // a NEGATIVE intermediateIndex value implies that the // value at this binding has changed. This way when the // next instruction runs it knows to concat the new string // together even if the binding value hasn't changed. if (isClassBased) { _lastClassIntermediateIndex = -intermediateIndex; _previousClassConcattedValue = previousConcattedValue; } else { _lastStyleIntermediateIndex = -intermediateIndex; _previousStyleConcattedValue = previousConcattedValue; } lView[intermediateIndex] = concattedValue; onExit(applyStyling); } } // this is designed only to be called once if one or more // style/class bindings have changed function applyStyling() { const lView = getLView(); const native = getNative(); if (_lastClassIntermediateIndex < 0) { const classes = lView[Math.abs(_lastClassIntermediateIndex)]; setClass(native, classes); _lastClassIntermediateIndex = 0; } if (_lastStyleIntermediateIndex !== 0) { const styles = lView[Math.abs(_lastStyleIntermediateIndex)]; setStyles(native, styles); _lastStyleIntermediateIndex = 0; } } ``` ## External Access? This algorithm takes full control over the `element.className` and `element.style.cssText` properties and backs out if notices any external access to said properties. The implication here is that any changes to the class and/or style properties from outside of this styling algorithm would be clobered on the next CD. To get around this issue of external access to the `className`/`style.cssText` properties, the direct mode algorithm will check to see if the classes/styles were modified before writing the new value by reading and verifing the value against what was written in the last update. ```typescript= if (_previousClassConcattedValue === element.className) { // no classes were added externally } if (_previousStyleConcattedValue === element.style.cssText) { // no classes were added externally } ``` This check is performed at every time styles/classes are written on the element (when the direct write algorithm is active). ```typescript= // shared global values between instructions let _lastClassIntermediateIndex: number; let _previousClassConcattedValue: string; let _lastStyleIntermediateIndex: number; let _previousStyleConcattedValue: string; function getValue(lView: LView, index: number) { return lView[Math.abs(index)]; } function directSetBinding(lView: LView, bindingIndex: number, value: string, isClassBased: boolean): void { // ... same as before ... } function applyStyling() { const lView = getLView(); const native = getNative(); // the last intermediate index value is the intermediateIndex // value of the last style/class instruction that was applied if (_lastClassIntermediateIndex < 0) { if (_previousClassConcattedValue === native.className) { const newClasses = getValue(lView, _lastClassIntermediateIndex); // ... write the classes to the element ... } else { // ... flag TNode and fallback to "merge mode" for classes ... } _lastClassIntermediateIndex = 0; _previousClassConcattedValue = ''; } if (_lastStyleIntermediateIndex < 0) { if (_previousStyleConcattedValue === native.style.cssText) { const newStyles = getValue(lView, _lastStyleIntermediateIndex); // ... write the styles to the element ... } else { // ... flag TNode and fallback to "merge mode" for styles ... } _lastStyleIntermediateIndex = 0; _previousClassConcattedValue = ''; } } ``` The "fallback" operation is explained next. ## Fallback Algorithm In the event that the direct mode algorithm needs to fallback to the merge mode algorithm, some extra steps need to be taken so that the styles/classes can be applied correctly. The challenge with falling back from "direct write mode" to "merge mode" is that the bindings need to be registered on to a new instance of a `TStylingContext` context. For this to happen, the bindings themselves need to be stored in the `TView.data` property matching exactly to the binding index values that are used to store the data in `LView`. So if we have a look our `LView` from the example above: ```typescript= // these are the bindings in the code above lView = [ //... // [style] 'opacity:0.5', 'color: red; opacity: 0.5', // color comes from the static styles // [style.width.px] '200px', 'color: red; opacity: 0.5; width: 200px;', // [style.height.px] '400px', 'color: red; opacity: 0.5; width: 200px; height: 400px', //... ] ``` The `TView.data` array would look like so: ```typescript= // these are the bindings in the code above lView = [ //... // [style] (bindingIndex = 20) null, 0, // [style.width.px] (bindingIndex = 22) 'width', 20, // points to [style] // [style.height.px] (bindingIndex = 24) 'height', 22, // points to [style.width] //... ] ``` There are two values for every styling entry in the `TView.data` array: the property name (`null` for `[MAP]` based entries) and the previous `bindingIndex` of the last styling value was registered. Now, when a fallback operation occurs, all the algorithm needs to do is loop upwards through each entry and register them into the `TStylingContext`. ```typescript= function fallbackToStylingContext(tData: TData, lView: LView, element: RElement, finalBindingIndex: number, styleSanitizer: any, isClassBased: boolean) { const context = allocTStylingContext(); let bindingIndex = finalBindingIndex; while (bindingIndex !== 0) { const prop = tData[bindingIndex] as string|null; const value = lView[bindingIndex] as any; if (isClassBased) { updateStyleViaContext(context, lView, element, TEMPLATE_DIRECTIVE_INDEX, prop, bindingIndex, value, styleSanitizer); } else { updateClassViaContext(context, lView, element, TEMPLATE_DIRECTIVE_INDEX, prop, bindingIndex, value); } bindingIndex = tData[binindexIndex + 1]; } applyStyling(context); return context; } ``` Now the next time the algorithm runs, the bindings will update accordingly using the "merge" algorithm. # Merge Algorithm The merge mode algorithm makes use of the `TStylingContext` together with the context resolution algorithm. (The context resolution algorithm itself will not change in anyway with the presence of the direct mode algorithm.) Here is the original design doc for context resolution: https://hackmd.io/mkSOhRxJQaWCl9BY9jzQZg # Changes to TNode Now that there are two distinct algorithms present within Angular, the `TNode` should be the source of truth for all styling-related configurations (right now all config values are stored inside of a `TStylingContext` instance which is placed on `tNode.styles` or `tNode.classes`). These configuration values should be added to the existing `tNode.flags` property. Here is a breakdown of all the configurations: ```typescript= // all the values in here are binary, but for the // sake of simplicity, the values are not displayed // in the enum below const enum TNodeFlags { //... all the existing flags... // whether or not there were any initial style/class values on the element hasInitialStyling, // whether or not any class values have been applied to the element hasExternalStylingAccess, // // class-related configurations // // whether or not direct apply is used for classes writeClassesDirectly, // whether or not there are any active [class] bindings hasClassMapBindings, // whether or not there are any active [class.prop] bindings hasClassPropBindings, // whether or not there are any duplicate bindings (i.e. multiple bindings that write to the same class value) hasDuplicateClassBindings, // // style-related configurations // // whether or not direct apply is used for styles writeStylesDirectly, // whether or not there are any active [style] bindings hasStyleMapBindings, // whether or not there are any active [style.prop] bindings hasStylePropBindings, // whether or not there are any duplicate bindings (i.e. multiple bindings that write to the same style value) hasDuplicateStyleBindings, } ``` These configurations are stored on `tNode.flags`. ## Initial Styling If one or more initial style/class values are applied to the element during `elementStart` then those values will be stored in the element's `TNode` value (this happens only during `firstTemplatePass`). ```typescript= // <div style="width:200px" class="active"> element(0, 'div', [1, 'width', '200px', 2, 'active']) ``` When evaluated the `TNode will have the following values populated`: ```typescript= tNode.styles = 'width:200px'; tNode.classes = 'active'; ``` The initial styles are used both in the direct write and merge styling modes. The direct write mode doesn't change anything with these values and will leave them on the `TNode` instance just as they are. However, when the merge mode algorithm is activated then it will instantiate instance of `TStylingContext` and apply them to the `TNode` on the `.styles` and `.classes` members. When this happens, the initial styling will be stored within the context as follows: ```typescript= tNode.styles = [ 'width:200px', // rest of the context... ]; tNode.classes = [ 'active', // rest of the context... ]; ``` # Development Roadmap Plan This effort consists of various clean up operations before the algorithm can land in its full entirety. Here is the roadmap for landing all features in this design doc: ## 1. TNode.firstUpdatePass Context locking is unnecessary because styling bindings are only registered into the `TData`/`TStylingContext` during the first update of a template/hostBindings function. **Task**: Introduce `tNode.firstUpdatePass` flag and remove context locking flags. This addition of the `tNode.firstUpdatePass` flag allows for the removal of `TemplateBindingsLocked` and `HostBindingsLocked` styling flags. From this point onwards, the `tNode.firstUpdatePass` flag is the source of truth for whether or not any bindings are being evaluated for the first time (which is when the bindings themselves are registered in the `TData`/`TStylingContext`). **PR #1**: https://github.com/angular/angular/pull/33486 <span style="color:green">(merged)</span> **PR #2**: https://github.com/angular/angular/pull/33521 <span style="color:green">(merged)</span> ## 2. TNode Styling Flags Styling flags currently live in `TStylingContext`. They should all be inside of `TNode.flags`. **Task**: Move all styling flags to `TNode.flags` and get rid of `TStylingContext[CONFIG]`. The `TNode.flags` value will now be the source of truth for all configurations for `tNode.styles` and `tNode.classes` **PR 1**: https://github.com/angular/angular/pull/33605 <span style="color:green">(merged)</span> **PR 2**: https://github.com/angular/angular/pull/33540 <span style="color:green">(merged)</span> ## 3. Full Micro-Benchmark Coverage Currently there exist cases in styling that are not covered in the micro-benchmark testing suites. **Task 1**: introduce a micro-benchmark for directives with styling collisions. **Task 2**: introduce a micro-benchmark for directives with multuiple map-based `[style]`/`[class]` bindings. <span style="color:maroon">~~**Task 3**: introduce a micro-benchmark that makes use of embedded templates with multiple `[style]`/`[class]` bindings.~~</span> > ^^ All micro-benchmarks operate with embedded templates behind the scenes anyway. **PR 1**: https://github.com/angular/angular/pull/33600 <span style="color:green">(merged)</span> **PR 2**: https://github.com/angular/angular/pull/33608 <span style="color:green">(merged)</span> ## 4. Direct Mode Algorithm **Task**: Introduce the `directModeSetValue` algorithm and remove all existing code from `applyStylingValueDirectly`/`applyStylingMapDirectly`. **PR 1**: https://github.com/angular/angular/pull/33666 **PR 2**: https://github.com/angular/angular/pull/33669 ## 5. Instruction Cleanup Each of the styling (e.g. `classMap`, `styleMap`, `styleProp` and `classProp`) instructions need to have a more obvious switch between the direct write and merge modes. **Task 1**: Make sure all instructions are coded down to a simple if statement: ```typescript= if (tNode.flags & TNodeFlags.writeStylesDirectly) { // apply style value(s) directly } else { // apply style value(s) using the merge mode algorithm } if (tNode.flags & TNodeFlags.writeClassesDirectly) { // apply class value(s) directly } else { // apply class value(s) using the erge mode algorithm } ``` **Task 2**: Update all documentation to mention only cases of **direct write** and **merge** modes. ## 6. Simplify Initial Styling All initial styling values are converted into a `StylingMapArray` during element creation. ```typescript= // <div style="width:200px; height:300px;" class="cool"></div> element.styles = ['width:200px; height:300px', 'width', '200px', 'height', '300px']; element.classes = ['cool', 'cool', true]; ``` These should be simiplified so that the array isn't needed. Just a simple string value so that can used during element creation and when the direct mode values are being applied. ```typescript= // <div style="width:200px; height:300px;" class="cool"></div> element.styles = 'width:200px; height:300px'; element.classes = 'cool'; ``` **Task**: Simplify `tNode.styles`/`tNode.classes`: ## 7. Improve Style/Class Debugging Design doc here: https://hackmd.io/Q8WNgWmCTEu7c1k7h4dpHg?both