Template syntax for property bindings

Property bindings in Angular templates use the square bracket notation []. This syntax works uniformly for both child components and native DOM elements. Consider a parent component A that needs to pass data to a child component b-comp and a native span element:

import { Component } from '@angular/core';

@Component({
  moduleId: module.id,
  selector: 'a-comp',
  template: `
      <b-comp [textContent]="AText"></b-comp>
      <span [textContent]="AText"></span>
  `
})
export class AComponent {
  AText = 'some';
}

For the native element, no additional setup is required beyond the binding itself. However, the child component must declare that it accepts the textContent property through its decorator metadata:

@Component({
    selector: 'b-comp',
    template: 'Comes from parent: {{textContent}}'
})
export class BComponent {
    @Input() textContent;
}

When the AText property on the parent changes, Angular automatically propagates the new value to both the B component’s textContent input and the span element’s property. Additionally, the child component’s ngOnChanges hook gets invoked.

The mechanism by which Angular validates bindings differs between elements and components. For DOM elements, the compiler consults the dom_element_schema_registry to verify the property exists. For components and directives, Angular checks the class metadata to see if the bound property is declared in the input decorator property list. When a binding fails this validation, the compiler produces an error:

Can’t bind to ‘text’ since it isn’a known property of …

This behavior is well documented, so let’s shift focus to the internal implementation.

Factory configuration details

A crucial insight is that even though bindings are declared on the child B component and the span, all the information needed for updating these inputs is stored in the parent A component’s factory. Here is what the generated factory for the A component looks like:

function View_AComponent_0(_l) {
  return jit_viewDef1(0, [
     jit_elementDef2(..., 'b-comp', ...),
     jit_directiveDef5(..., jit_BComponent6, [], {
         textContent: [0, 'textContent']
     }, ...),
     jit_elementDef2(..., 'span', [], [[8, 'textContent', 0]], ...)
  ], function (_ck, _v) {
     var _co = _v.component;
     var currVal_0 = _co.AText;
     var currVal_1 = 'd';
     _ck(_v, 1, 0, currVal_0, currVal_1);
  }, function (_ck, _v) {
     var _co = _v.component;
     var currVal_2 = _co.AText;
     _ck(_v, 2, 0, currVal_2);
  });
}

If you’ve read the earlier referenced articles, the view nodes in this factory should look familiar. The first two definition nodes, jit_elementDef2 and jit_directiveDef5, represent the element and directive definitions that form the B component. The third node is the element definition for the span.

Bindings defined on node definitions

What stands out in this factory compared to others you may have encountered are particular parameters passed to these node definitions. Our jit_directiveDef5 receives a new parameter here:

jit_directiveDef5(..., jit_BComponent6, [], {
    textContent: [0, 'textContent']
}, ...),

This parameter, named props, matches the props parameter defined in the directiveDef function’s signature:

directiveDef(..., props?: {[name: string]: [number, string]}, ...)

This prop object uses keys to map binding indices to the target property names. In our example, there’s a single binding for textContent:

{textContent: [0, 'textContent']}

Should the directive have multiple bindings, such as in this template:

<b-comp [textContent]="AText" [otherProp]="AProp">

the props object would then include two entries:

jit_directiveDef5(49152, null, 0, jit_BComponent6, [], {
    textContent: [0, 'textContent'],
    otherProp: [1, 'otherProp']
}, null),

During view node creation, Angular uses these props values to generate bindings. Each binding determines the operation type Angular will use during change detection and supplies contextual data. The binding type is defined through binding flags. For property updates, the compiler sets these flags for each binding:

