Search code examples
javascriptstringangularimmutabilityangular-ngmodel

Angular2 ngModel: Why can it change an immutable string?


I am kind of confused here. Using ngModel i can do the following:

import {NgModel} from "angular2/common";
@Component({
  selector: "test",
  template: `<input tpye="text" [(ngModel)]="strInObj.str">  {{strInObj|json}}`,
  directives: [NgModel]
})
class Test{
  strInObj: {str: string} = {str: "this is a string"};
}  

Now when i type in the input, the strInObj.str gets updated. I am confused by this, because as far as i know strings are immutable, and there is no way of finding the parent of a reference.

In this case I pass the str property directly, meaning that ngModel gets a reference to the string. If it "changes" the string on that reference a new one is created therefore not changing the original string, which strInObj.str points to. At least that is my understanding.
And there is no way of finding the parent of the reference passed to ngModel (there is no concept like str.parent(), which would return strInObj) So how do they do this? I tried to understand the ts and js sources, but...well, here i am.

I tried to build a similar directive, but ended up only beeing able to pass objects that wrap strings, i have not found a way to modify the original object when passing the str property directly.... (in the example i would pass strInObj to my directive, which then would work with the str property of the passed object, which works fine).

I'd be glad if someone could help me unriddle this :)

EDIT In this plunker i have my custom directive StrDirective and an input field with a NgModel directive. Both have the same binding exampleStr which is outputted in a simple span.
Now, when i enter text in the input, you can see exampleStr being updated. This is expected behaviour. I know that this works.
The StrDirective updates its binding when clicked on. You can see that it updates its "working copy" of the string, but exampleStr is not getting updated.
My Question is now: How do they do it / How can i get my directive to update ExampleStr without having to wrap it in an Object?


Solution

  • In Javascript, all strings are immutable. When someone types in an input field, it updates the "working copy" of the string so that the working copy points to a new reference. Or put another way, every time the string changes, its a new string reference.

    When a key is pressed that changes the model, ngModelChange output event is triggered, which then updates parent component's model with the new reference. The references are now in sync. When you say "modify the string that was passed to it", that's not possible because strings are immutable.

    When there is two-way binding to a model:

    [(ngModel)]="str"
    

    The binding is equivalent to:

    [ngModel]="str" (ngModelChange)="str=$event"
    

    The @Output ngModelChange event is triggered whenever str (which the model is bound to) changes. In this way, a change in reference is propagated upwards to all components where two-way model binding is setup, so that each model points to the same reference.

    [Edit]

    In the Plnkr from the updated question, it shows that two-way binding is being restored after the user types a key in the input box. The question is how and why?

    To understand what's happening let's look at the two scenarios:

    1. User clicks the label and the event handler is triggered which changes the bound @Input value

    2. User types a key in the input box. Both labels are automatically re-bound to the same reference, and two-way model binding works for both labels.

    Before the user clicks the label, all bindings are in sync:

                           S1 (app component)
                           /\
    (exampleStr binding) S1  S1 (str component)
    

    After the click event, the bound @Input model changes. Then, a round of change detection occurs starting from the root and working its way to the child components in depth first order. Since the @Input binding propagates downwards, nothing really changes to the other bindings.

    In the first scenario, this is the state of the bindings after the click event:

                           S1 (app component)
                           /\
    (exampleStr binding) S1  S2 (str component)
    

    When the user starts to type in the textbox which has two-way binding setup, it triggers an ngModelChange event which changes the value of exampleStr to S3.

                           S3 (app component)
                           /\
    (exampleStr binding) S3  S2 (str component)
    

    The default change detection strategy then kicks in, which starts from the root and works its way down to the child components in depth first order.

    The state of the bindings after a key is pressed is:

                           S3 (app component)
                           /\
    (exampleStr binding) S3  S3 (str component)
    

    As you can see, all the bindings are in sync again. The default change detection strategy checks all components; changes to the model are propagated through the component's @Input bindings in a predictable and uni-directional flow from parent to child.

    To understand how change detection works, think of it happening in phases. This is over-simplified, but it may help with your understanding:

    1. Input bindings are propagated from root to children. Angular keeps track of which models are bound to which input properties. This is needed later for change detection.
    2. An event fires (such as a click event) which modifies @Input properties. With two-way model binding, the change in model is propagated upwards from child to parent.
    3. After the event fires, a single round of change detection is triggered starting from the root (repeat step 1) to re-sync all the bindings. During this process, all application and view bindings are updated.

    Note: Angular uses zones to monkey-patch browser events, so it knows to when to trigger change detection.

    [Edit]

    If you want the message to be updated when the label is clicked, setup two-way binding like you would with ngModel:

    @Input("str") value : string;
    @Output("strChange") valueChange:EventEmitter<string> = new EventEmitter();
    
    
    onClick(){
      this.value = "new string";
      this.valueChange.next(this.value);
    }
    

    HTML

    <span [(str)]="exampleStr"></span><br>
    

    Demo Plnkr