Modern [styling] with Angular (ivy)

http://

  yom.nu/
    modern-     

    styling-ivy

@yearofmoo

github.com/matsko

www.yearofmoo.com

Matias Niemelä

How to add
[styling] to
Application code?

<div style="...">

Used to apply inline styles to an element.

 

With Angular there are various ways to do this using bindings.

<div class="...">

Used to add, remove and toggle classes on a DOM element.

 

With Angular there are various ways to do this using bindings.

Styling in HTML

style bindings

style="width: 100px"

style="width: {{ 100 }}px"

[ngStyle]="{width:'100px'}"

[style.width.px]="100"

 

renderer.setStyle('width', '100px')

element.style.width="..."

 

[attr.style]="'width: 100px'"

class bindings

class="foo"

class="{{ 'foo' }}"

[ngClass]="{foo:true}"

[class.foo]="true"

 

renderer.addClass('foo')

element.classList.add('foo')

 

[attr.className]="'foo'"

style bindings

style="width: 100px"

style="width: {{ 100 }}px"

[ngStyle]="{width:'100px}"

[style.width.px]="100"

 

renderer.setStyle('width', '100px')

element.style.width="..."

 

~ [attr.style]="'width: 100px'"

class bindings

class="foo"

class="{{ 'foo' }}"

[ngClass]="{foo:true}"

[class.foo]="true"

 

renderer.addClass('foo')

element.classList.add('foo')

 

~ [attr.className]="'foo'"

How things are evaluated?

Style Class Description
style="..." class="..." Handled at compile time
style="{{ x }}" class="{{ x }}" Compile + Runtime change for entire value through the renderer
[style.value]="x" [class.name]="x" Runtime binding that toggles the value through the renderer
[attr.style]="x" [attr.className]="x" Rewrites the entire value through setAttribute
[ngStyle] [ngClass] Special Directives

What about @animations?

  • Animation @triggers also have styling
  • Single and Multiple element levels of element styling
  • This is just another layer of styling to consider with style and class values
<div
  style="width:100px"
  [style.width]="w1"
  [ngStyle]="{width:w2}"
  [@widthAnim]="state">
  
  The width gets
  updated in the
  animation as well!

</div>

It's not very obvious...

<div style="width: 100px; height:200px"
     [ngStyle]="{width:w1}"
     [style.height.px]="h1"
     [@expandCollapse]="state"
     class="ready {{ active ? 'active' : '' }}"
     [class.disabled]="disabledExp"
     [ngClass]="{ ready: false }"
     [attr.style]="editMode
        ? generateEditStyles() : ''">
  ...
  How does all this function?
  ...
</div>

Angular Ivy [styles] it Better

New Styling Improvements 

  • [style] / [class] instead of [ngStyle] / [ngClass]
  • Better designed diff'ing algorithm
  • Improved memory management
  • Everything keeps up state
  • Sanitization support
  • Animation support

Styling in Angular Ivy

- [style] and [class] are the future

- Or [style.prop] and [class.name] still do work

- style="{{ x }}" and class="{{ y }}" still do work

- [style], [style.prop], [class], [class.prop] are in core

<!-- styles are handled together -->
<div style="width:100px"
     [style]="{width:'200px'}"
     [style.width]="
       closed ? 0 : null">

<!-- classes too -->
<div class="
  active {{loading?'loading':''}}"
     [style]="{active:false}"
     [style.active]="isActive">

The New Diffing Algorithm

- Template compiler produces instructions

- Instructions read expression values

- values are compared / evaluated

- ONLY the changed values are applied

- No function closures, no intermediate data structures

<!-- template code -->
<div style="width:100px"
     [style]="x1"
     [style.width]="x2"
     [class.active]="c1">

<!-- AOT template code -->
if (flags & 1) {
  elementStart(0)
  elementStyling(['active'],
    ['width', 0, 'width', '100px'])
  elementEnd();
}
if (flags & 2) {
  elementStylingMap(0, null, ctx.x1);
  elementStyleProp(0, 0, ctx.x2);
  elementClassProp(0, 0, ctx.c1);
  elementStylingApply();
}

The StylingContext

- Every element has a styling context array

- All classes/styles are stored here for diffing

- Uses bit shifting to allow for efficient lookups

