Using predictable style bindings
Angular has many ways to bind styles and classes to Document Object Model (DOM) elements. Ivy introduces predictable style bindings because of a precedence ruleset that covers all of Angular's style binding APIs except for the NgClass
and NgStyle
directives.
Template element bindings have higher priority than directive host bindings, which have higher priority than component host bindings. Binding of individual Cascading Style Sheets (CSS) classes and style properties have higher priority than binding maps of class names and style properties. Binding values that define the full class
or style
attributes have even lower priority. The NgClass
and NgStyle
directives override all other bindings on every value change.
Bottom values in style bindings are treated differently. Binding undefined
will defer to lower-priority bindings, while null
will override bindings with lower priority.
Let's look at the following example:
@Component({ selector: 'app-root', template: ` <app-host-binding [ngStyle]="{ background: 'pink' }" [style.background]="'red'" [style]="{ background: 'orange' }" style="background: yellow;" appHostBinding ></app-host-binding> `, }) class AppComponent {} @Directive({ host: { '[style.background]': "'blue'", style: 'background: purple;', }, selector: '[appHostBinding]', }) class HostBindingDirective {} @Component({ host: { '[style.background]': "'gray'", style: 'background: green;', }, selector: 'app-host-binding', }) class HostBindingComponent {}
In the preceding code example, we see components and a directive using many different types of style bindings. Despite this, it will output only a single style rule to the DOM for the <app-host-binding>
element. The background color of this rule will be evaluated as pink
.
The order in which the background colors are applied is shown here, with the highest precedence first:
- Pink (
NgStyle
directive binding) - Red (template property binding)
- Orange (template map binding)
- Yellow (static style value)
- Blue (directive host property binding)
- Purple (static directive host style binding)
- Gray (component host property binding)
- Green (static component host style binding)
As seen in the example, the order in which the bindings are mentioned in templates and metadata options does not matter—the precedence ruleset is always the same.
Having predictable style bindings makes it easier to implement complex use cases in our applications. It is worth mentioning that another reason for introducing this breaking change is that Ivy does not guarantee the order in which data bindings and directives are applied.
In this section, we witnessed the following styling precedence rules in effect, from highest priority to lowest:
- Template property bindings
- Template map bindings
- Static template class and style values
- Directive host property bindings
- Directive host map bindings
- Static directive host class and style bindings
- Component host property bindings
- Component host map bindings
- Static component host class and style bindings
The order in which style bindings are listed in code only matters if two bindings share the same precedence, in which case the last one wins.
NgClass
and NgStyle
directive bindings override all other style bindings. They are the !important
equivalents of Angular style bindings.
Now that we can predict how multiple style bindings and values affect our user interface (UI), let's look at how we can use class inheritance to share directive and component metadata.
Sharing metadata through directive and component inheritance
Angular Ivy changes directive and component inheritance in a more explicit but predictable manner, which allows the bundle size and compilation speed to decrease.
When a base class is using any of the following Angular-specific features, it has to have a Directive
or Component
decorator applied:
Dependency
orAttribute
injectionInput
orOutput
propertiesHostBinding
orHostListener
bindingsViewChild
,ViewChildren
,ContentChild
, orContentChildren
queries
To support this, we can add a Directive
decorator without any options. This conceptually works like an abstract directive and will throw a compile-time error if declared in an Angular module.
We could make the base class abstract
, but that would cause us to have to extend it to test it, so it is a trade-off.
By extending base directives, we can inherit the inputs
, outputs
, host
, and queries
metadata options. Some of them will even be merged if declared both in the subclass and base class.
Components are able to inherit the same metadata options from their base class but are unable to inherit styles
and template
metadata. It is possible to refer to the same styleUrls
and templateUrl
though.
Let's write some example components that share behavior through a base class. First, we will create a base search component, as seen in the following code snippet:
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; @Component({ selector: 'app-base-search', template: '', }) export class BaseSearchComponent { #search = new EventEmitter<string>(); @Input() placeholder = 'Search...'; @Output() search = this.#search.pipe(debounceTime(150), distinctUntilChanged()); onSearch(inputEvent: Event): void { const query = (inputEvent.target as HTMLInputElement)?.value; if (query == null) { return; } this.#search.next(query); } }
The base search component has an event handler that handles input events representing a search query. It debounces searches for 150
milliseconds (ms) and ignores duplicate search queries before outputting them through its search
output property. Additionally, it has a placeholder
input property.
Next, we will create a simple search box component that inherits from the base search component, as seen in the following code snippet:
import { Component } from '@angular/core'; import { BaseSearchComponent } from './base-search.component'; @Component({ selector: 'app-search-box', styleUrls: ['./base-search.scss'], template: ` <input type="search" [placeholder]="placeholder" (input)="onSearch($event)" /> `, }) export class SearchBoxComponent extends BaseSearchComponent {}
The search box component uses base search styles and can add its own component-specific styles if it needs to. The <input>
element in its component template binds to the placeholder
input property it inherits from the base search component. Likewise, the input
event is bound to the onSearch
event handler it inherits.
Let's create another component that inherits from the base search component. The following code block lists the suggested search component:
import { Component, Input } from '@angular/core'; import { BaseSearchComponent } from './base-search.component'; @Component({ selector: 'app-suggested-search', styleUrls: ['./base-search.scss'], template: ` <input list="search-suggestions" [placeholder]="placeholder" (input)="onSearch($event)" /> <datalist id="search-suggestions"> <option *ngFor="let suggestion of suggestions" [value]="suggestion"> {{ suggestion }} </option> </datalist> `, }) export class SuggestedSearchComponent extends BaseSearchComponent { @Input() suggestions: readonly string[] = []; }
In addition to the inherited input property, placeholder
, the suggested search component adds a suggestion
input property, which is a list of search query suggestions. The component template loops over these suggestions and lists them as <option>
elements in a <datalist>
element that is tied to the <input>
element.
Similar to the search box component, the suggested search component binds to the onSearch
event handler and the placeholder
input property. It also uses the base search styles.
Important Note
As seen in these examples, we do not have to add duplicate constructors in subclasses to enable constructor injection.
Directive and component metadata is the special glue that ties TypeScript classes to the DOM through component templates and data binding. Through classical object-oriented programming (OOP) patterns, we are familiar with sharing properties and methods through class inheritance.
In this section, we learned how Ivy has enabled us to share metadata in a similar, predictable way through metadata-enabled class inheritance.
The next topic we are going to explore is AOT compilation. Ivy heralds the era of AOT compilation everywhere in Angular applications.