export const enum BindingFlags {    TypeProperty = 1 << 3,

Since we also have bindings on the span element, the compiler generates props parameters for the span’s element definition too:

jit_elementDef2(..., 'span', [], [[8, 'textContent', 0]], ...)

Element definitions take a slightly different props structure — an array of props. With just one input binding, the span has a single child array. The first number in this array identifies the operation type, which in this case is property update:

export const enum BindingFlags {
    TypeProperty = 1 << 3, // 8

Other possible operation values, detailed in The mechanics of DOM updates in Angular, include:

TypeElementAttribute = 1 << 0,
TypeElementClass = 1 << 1,
TypeElementStyle = 1 << 2,

The compiler omits operation type in the props for directive definitions because directives can only have property updates — all bindings are implicitly BindingFlags.TypeProperty.

Update directives and update renderer functions

The compiler generated two distinct functions within the factory:

function (_ck, _v) {
    var _co = _v.component;
    var currVal_0 = _co.AText;
    var currVal_1 = _co.AProp;
    _ck(_v, 1, 0, currVal_0, currVal_1);
},
function (_ck, _v) {
    var _co = _v.component;
    var currVal_2 = _co.AText;
    _ck(_v, 2, 0, currVal_2);
}

The second function, updateRenderer, should be familiar from the article on DOM updates. The new one is updateDirectives. Both functions conform to the ViewUpdateFn interface and are both attached to the view definition:

interface ViewDefinition {
  flags: ViewFlags;
  updateDirectives: ViewUpdateFn;
  updateRenderer: ViewUpdateFn;

The function bodies are remarkably similar. Each receives two parameters, _ck and v, referring to the same entities in both cases. Why then have two separate functions?

The reason lies in the two distinct phases during change detection:

  • updating input properties on child components
  • updating DOM elements for the current component

Each operation happens at a different point in the change detection cycle for a component. Thus Angular defines two functions, each targeting specific node types and invoked at different stages:

  • updateDirectives — handles updates for directiveDef nodes, called early in the check
  • updateRenderer — handles updates for elementDef nodes, called midway through the check

During every change detection pass on a component, the framework invokes both functions with parameters supplied by the change detection mechanism. With each invocation, the _ck parameter is short for check and points to the prodCheckAndUpdate function. The v parameter passes the component’s view containing its nodes. These functions fetch the bound property’s current value from the component instance, then call _ck with the view, node index, and the retrieved value. The nodeIndex identifies which view node requires the change detection check.

Notably, DjangoElement updates occur per view node — hence the requirement for the index. If the template contained two spans and two directives:

<b-comp [textContent]="AText"></b-comp>
<b-comp [textContent]="AText"></b-comp>
<span [textContent]="AText"></span>
<span [textContent]="AText"></span>

the generated updateRenderer and updateDirectives function bodies would look like:

function(_ck, _v) {
    var _co = _v.component;
    var currVal_0 = _co.AText;
    
    // update first component
    _ck(_v, 1, 0, currVal_0);
    var currVal_1 = _co.AText;
    
    // update second component
    _ck(_v, 3, 0, currVal_1);
}, 
function(_ck, _v) {
    var _co = _v.component;
    var currVal_2 = _co.AText;
    
    // update first span
    _ck(_v, 4, 0, currVal_2);
    var currVal_3 = _co.AText;

    // update second span
    _ck(_v, 5, 0, currVal_3);
}

There’s limited logic within these generated functions; the core functionality resides elsewhere. Let’s explore what happens beyond these wrappers.

Property updates on DOM elements

Earlier we established that the compiler-generated updateRenderer function is invoked during change detection to update inputs on DOM elements. The _ck function it receives references checkAndUpdate. This compact, generic function orchestrates a series of calls that ultimately invoke checkAndUpdateElement. Its primary role is to distinguish between Angular’s special binding forms, such as [attr.name, class.name, style.some], and standard node-specific properties:

case BindingFlags.TypeElementAttribute -> setElementAttribute
case BindingFlags.TypeElementClass     -> setElementClass
case BindingFlags.TypeElementStyle     -> setElementStyle
case BindingFlags.TypeProperty         -> setElementProperty;

This is where the bindings explored earlier become relevant. Since the bindings were set to BindingFlags.TypeProperty, the setElementProperty function is employed. This function, in turn, delegates to the renderer’s setProperty method to apply the update to the DOM element.

How directives receive property updates

While the updateRenderer function from the earlier part handles DOM updates, the compiler also generates a companion updateDirective function in the component factory. This one is dedicated to assigning new values to input properties on components. Like its sibling, it receives the _ck argument during change detection, which points to the checkAndUpdate utility. The difference lies in the specific helper invoked — here it's checkAndUpdateDirective — because the target nodes carry the NodeFlags.TypeDirective marker. The routine performs several distinct steps:

  1. extracts the component or directive instance from the view node
  2. determines whether the bound property value has actually changed
  3. if a change is detected:
    a. writes the new value onto the corresponding class property
    b. constructs SimpleChange metadata and refreshes oldValues
    c. flips the state to checksEnabled when the component opts into OnPush
    d. triggers the ngOnChanges hook
  4. invokes ngOnInit during the initial check of the view
  5. fires ngDoCheck

Naturally, each lifecycle method is only executed when the class actually defines it. Angular relies on the nodeDef flags mentioned earlier to decide this, as demonstrated in the following excerpt:

if (... && (def.flags & NodeFlags.OnInit)) {
  directive.ngOnInit();
}
if (def.flags & NodeFlags.DoCheck) {
  directive.ngDoCheck();
}

Before any DOM refresh takes place, all prior values are preserved on the view through the oldValues array. That completes the mechanism.