2
votes

Still continuing my Angular2 (beta 1) newcomer journey, I'm trying to understand how to properly implement 2-way binding without having my app stuck in an endless loop.

Please find the sample repro at this Plunker: http://plnkr.co/edit/83NeiUCEpPvYvUXIkl0g . Just run and click the button.

My scenario:

  • a directive wraps the Ace code editor and exposes a text property and a textChanged event.
  • an (XML) editor component uses this directive. It should be able to respond to changes in the underlying editor to update its XML code, and to set a new text in the underlying editor from its XML code.

My problem is that whenever the editor component programmatically sets its xml property, this triggers the change in the underlying Ace editor, which in turn triggers the text-changed event, which in turn is handled by the editor component's callback, and so forth. This generates an endless loop and you can see the editor's text flicker. What I'm doing wrong here?

Here is the code for the directive:

import {Component,Directive,EventEmitter,ElementRef} from 'angular2/core';
declare var ace: any;

@Directive({
    selector: "ace-editor",
    inputs: [
        "text"
    ],
    outputs: [
        "textChanged"
    ]
})
export class AceDirective { 
    private editor : any;
    public textChanged: EventEmitter<string>;

    set text(s: string) {
        if (s === undefined) return;
        let sOld = this.editor.getValue();
        if (sOld === s) return;        
        this.editor.setValue(s);
        this.editor.clearSelection();
        this.editor.focus();
    }

    get text() {
        return this.editor.getValue();
    }

    constructor(elementRef: ElementRef) {
        var dir = this;
        this.textChanged = new EventEmitter<string>();

        let el = elementRef.nativeElement;
        this.editor = ace.edit(el);
        let session = this.editor.getSession();
        session.setMode("ace/mode/xml");
        session.setUseWrapMode(true);

        this.editor.on("change", (e) => {
            let s = dir.editor.getValue();
            dir.textChanged.next(s);
        });
    }
}

And here is the editor component:

import {Component,EventEmitter} from "angular2/core";
import {AceDirective} from "./ace.directive";

@Component({
    selector: "my-editor",
    directives: [AceDirective],
    template: `<div style="border:1px solid red">
    <ace-editor id="editor" [text]="xml" (textChanged)="onXmlChanged($event)"></ace-editor>
    </div>
    <div><button (click)="changeXml()">set xml</button></div>`,
    inputs: [
        "xml"
    ]
})
export class EditorComponent { 
    private _xml: string;

    // using a property here so that I can set a breakpoint
    public set xml(s: string) {
        this._xml = s; 
    }
    public get xml() : string {
        return this._xml;
    }

    constructor() {
        this._xml = "";
    }

    public onXmlChanged(xml: string) {
        this._xml = xml;
    }

    // an action which somehow changes the XML content
    public changeXml() {
        this._xml = "<x>abc</x>";
    }
}
1

1 Answers

3
votes

Just do the [(ngModel)]="property" syntax. Even though it looks to behave as the old two-way binding in reality is breaking it up into two different one way bindings, one for input and one for output, the way Angular 2 handles changes and his dirty check cycles under the hood is what changed and will not allow you to do an endless loop.