53

One-way property binding mechanism in Angular

 5 years ago
source link: https://www.tuicool.com/articles/hit/M7nQZzU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

D ata binding is one of the most important features in Angular. Data binding in Angular works by synchronizing the data in the components with the UI so that it reflects the current value of the data. To achieve the synchronization of the View and the Model, Angular uses change detection.

It detects when a value in the data changes and updates the View to reflect the changes, thus making the HTML dynamic!!

In Angular, there are two types of data binding, one-way data binding, and two-way data binding. In one-way data binding, the template expression {{}} and square braces [] are used to bind a property to the DOM.

In this post, we will be concerned with the square braces one-way binding method which can also be called property binding because it data-binds a data to the property of an element.

This post provides you with an in-depth knowledge of how Angular updates properties on directives/elements and runs a DOM re-render to reflect new changes in the directives/elements, thus providing a one-way communication.

Property binding

Property binding is the primary way of binding data in Angular. The square braces are used to bind data to a property of an element, the trick is to put the property onto the element wrapped in brackets: [property] .

class {
    this.srcURL = "http://pexels/image.jpg"
}
<img [src]="srcURL" />
    |
    |
<img src="http://pexels/image.jpg" />

The src property of the HTMLElement img is bound to the srcURL property of the class. Whenever the srcURL property changes the src property of the img element changes.

Also, property binding could be used to pass data to a component. Let’s say you want to pass data to people component, we will do something like this:

@Component({
  selector: 'app-root',
  template: `
➥  <people [person]="title"></people>
    <button (click)="changeTitle()">ChngTitle</button>
  `,
  styles: []
})
export class AppComponent {
  title = 'app';
  constructor(){}
changeTitle() {
    this.title = 'Angular app'
  }
}

The people element is a Component so Angular will evaluate title and pass it into the people component via person attribute. Angular components can receive values from the outside world and can send values to the outside world. If an Angular component needs to receive values from the outside world, you can bind the producers of these values to the corresponding inputs of the component.

For people component to receive we have to decorate the person property with a @Input decorator.

@Component({
    selector:'people',
    template: `
        {{person}}
    `
})
export class PeopleComponent {
➥  @Input()person
}

Now, whenever the title property from the parent component ( AppComponent ) changes, the changed is communicated to the child component ( PeopleComponent ) via the person property binding and the changes is reflected on the DOM. This is also called Parent-Child communication which is a one-way event. If it was Child-Parent communication it would be two-way communication.

property binding uses the square brackets to indicate to Angular the property of an element that should be updated when the data-bound variable changes. The element could be an HTMLElement ( span , h1 , input , etc) or a Component element (a custom element defined using the @Component decorator).

@Component({
    selector: 'app-root',
    template: `
1.➥      <img [src]="srcURL" />
2.➥      <people [person]="title"></people>
    `
})

If the element is an HTMLElement ( 1. ), the property of the HTMLElement is updated on the DOM. If it is a Component element ( 2. ), the property of the Component 's class is updated. Both are Parent-Child communication, Parent comms to the Child img element the value of its src property also, Parent comms to child Component element people how to update its class property.

OK, we have seen how property binding works, let’s peep internally how Angular manages to pull this off.

NB: I will be majorly showing property binding in a Component element, then, later on, I’ll show property binding in HTMLElement. It will be easier to comprehend considering that we have just dealt with a more complex one.

In the beginning…

Angular components are compiled down to factories before being executed by the framework. The factories are created from the data supplied to the @Component decorator. The factories are made of Views which Angular uses to manipulate the DOM. The View is made up of nodes: element node, text node, directive node etc. Each node is constructed and created by a specialized function with all the information needed to create the node so as to optimize for speed.

Let’s we have a component like this :

@Component({
    selector: 'app-root',
    template: `
        <div>I am {{title}}</div>
    `
})
export class AppComponent {
    title: string = 'AppComponent'
}

When compiled its factory will look like this:

