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