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)
- 523
 
   
   
  