function View_AppComponent_0(_l) {
    return ɵvid(0, [
        (_l()(), ɵted(0, null, ["I am "])),
        (_l()(), ɵeld(-1, null, "div")),
        (_l()(), ɵted(-1, null, ["\n  "]))
    ],
    // update directives
    function(_ck, _v) {
        var _co = _v.component;
        var currVal_0 = _co.title;
        _ck(_v, 0, 0, currVal_0);
    } , null);
}
function View_AppComponent_Host_0(_l) { return ɵvid(0, [(_l()(), ɵeld(0, 0, null, null, 1, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent)), ɵdid(1, 49152, null, 0, AppComponent, [], null, null)], null, null); }
var AppComponentNgFactory = ɵccf("app-root", AppComponent, View_AppComponent_Host_0, {}, {}, []);

This describes the view structure of AppComponent. The view is constructed from View_AppComponent with View_AppComponent_Host being the host view, where the main view will be rendered. The ɵvid function takes in different parameters with the NodeDef array being the most useful. NodeDef is used by Angular to classify DOM nodes: element , text , directive , etc.

Looking at the array, we see that each node is created by different functions, element node div is created by ɵeld and text node I am by ɵted . Notice it has a plethora of other parameters that provide the function with the information required to instantiate the nodes.

To learn more about component factories, NgModule factories and DOM manipulations in Angular, here are a few links:

I’ll take it that you have gone through the above articles thoroughly and understood how Angular works under the hood, at least at a basic level.

Our App and Component Factories

We have seen above that components are compiled to factories before execution. Now, let’s look at our components:

@Component({
    selector:'people',
    template: `
        {{person}}
    `
})
export class PeopleComponent {
    @Input()person
}
@Component({
  selector: 'app-root',
  template: `
    <people [person]="title"></people><button (click)="changeTitle()">ChngTitle</button>
  `,
  styles: []
})
export class AppComponent {
  title = 'app';
  constructor(){}
changeTitle() {
    this.title = 'Angular app'
  }
}

Like we mentioned earlier, the title property of AppComponent is bound to the people’s person property. PeopleComponent uses @Input to receive the title value through the person property.

Whenever title changes, the change is passed to PeopleComponent via its person attribute. The PeopleComponent’s view is updated if the @Input person is bound to the view.

Let’s look at their component factories to determine what happens beneath:

function View_PeopleComponent_0(_l) {
    return i0.ɵvid(0, [
        (_l()(), i0.ɵted(0, null, ["\n        ", "\n    "]))
    ], null, function(_ck, _v) {
        var _co = _v.component;
        var currVal_0 = _co.person;
        _ck(_v, 0, 0, currVal_0);
    });
}
function View_PeopleComponent_Host_0(_l) {
    return i0.ɵvid(0, [
        (_l()(), i0.ɵeld(0, 0, null, null, 1, "people", [], null, null, null, View_PeopleComponent_0, RenderType_PeopleComponent)),
        i0.ɵdid(1, 49152, null, 0, i1.PeopleComponent, [], null, null)
    ], null, null);
}
var PeopleComponentNgFactory = i0.ɵccf("people", i1.PeopleComponent, View_PeopleComponent_Host_0, { person: "person" }, {}, []);
function View_AppComponent_0(_l) {
    return i0.ɵvid(0, [
        (_l()(), i0.ɵted(-1, null, ["\n    "])),
        (_l()(), i0.ɵeld(1, 0, null, null, 1, "people", [], null, null, null, View_PeopleComponent_0, RenderType_PeopleComponent)),
        i0.ɵdid(2, 49152, null, 0, i1.PeopleComponent, [], { person: [0, "person"] }, null),
        (_l()(), i0.ɵeld(3, 0, null, null, 1, "button", [], null, [
            [null, "click"]
        ], function(_v, en, $event) {
            var ad = true;
            var _co = _v.component;
            if (("click" === en)) {
                var pd_0 = (_co.changeTitle() !== false);
                ad = (pd_0 && ad);
            }
            return ad;
        }, null, null)),
        (_l()(), i0.ɵted(-1, null, ["ChngTitle"])),
        (_l()(), i0.ɵted(-1, null, ["\n  "]))
    ], function(_ck, _v) {
        var _co = _v.component;
        var currVal_0 = _co.title;
        _ck(_v, 2, 0, currVal_0);
    }, null);
}
function View_AppComponent_Host_0(_l) {
    return i0.ɵvid(0, [
        (_l()(), i0.ɵeld(0, 0, null, null, 1, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent)),
        i0.ɵdid(1, 49152, null, 0, i1.AppComponent, [], null, null)
    ], null, null);
}
var AppComponentNgFactory = i0.ɵccf("app-root", i1.AppComponent, View_AppComponent_Host_0, {}, {}, []);

