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>

Made with Slides.com