- No key/value maps are ever used

- No classes are used for style/class storage

// [style]="{width}", [class.active], style="width:100px"
StylingContext = [
 // configuration values
 null, null, [null, null, '100px'],
 MASTER_FLAG, 1, 0, null, null,

 // width map value
 FLAG, 'width', x1, null,

 // active class map value
 FLAG, 'active', null, null,

 // width prop value
 FLAG, 'width', x2, null,

 // active class prop value
 FLAG, 'active', c1, null
];
//
// x2 is changed to '200px'
// <div [style.width]="x2"
//
StylingContext = [
 // ...
 ~MASTER_FLAG, //...

 // width map value
 FLAG, 'width', x1, null,

 //...

 // width prop value
 ~FLAG, 'width', x3, null, // FLAG is set to dirty

 //...
];
// the FLAG values contain the data

// 32 bit number
FLAG =
  // initial value index addr (13 bits)
  0000000000000
  // map/prop value index addr (13 bits)
  0000000000000
  // control flags (5 bits)
  00000;

initialIndex = (FLAG >> 13) & ob1111111111111;
mapOrPropIndex = (FLAG >> 26) & ob1111111111111;
config = FLAG & 0b11111;
function elementStylingApply() {
  // loops over the entire array
  for (let i = VALUES_START_INDEX;
    i < context.length; i++) {

    if (FLAG & DIRTY) {
      if (FLAG & CLASS_BASED) {
        addClass(element, 'active');
      } else {
        setStyle(element, 'width', c2);
      }
      FLAG &= ~DIRTY;
    }
  }
}

Code Complexity

Operation Normal Case Worst Case
elementStyling (TNode) o(k) o(k)
elementStyling (LNode) o(k) o(k)
elementStyleMap o(k) * o(n^2)
elementStyleProp o(1) o(n-k)
elementClassProp o(1) o(n-k)
elementStylingApply o(0) or o(k) o(n)

n = total styling values / k = size of map or changes

 

* = this only happens once

Code Complexity

Operation Normal Case Worst Case
initial render o(k) o(n^2)
re-render (no changes) o(1) o(1)
re-render (changes) o(k) o(n)

n = total styling values / k = size of map or changes

 

Perf: NgClass vs [class]

Operation [ngClass] [class]
initial render ~100% ~100%
re-render (no changes) ~100% ~ 50%
re-render (changes) ~100% ~ 50%

n = total styling values / k = size of map or changes

 

Memory Consumption

Operation NgClass [class]
initial render 100% 50%
re-render (no changes) 100% 50%

n = total styling values / k = size of map or changes

 

([style] && [class]) + animations

Animations == styles / time

- Animations are just styles occurring over time

- They should act as an extension to styling

- [style] and [class] bindings should animate easily

@Component({
  styles: [`
    .box { height: 200px; }
  `],
  template: `
    <header (click)="toggle()">{{ title }}</header>
    <div class="box" [style.height]="
      isOpen ? '0px' : null">

      I will open and close
    </div>
  `
})
class OpenClosePanel {
  title = 'OpenClose';
  toggle() {
    this.isOpen = !this.isOpen;
  }
}
<!-- simple style transitions -->
<div class="box" [style.height]="
  (isOpen ? '0px' : null)
     | animate:'300ms ease-out'">

  I will open and close
</div>
<div
  style="width:100px; height:100px"
  [style]="{width:w} | animate:'300ms ease-out'"
  [style.opacity]="o | animate:'500ms ease-out'"
  [class]="{active:active}|animate:1000"
  [class.disabled]="
    isDisabled | animate:'300ms 0.5s cubic-bezier()">
 ... this element sure knows how to animate
</div>

Features

- Works on any style and class

- Works with media queries

- Automatically computes heights / widths

- Fully dynamic

- Animations can be lazy loaded

@angular/animations

  • Working to get @triggers to work
  • [class] / [style] + the animation DSL
  • Timelines

Still TODO

  • Final fixes for `:host` bindings
  • AnimatePipe in master next week
  • CSS Keyframe support
  • Better support for transforms
  • Debugging Tools

Thank you!

Modern [styling] with Angular (ivy)

By Matias Niemelä

Modern [styling] with Angular (ivy)

  • 412