Most of these should be familiar. The *NgFactory variables are used to dynamically create and append components’ view on the DOM. The View_*_Host_0 functions are used to create host views of components and View_*_0 functions are used to generated views for components, they define the HTML feel of a component.

We will also notice methods prefixed with ɵ , these are functions exported from the @angular/core . ɵvid refers to a viewDef function which creates a ViewDefinition from information passed to it. Looking above, we see that viewDef takes 4 parameters:

export function viewDef(
    flags: ViewFlags, nodes: NodeDef[], updateDirectives?: null | ViewUpdateFn,
    updateRenderer?: null | ViewUpdateFn): ViewDefinition {...}

flags param denotes the kind of change detection strategy to use by the view. nodes array contains the DOM composition of the view, on compiling a component's view, the Angular compiler assigns each DOM node (element, text) a separate index in an array. This is useful so an operation can be easily carried out on a separate node without affecting other nodes.

updateDirectives is a function arg used to update directive's properties and updateRenderer param updates DOM elements on a component. Other functions are ɵeld which refers to elementDef, this is used to create element nodes. ɵted refers to textDef which is used to create text nodes and ɵdid , directiveDef which is used to create a directive node.

The mechanics

You know this is a Parent-Child communication. The Parent AppComponent uses person property in the child class PeopleComponent to send it the value of the title property. We can explain it further like this. Let's say we have two classes:

class Child {
    person:string
    public displayView() {
        console.log(`${this.person}`)
    }
}
class Parent {
    title:string
    constructor(private child: Child) {
        this.title = 'app'
    }
    public changeTitle(newTitle: string) {
        this.title = newTitle
    }
    private updateDirective() {
        this.child.person = this.title        
    }
    public tick() {
        this.updateDirective()
        this.child.displayView()
    }
}

We have Parent and Child classes. Parent takes the instance of Child on instantiation. It has methods: changeTitle, which changes the its property title with a new title; updateDirective, updates the person property of the Child class with its updated title property and tick method, which first updates the Child property person via updateDirective() and calls the displayView method of Child which displays the value of its person property on console.

This is much like what we did in Angular, the Parent class comms to the Child class through the person property. If its title property changes this.title = newTitle , it changes the Child's person value this.child.person = this.title with the new title value.

Let’s run the program:

const parent = new Parent(new Child())
parent.tick() // Displays `app`
parent.changeTitle('Angular app')
parent.tick() // Displays `Angular app`
parent.changeTitle('Very Funny')
parent.tick() // Displays `Very Funny`

You see we have bound the title property of Parent class to the person property of the Child class ( [property binding] ). The Parent class changes its title property by calling the changeTitle function. It updates the Child class's person property with the new value of its title property because the person property is bound to the title property.

With this, the Child class's person property has the same value as Parent's title property, thus a one-way communication. It's now left to the Child what it does with the value of the person property, the important thing is that Parent-Child communication has been achieved. In our case, we just display the updated property, displayView() .

Whenever the property-bound variable of the Parent class changes, it must be communicated to the Child class —  Chidume Nnamdi

This is exactly what happens in Angular’s one-way property binding.

1:The instance of the directive/component class PeopleComponent is created using NodeDef object created by this:

1.➥ i0.ɵdid(2, 49152, null, 0, i1.PeopleComponent, [], { person: [0, "person"] }, null),

This is synonymous to this const parent = new Parent(new Child()) in our Parent-Child demo.

2:If the property of the property-bound value in the parent class changes it must be communicated to the child’s class property. When a property in a class changes due to an async operation, Angular runs a change detection through the tick method in ApplicationRef class.

This tick method updates the directives and elements properties through the updateDirective and updateRenderer function arg in Component factories. The updateDirective in parent component updates the instance of the child component PeopleComponent with the new value of the property-bound variable title:

