The Grand [styling] Refactor in Ivy
Angular Team Tech Talk
June 2019
Matias Niemelä
- Styling, Animations & Core Framework Stuff
- 7 Years of NG
- San Francisco
Angular [styling]...
style="prop:value"
class="classOne classTwo"
[style.prop]="myExp"
[class.name]="myExp"
[ngStyle]="myStylesExp"
[ngClass]="myClassesExp"
What Are [styling] Bindings?
- CSS Styling
- CSS Classes
- What does Angular do with this stuff?
<!--
- how does Angular parse this?
- how to it efficiently render this?
- how does it handle collisions?
- how is this better in Ivy?
-->
<header class="big">I love styling!</div>
<div class="content">
<p>Lorem ipsum dolor sit amet,
consectetur adipiscing elit. Integer mattis,
mi nec sodales pellentesque, nisi ex feugiat leo,
porttitor tempor nisi enim eu quam...</p>
<p [style.display]="showMore ?
'block' : 'none'">
TNunc vitae fringilla libero. Integer in dolor tellus.
Duis ultricies luctus luctus. Suspendisse condimentum
suscipit bibendum. Phasellus ut magna ex. Aenean urna
sapien, pellentesque a posuere at, dignissim vel lectus.
Proin fringilla maximus justo in bibendum.
</p>
<button (click)="toggleShowMore()">
Show More
</button>
</div>
<div [ngClass]="myClassExp"
[ngStyle]="myStyleExp">
...
</div>
@Directive({
selector: '[ngStyle]'
})
class NgStyleDirective {
@Input('ngStyle')
ngStyleVal: string;
@Input('style')
styleVal: string;
}
Class Bindings
class="classOne classTwo"
[class.name]="x"
[ngClass]="classesStr"
[attr.class]="classesStr"
renderer.addClass()
renderer.removeClass()
renderer.setAttribute()
Style Bindings
style="key:value"
[style.prop]="x"
[ngStyle]="{key:value}"
[attr.style]="key: value"
renderer.setStyle()
renderer.removeStyle()
renderer.setAttribute()
Ivy Styling (now)
VE Styling (v8)
- [style.prop] and [class.prop]
- ngStyle and ngClass are independent directives
- [style] and [class] not supported
- Applied immediately
- [style.prop] and [class.prop]
- ngStyle and ngClass will be deprecated
- [style] and [class] supported
- Applied after CD
<div
[style.width]="w1"
[ngStyle]="{width:w2}"
dir-that-sets-width>
How does the view
engine handle this?
</div>
ngStyle / ngClass
[style.prop] / [class.name]
- Core instructions applied through the renderer
- No synchronization to with other styling mechanisms
- Applied immediately
- Common directive code that is applied standalone
- No synchronization to with other styling mechanisms
- Applied after CD
Angular[styling] in Ivy
- Late 2018
- Supports [style.prop], [style], [class.prop] and [class]
- Synchronizes code across all input sources
Some lessons learned
- Bundle size is too large
- Large memory pressure
- Lots of lookups and checks
- Code itself is too complex to understand
// <div [style.width]="w">
stylesContextForDiv = [
/* header data */
config, 'width', '200px',
// ...
]
<div *ngFor="let item of items"
[style.width]="w">
{{ item }}
</div>
<!-- context -->
<div style="width:200px">...</div>
<!-- context -->
<div style="width:200px">...</div>
<!-- context -->
<div style="width:200px">...</div>
<!-- context -->
<div style="width:200px">...</div>
Angular[styling] in Ivy v2
- Compiler & Instructions
- The Diffing Algorithm
- Improvements...
Design Doc is Available
- Up to date with current implementation
- Roadmap with PR updates
- More details about the underlying algorithm
Compiler & Instructions
- HTML => instructions
- instructions render the component
- instructions include style and class bindings
<div [binding]
attr="value">
... text ...
<p *ngIf></p>
</div>
<script>
function templateFn(){
instruction();
instruction();
instruction();
instruction();
}
</script>
<!--
templateFn for my-component.ts
-->
<div class="modal-box"
[style.width]="width"
[style.height]="height"
[class.disabled]="isDisabled">
My Fancy Modal Box
</div>
// Generated Ng code for my-component.html
myComponentTplFn = function(rf, ctx) {
if (rf & CREATE) {
elementStart(0, 'div', [2, 'modal-box']);
styling();
elementEnd();
text(1, 'My Fancy Modal Box');
}
if (rf & UPDATE) {
classProp('disabled', ctx.isDisabled);
styleProp('width', ctx.width);
styleProp('height', ctx.height);
stylingApply();
}
}
Binding | Ivy Instruction |
---|---|
[style.prop]="value" | styleProp(ctx.value) |
[class.name]="value" | classProp(ctx.value) |
[style]="{key:value}" | styleMap({key:value}) |
[class]=" 'one two three' " | classMap("one two three") |
<div style="key:value"> | element('div', [1, 'key', 'value']) |
<div class="classOne classTwo"> | element('div', [2, 'classOne', 'classTwo']) |
[ngStyle] => [style] [ngClass] => [class]
classProp('disabled', ctx.isDisabled);
styleProp('width', ctx.width);
styleProp('height', ctx.height);
styleMap({ width: '200px' });
classMap('active');
Ivy Styling
- Templates, directives, and components are all understood
- Only render once
- Prioritization
- Fast
- Tree-Shakeable
- Lazy Loadable
- Small Bundle Size
- Low Memory Pressure
JS Optimization
- No maps
- No JS/TS classes
- No global knowledge
- No global state
- No boolean vars (only bits)
- No extra arrays
- No closures
- Nothing beyond o(n)
- Lots of Caching
- Lots of bit shifting
fixed => static/AOT dynamic => arrays
// Fastest
data = {name:null};
data.name = "Matias";
// Slow
data = {};
data.name = "Matias";
// Fastest
data = {name:null};
data.name = "Matias";
// Not As Fast
data = [null];
data[NAME_POSITION] = "Matias";
// Fastest
data = [null];
data[NAME_POSITION] = 'Matias';
// Not as fast
data = new Map();
data = new Set();
setValue(data, 'name', 'Matias');
state/data => array keys => const/enums
class StyleValues {
private _dirty = false;
updateValue(prop: string,
value: string|null) {
//...
this._dirty = true;
}
isDirty() {
return this._dirty;
}
}
var StyleValues = function() {
function t() {
this._dirty = !1
}
return t.prototype.updateValue =
function(t, i) {
this._dirty = !0
}, t.prototype.isDirty =
function() {
return this._dirty
}, t
}();
const enum StyleValuesIndex {
DirtyFlagPosition = 0,
ValuesStartPosition = 1,
}
interface StyleValues extends Array<string|null|number>{
[0]: boolean; //dirty flag
}
function markDirty(styles: StyleValues, yes: boolean) {
styles[StyleValuesIndex.DirtyFlagPosition] = yes;
}
function updateValue(styles: StyleValues, prop: string,
value: string) {
markDirty(styles, true);
// add the value to the array
}
function d(t, a) {
t[0] = a
}
function u(t, a, i) {
d(t, !0)
}
ES6 will makes things better...
The Diffing Algorithm
- HTML => instructions
- instructions render the component
- instructions include style and class bindings
classesContext = [
"initial value",
0b11101,
1, 1, "--MAP--", 30,
1, 2, "width", 23, null,
1, 2, "height", 24, "100px",
]
//...
if (classBitMask & updateMask) {
setClass(className, value);
}
if (styleBitMask & updateMask) {
setStyle(prop, value);
}
NG Template vs Element
- There is only one tNode object per template entry
- Elements are instances of the template object in the component
// list-item.ts
@Component({
selector: 'list-item',
template: `
<h2>{{ title }}</h2>
`
})
class ListItemComp {
@Input('title');
public title: 'string';
}
<!-- app.html -->
<list-item title="one">
</list-item>
<list-item title="two">
</list-item>
<list-item title="three">
</list-item>
<list-item title="four">
</list-item>
Element (LView entry)
Template Item (tNode)
- There is only one tNode per element
- There is only one lView per component instance
- The lView holds all the element data inside of it
- Each element contains minimal data
- All bindings and data are stored somewhere in the lView
- The lView is updated each time on change
tNode => definition lView => instances
<!--
templateFn for my-component.ts
-->
<div class="modal-box"
[style.width]="width"
[style.height]="height"
[class.disabled="isDisabled">
My Fancy Modal Box
</div>
/* class="modal-box"
[style.width]="width"
[style.height]="height"
[class.disabled="isDisabled"> */
tNode1 = {
attributes: [2, 'modal-box'],
bindings: [
'style.width',
'style.height',
'class.disabled'],
}
// <div class="modal-box"...>1</div>
// <div class="modal-box"...>2</div>
lView = [
// ...
'100px', // width value for 1
'200px', // height value for 1
true, // disabled value for 1
//...
'200px', // width value for 2
'400px', // height value for 2
false, // disabled value for 2
]
styling...
- tNode contains a StylingContext (one for class and one for style)
- The context contains the binding locations
- After instructions are run, the context is applied
- style/class values are read from the lView
<!--
templateFn for my-component.ts
-->
<div class="modal-box"
[style.width]="width"
[style.height]="height"
[class.disabled]="isDisabled">
My Fancy Modal Box
</div>
// styles are width and height
tNode.styles = [
bitMask, total, 'height', 31, null,
bitMask, total, 'width', 30, null,
];
// classes are modal-box and disabled
tNode.classes = [
bitMask, total, 'disabled', 30, null,
bitMask, total, 'modal-box', true,
];
bit masks...
- used for caching (update when a style/class changes)
- guard mask and update mask
- guard mask is set when first registered
- cache mask is updated when a value changes
<!--
templateFn for my-component.ts
-->
<div class="modal-box"
[style.width]="width"
[style.height]="height"
[class.disabled]="isDisabled">
My Fancy Modal Box
</div>
/*
styles
1. width // lView index = 32
2. height // lView index = 32
classes
1. modal-box // only a default value
2. disabled // lView index = 31
*/
let widthGuardMask = 0b001; // style index 0.
let heightGuardMask = 0b010; // style index 1.
let modalBoxGuardMask = 0b001; // class index 0.
let disabledGuardMask = 0b010; // class index 1.
/*
width = '999px'; (index = 0, bindingIndex = 31)
height = '0px'; (index = 1, bindingIndex = 32)
disabled = true; (index = 0, bindingIndex = 33)
*/
let stylesUpdateMask = 0;
let classesUpdateMask = 0;
stylesUpdateMask |= 1 << 0; // width index is 0
lView[31] = '999px';
stylesUpdateMask |= 1 << 1; // height index is 1
lView[32] = '0px';
classesUpdateMask |= 1 << 0; // disabled index is 0
lView[33] = true;
let i = 0;
while (i < styleContext.length) {
const guardMask = styleContext[i++];
const valuesCount = styleContext[i++];
const prop = styleContext[i++];
if (guardMask & styleUpdateMask) {
for (let j = 0; j < valuesCount; j++) {
const bindingIndex = stylingContext[i + j];
let value = lView[bindingIndex];
if (typeof bindingIndex === 'number' && !value) {
continue;
}
setStyle(element, prop, value);
}
}
i += valuesCount;
}
let i = 0;
while (i < styleContext.length) {
const guardMask = styleContext[i++];
const valuesCount = styleContext[i++];
const className = styleContext[i++];
if (guardMask & classUpdateMask) {
for (let j = 0; j < valuesCount; j++) {
const bindingIndex = stylingContext[i + j];
let value = lView[bindingIndex];
if (typeof bindingIndex === 'number' && !value) {
continue;
}
setClass(element, className, value);
}
}
i += valuesCount;
}
/*
<div
style="width:200px"
[style.width]="w" # binding index = 33
dir-that-sets-width # binding index = 34
dir-that-sets-width-also> # binding index = 35
...
</div>
*/
tNode.styles [
//...
0b111, 3, 'width', 33, 34, 35, '200px'
//...
];
[style] and [class] bindings
- Replacements for ngStyle and ngClass
- Tree-shaken away if not used
- Fully synced up with single prop styling bindings
<div
[style.width]="w"
[style.height]="w"
[style]="{width:'200px'}"
[class.active]="a"
[class.disabled]="d"
[class]=" 'one two three' ">
...
</div>
[class]="..."
[style]="..."
- Applies multiple styles all the element as one binding
- Fully handles diffing
- Applied alongside other style bindings
- HostBinding support
- Applies multiple classes all the element as one binding
- Fully handles diffing
- Applied alongside other style bindings
- HostBinding support
<!--
templateFn for my-component.ts
style = {width:'200px',height:'100px'};
class = 'foo bar';
-->
<div [style]="myStyles"
[class]="myClasses">
Lots of styling...
</div>
function templateFn(ctx, rf) {
if (rf & CREATE) {
styling();
}
if (rf & UPDATE) {
styleMap(ctx.myStyles);
classMap(ctx.myClasses);
stylingApply();
}
}
stylesArrayMap = [
{...}, 'height', '100px', 'width', '200px',
];
classesArrayMap = [
'foo bar', 'bar', true, 'foo', true,
];
lView = [
// ...
stylesArrayMap, // value for [style]
classesArrayMap, // value for [class]
]
algorithm?
- classMap and styleMap activate map-based bindings
- the algorithm checks for these values during flush
- the flush itself works very similar to merge within merge-sort
- it is always o(n) because of the pre-sorted values
stylesMap1 = [ // binding index = 23
{...}, 'height', '100px', 'width', '200px',
];
stylesMap2 = [ // binding index = 24
{...}, 'color', 'blue',
];
tNode.styles = [
0b0001, 2, '--MAP--', 23, 24,
0b0010, 2, 'color', 21, null,
0b0100, 2, 'width', 20, null,
0b1000, 2, 'height', 22, '999px',
];
Improvements...
- Styling is handled across the template and directives / components
- Debugging support
- Animations...
<div
#ref
[style]="myStyles"
[style.width]="width"
[style.height]="height"
[class]="{active:true}"
[class.loading]="loading"
transform-item-directive>
...
</div>
window.ng.debugStyles(ref);
// {
// bindings: ['width', ...]
Prioritization
- what happens when multiple style/class props collide?
- Which one wins?
- The order is
- Template bindings
- Then Directive
- And Component
- Finally static
<width-comp
[style]="{width:200px}"
[style.width]="w"
dir-that-sets-width
another-dir-that-sets-width>
What is the width!!!?
</width-comp>
1. [style.width]="w"
2. [style]="{width:200px}"
3. dir-that-sets-width
4. another-dir-that-sets-width
5. width-comp
If nothing is found then either the
static `width` value (`style=""`)
is applied or just `null`.
New Features
- Directives / Components can now have dynamic styles / classes
- A [style] or [class] binding can be added via @HostBinding() or @Directive.host
- All style values can be tested and debugged via helper functions
<width-comp
[style]="{width:200px}"
[style.width]="w"
dir-that-sets-width
another-dir-that-sets-width>
What is the width!!!?
</width-comp>
// this API might change (pseudocode for now)
import {styleDebug} from '@angular/core/testing';
it('should have the correct value for `width`', () => {
// create the element and
// component in your test first
const styles = styleDebug(myTargetElement);
expect(styles.values['width']).toEqual(expectedWidth);
expect(styles.summary['width']).toEqual({
value: expectedWidth,
defaultValue: null,
bindingValues: [,
/* each of the binding values for the element */
]
});
});
// this API might change (pseudocode for now)
const styles = window.ng.debugStyles(targetElement);
console.log(styles.values['width'])
/* => expectedWidth; */
console.log(styles.summary['width']) /* => {
value: expectedWidth,
defaultValue: null,
bindingValues: [,
// each of the binding values for the element
]
}
*/
Animations
- All classes and styles are now known to Angular
- This means that animating the change in styling is more controlled
- Why not just animate the difference in styling automatically?
<!--
templateFn for my-component.ts
-->
<div class="modal-box"
[style.width]="width"
[style.height]="height"
[class.disabled="isDisabled">
My Fancy Modal Box
</div>
<!--
notice the `animate` pipe
-->
<div
class="modal-box"
[style.width]="width | animate:1000"
[style.height]="height | animate:1000"
[class.disabled="isDisabled | animate:1000">
My Fancy Modal Box
</div>
AnimatePipe
- Works on any style or class binding
- Also works on `[style]` and `[class]` bindings
- multiple classes and styles at the same time
- allows for animation directives
When's it all here?
- The styling refactor is 90% done
- Last PR is waiting on review from Misko
- Is apart of v9
- AnimatePipe is still being experimented with...
</style>
The Grand Styling Refactor in Ivy
By Matias Niemelä
The Grand Styling Refactor in Ivy
- 214