function View_AppComponent_0(_l) {
    return i0.ɵvid(0, [
    ...
1.➥   i0.ɵdid(2, 49152, null, 0, i1.PeopleComponent, [], { person: [0, "person"] }, null),
    ...
2.➥    function(_ck, _v) {
        var _co = _v.component;
        var currVal_0 = _co.title;
        _ck(_v, 2, 0, currVal_0);
    }, null);
}

In our Parent-Child, we update the directive:

private updateDirective() {
        this.child.person = this.title        
    }

whenever the parent’s class property value changes

public changeTitle(newTitle: string) {
        this.title = newTitle
    }

still in sync with Angular.

3:The child component now has the new value, like we said it can now do anything it wants with the value. The most important thing is that the communication passed successfully. In our case, we display the property-bound variable on the DOM using updateRenderer function in PeopleComponent 's component factory:

function View_PeopleComponent_0(_l) {
    return i0.ɵvid(0, [
        (_l()(), i0.ɵted(0, null, ["\n        ", "\n    "]))
    ], null, 
3.➥  function(_ck, _v) {
        var _co = _v.component;
        var currVal_0 = _co.person;
        _ck(_v, 0, 0, currVal_0);
    });
}

In our demo, we displayed the value on console:

public displayView() {
        console.log(`${this.person}`)
    }

You see, the important part is updating the child’s property with the new property value from the parent. In the following sections, we will be looking in-depth at what the functions we used above really do.

DirectiveDef

The directiveDef function at View_AppComponent_0 creates a NodeDefinition with NodeFlags as TypeDirective

i0.ɵdid(2, 49152, null, 0, i1.PeopleComponent, [], { person: [0, "person"] }, null),

The directiveDef function takes in several parameters:

export function directiveDef(
    checkIndex: number, flags: NodeFlags,
    matchedQueries: null | [string | number, QueryValueType][], childCount: number, ctor: any,
    deps: ([DepFlags, any] | any)[], props?: null | {[name: string]: [number, string]},
    outputs?: null | {[name: string]: string}): NodeDef {...}

checkIndex : This is the index the directive is at in the NodeDef array. Here, our directive is at index 2 , this is useful when updating a directive or element or text. flags: This denotes the type of Node to create, Element , Text , Directive , etc. ctor: This indicates the class of the directive. deps: If our directive class depends on some other classes, here is where the array of its dependencies is passed. props: This object holds the properties of the directive through which inputs to the directives are received.

In our case, our directive PeopleComponent is receiving an input through its person property, so the props will be { person: [0, "person"] } outputs: This holds the output properties of our directives denoted with the @Output decorator.

The NodeDefinition generated is used to create an instance of our directive PeopleComponent (NB: components and directives are the same on compilation) on Angular first nodes creation via createViewNodes :

function createViewNodes(view: ViewData) {
...
➥    case NodeFlags.TypeDirective: {
        nodeData = nodes[i];
        if (!nodeData) {
          const instance = createDirectiveInstance(view, nodeDef);
          nodeData = <ProviderData>{instance};
        }
        if (nodeDef.flags & NodeFlags.Component) {
          const compView = asElementData(view, nodeDef.parent !.nodeIndex).componentView;
          initView(compView, nodeData.instance, nodeData.instance);
        }
        break;
      }
...
}

You see here, an instance of the directive PeopleComponent is created and stored in view.nodes. Looking into createDirectiveInstance function we see that it creates the instance using the normal new keyword.

export function createDirectiveInstance(view: ViewData, def: NodeDef): any {
...
  const instance = createClass(
      view, def.parent !, allowPrivateServices, def.provider !.value, def.provider !.deps);
...
  return instance;
}
function createClass(
    view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, ctor: any, deps: DepDef[]): any {
  const len = deps.length;
  switch (len) {
    case 0:
      return new ctor();
    case 1:
      return new ctor(resolveDep(view, elDef, allowPrivateServices, deps[0]));
...
  }
}

So after the creation, our directive instance is stored on the view.nodes array at index 2 , let's remember that.

UpdateDirective on AppComponent

In the last section, we saw how the instance of the child component PeopleComponent is created and stored at view.nodes array at the same array index.

Here, the updateDirective on View_AppComponent_0:

function(_ck, _v) {
        var _co = _v.component;
        var currVal_0 = _co.title;
        _ck(_v, 2, 0, currVal_0);
    }

It gets the new value of the title property and runs the _ck function.

NB: From other articles, updateDirectives runs an update on our directives. The function is triggered when a change detection cycle is initiated.

Looking at the prodCheckAndUpdateNode function _ck it takes the current view _v, array index 2, argStyle 0 and the current value of the title property as args.

Remeber, updateDirectives update the properties of directives. The second parameter denotes the index of the directive in the NodeDef array it is going to update. Here, it is index 2 which is the PeopleComponent we talked about in the last section:

i0.ɵdid(2, 49152, null, 0, i1.PeopleComponent, [], { person: [0, "person"] }, null),

So its going to update the property of PeopleComponent person with the current value of title . Rememeber, that was what we defined in the template view of AppComponent:

@Component({
  selector: 'app-root',
  template: `
➥  <people [person]="title"></people>
    <button (click)="changeTitle()">ChngTitle</button>
  `
})
export class AppComponent {...}

The prodCheckAndUpdateNode function get the NodeDef from view.def array, this retieves the NodeDef generated by directiveDef defined in the last section:

function prodCheckAndUpdateNode(
    view: ViewData, checkIndex: number, argStyle: ArgumentType, v0?: any, v1?: any, v2?: any,
    v3?: any, v4?: any, v5?: any, v6?: any, v7?: any, v8?: any, v9?: any): any {
  const nodeDef = view.def.nodes[checkIndex];
➥checkAndUpdateNode(view, nodeDef, argStyle, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
  return (nodeDef.flags & NodeFlags.CatPureExpression) ?
      asPureExpressionData(view, checkIndex).value :
      undefined;
}

Then, it calls the checkAndUpdateNode function with the currrent view, the NodeDef and the current title value. The checkAndUpdateNode make a few of calls whic lands at checkAndUpdateNodeInline:

function checkAndUpdateNodeInline(
    view: ViewData, nodeDef: NodeDef, v0?: any, v1?: any, v2?: any, v3?: any, v4?: any, v5?: any,
    v6?: any, v7?: any, v8?: any, v9?: any): boolean {
  switch (nodeDef.flags & NodeFlags.Types) {
    case NodeFlags.TypeElement:
      return checkAndUpdateElementInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
    case NodeFlags.TypeText:
      return checkAndUpdateTextInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
➥  case NodeFlags.TypeDirective:
      return checkAndUpdateDirectiveInline(view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
    case NodeFlags.TypePureArray:
    case NodeFlags.TypePureObject:
    case NodeFlags.TypePurePipe:
      return checkAndUpdatePureExpressionInline(
          view, nodeDef, v0, v1, v2, v3, v4, v5, v6, v7, v8, v9);
    default:
      throw 'unreachable';
  }
}

Here, the case NodeFlags.Directive is executed because the NodeDef is a TypeDirective. The checkAndUpdateDirectiveInline function is called:

export function checkAndUpdateDirectiveInline(
    view: ViewData, def: NodeDef, v0: any, v1: any, v2: any, v3: any, v4: any, v5: any, v6: any,
    v7: any, v8: any, v9: any): boolean {
  const providerData = asProviderData(view, def.nodeIndex);
  const directive = providerData.instance;
  let changed = false;
  let changes: SimpleChanges = undefined !;
  const bindLen = def.bindings.length;
  if (bindLen > 0 && checkBinding(view, def, 0, v0)) {
    changed = true;
    changes = updateProp(view, providerData, def, 0, v0, changes);
  }
  if (bindLen > 1 && checkBinding(view, def, 1, v1)) {
    changed = true;
    changes = updateProp(view, providerData, def, 1, v1, changes);
  }
  if (bindLen > 2 && checkBinding(view, def, 2, v2)) {
    changed = true;
    changes = updateProp(view, providerData, def, 2, v2, changes);
  }
...
  if (changes) {
    directive.ngOnChanges(changes);
  }
  if ((def.flags & NodeFlags.OnInit) &&
      shouldCallLifecycleInitHook(view, ViewState.InitState_CallingOnInit, def.nodeIndex)) {
    directive.ngOnInit();
  }
  if (def.flags & NodeFlags.DoCheck) {
    directive.ngDoCheck();
  }
  return changed;
}

Here, the instance of our directive we generate in the last section through directiveDef function is retrieved, also the providerData is retrieved using the asProviderData function. The providerData is the NodeDef created by directiveDef function.

We see a lot of if checks and checkBinding, they are all checking to see if the property of the directive has changed so as to perform an update. Also, see the updateProp function call updateProp(view, providerData, def, 0, v0, changes); , this function updates our directive with the property set in the bindings property in the NodeDef object.

function updateProp(
    view: ViewData, providerData: ProviderData, def: NodeDef, bindingIdx: number, value: any,
    changes: SimpleChanges): SimpleChanges {
  if (def.flags & NodeFlags.Component) {
    const compView = asElementData(view, def.parent !.nodeIndex).componentView;
    if (compView.def.flags & ViewFlags.OnPush) {
      compView.state |= ViewState.ChecksEnabled;
    }
  }
  const binding = def.bindings[bindingIdx];
  const propName = binding.name !;
  // Note: This is still safe with Closure Compiler as
  // the user passed in the property name as an object has to `providerDef`,
  // so Closure Compiler will have renamed the property correctly already.
  providerData.instance[propName] = value;
  if (def.flags & NodeFlags.OnChanges) {
    changes = changes || {};
    const oldValue = WrappedValue.unwrap(view.oldValues[def.bindingIndex + bindingIdx]);
    const binding = def.bindings[bindingIdx];
    changes[binding.nonMinifiedName !] =
        new SimpleChange(oldValue, value, (view.state & ViewState.FirstCheck) !== 0);
  }
  view.oldValues[def.bindingIndex + bindingIdx] = value;
  return changes;
}

Of all the code what is most important is that it updates the directive with the new value:

...
  const binding = def.bindings[bindingIdx];
  const propName = binding.name !;
providerData.instance[propName] = value;
...

It accesses the directive PeopleComponent with the instance property, remember last section in createDirectiveInstance function. Then, it references the property person of the directive and assigns it the value title .

The above code translates to this:

...
  const binding = def.bindings[bindingIdx];
  /**
  * binding = {
  *             name: 'person'
  *           }
  * propName becomes 'person'
  */
  const propName = binding.name !;
/*
  * providerData = {
  *                     instance: new PeopleComponent()
  *               }
  * `providerData.instance[propName]` becomes 
  * `new PeopleComponent().person`
  * value is the AppComponent's title property which is `app`
  * Finally,
  * `providerData.instance[propName] = value` is equal to
  * `new PeopleComponent().person = title`
  */
  providerData.instance[propName] = value;
...

The binding indicates the property of the directive that should be updated. So, we have seen here that the updateDirective in AppComponent factory View_AppComponent_0 updates the property of PeopleComponent. Since we have updated the PeopleComponent’s person property we have to update its DOM to reflect the change.

UpdateRenderer on PeopleComponent

Since our directive property has changed. A UI re-render has to be performed on the directives’ view to reflect the new change.

The updateRenderer is used to update DOM elements i.e perform a UI re-render on a component’s view. The updateRenderer on View_PeopleComponent_0 is this:

function View_PeopleComponent_0(_l) {
    return i0.ɵvid(0, [
        ...
    ], null, 
➥  function(_ck, _v) {
        var _co = _v.component;
        var currVal_0 = _co.person;
        _ck(_v, 0, 0, currVal_0);
    });
}

It is the same as updateDirectives param we saw in the last section. It gets the instance of the component class from the view _v component property. Next, it retrieves the current value of its property person , then calls prodCheckAndUpdateNode _ck function. Looking at the _ck function call parameters, we see that it updates the text node at index 0 with the value of PeopleComponent person property:

function View_PeopleComponent_0(_l) {
    return i0.ɵvid(0, [
➥      (_l()(), i0.ɵted(0, null, ["\n        ", ""])),
        (_l()(), i0.ɵeld(1, 0, null, null, 1, "button", [], 
        ...);
}

This operation will go through the same functions as the last section updateDirectives but this time it will be for the NodeFlags.TypeText because we are updating a text node.

In the end, the value of new PeopleComponent().person will be appended onto the text node.

property binding on HTMLElements

The mechanism is still the same as that of Component elements. The difference is that there is no updating of the element’s class property, the element’s property is updated on the DOM through updateRenderer .

@Component({
    selector: 'img-app',
    template: `
        <img [src]="srcURL" />
    `
})
export class ImgComponent {
    srcURL:string = 'http://google.com/'
}

The component factory will look like this:

function View_ImgComponent_0(_l) {
    return viewDef(0,[
        (_l()(), elementDef(0, 0, null, null, 0, "img" ,[] ,[
            [8, "src", 0]
    ], null, null, null, null))
    ],null, 
    //updateRenderer function arg
    function(_v, _ck) {
        var curr = _v.srcURL
        _ck(_v, 0, 0, curr)
    })
}

The elementDef function creates the element node with the information given to it by the compiler. The new thing here is the bindings arg is with a value. The bindings arg represents the property name of the DOM element that is to be updated when a change detection cycle is run.

Here, we have src there, because the src property of the img element is to be updated when the bound class variable srcURL changes. The updateRenderer function updates the img DOM element src property with the srcURL new value. See that the index of the NodeDef to update is 0 which is the index position of the img element in the NodeDef array in the viewDef function.

To demonstrate this on a basic HTML/JS app:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Propery Binding on DOM elements</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
1.➥<script type="text/javascript" src="node_modules/zone.js/dist/zone.js"></script>
2.➥<script type="text/javascript" src="node_modules/rxjs/bundles/Rx.min.js"></script>
3.➥<script type="text/javascript" src="node_modules/@angular/core/bundles/core.umd.js"></script>
</head>
<body>
    <div>
4.➥    <img id="img" />
5.➥    <button id="button">ChangeSrcURL</button>
    </div>
    <script>
6.➥    const {
            NgZone
        } = ng.core
7.➥    const ngZone = new NgZone({
            k: null
        })
8.➥    const imgBoundProperty = 'src'
9.➥    let srcURL = './red_angularlogo.png'
10.➥   function changeSrcURL() {
            srcURL = './blue_angularlogo.png'
        }
11.➥   function updateRenderer() {
            const img = document.getElementById('img')
            img[imgBoundProperty] = srcURL
        }
12.➥   function tick() {
            updateRenderer()
        }
13.➥   ngZone.run(() => {
            const button = document.getElementById('button')
            button.addEventListener('click', () => {
                changeSrcURL()
            })
        })
14.➥   ngZone.onMicrotaskEmpty.subscribe({
            next: () => {
                ngZone.run(() => {
                    tick()
                });
            }
        });
15.➥   tick()
    </script>
</body>
</html>

Looking at the code, we can see that we simulated how Angular runs property binding on DOM elements. First, we used Angular’s NgZone ( 6. ) to detect when to run a change detection via (12.) . The change detection runs the updateRenderer (11.) function which updates the element property src .

To get access to NgZone, we imported ( 1. ) zone.js, RxJS ( 2. ) and the Angular core ( 3. ) libraries.

We added the img ( 4. ) element and a button ( 5. ) element. The img element is the element whose src property is to be data-bound. The button will change the value of the variable property-bound to img 's src property and will trigger the change detection when clicked.

8. is the property of the img element to be property-bound. 9. is the variable that will be bound to img src property. 10. changes the value of the srcURL variable, since the srcURL variable is bound to the img's src property whenever the srcURL variable changes the src property must be updated with the current value of srcURL. 11. function updates the img DOM element's src property with the current value of the srcURL variable. 12. function is run when change detection is triggered, we see it calls the 11. function.

Notice: I used Angular function names to help you understand what it really does. It performs the same function as found in the Angular core.

A click EventListener is attached to the button inside the Angular zone. This is to enable us to pick up the async operation and run a change detection cycle via 14. .

We run the (15.) tick() at load so as to update the img src property with the default value of the srcURL.

If we load the app on our browser, we will see the img src property set to ./red_angularlogo.png , if we press the button the src property will be updated to  ./blue_angularlogog.png . If you have real blue_angularlogo.png and red_angularlogo.png pictures in your folder, you will see the image element transit from a red Angular logo to a blue Angular logo.

To read more about NgZone/Zone.js and change detection, you can go through the following links:

Conclusion

In this article, we looked deep into the mechanics of property binding both on Angular’s Component element and DOM elements. The most important thing we must understand in property binding is that whenever the variable property-bound to a DOM/Component element’s property changes the element’s property must be also updated to reflect the new value.

I would like you to play around with the demos I created in this article, it will go a long way of deepening your understanding of property binding mechanics in Angular. If you have any questions, feel free to ask!!!

Resources

To further understand NG component factories in-depth, here are useful links.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK