Naftis
Naftis

Reputation: 4559

Angular2 2-way databinding between parent component and directive

See update below

Still playing with Angular2 Beta, I'm trying to implement a scenario where an "editor" component template contains a directive wrapping the Ace editor. Thus, the "editor" component is the parent of the Ace wrapper directive, and I want to get the code from the directive or set the code into it.

While the directive alone works fine, when I include it in this component I get nothing displayed; yet no errors are shown in the browser's console. You can find a repro at this Plunker: http://plnkr.co/edit/kzclJLIX6hRMWa14A0Pb.

In my implementation, the ace.directive directive wrapping the Ace editor has a text property and a textChanged event.

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;
    private settingText : boolean;
    public textChanged: EventEmitter<string>;

    set text(s: string) {
        let sOld = this.editor.getValue();
        if (sOld === s) return;

        this.settingText = true;
        this.editor.setValue(s);
        this.editor.clearSelection();
        this.editor.focus();
        this.settingText = false;
    }

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

        let el = elementRef.nativeElement;
        this.editor = ace.edit(el);

        this.editor.on("change", (e) => {
            if (dir.settingText) return;
            dir.textChanged.next(dir.editor.getValue());
        });
    }
}

The editor.component component uses the directive: it has an xml property representing the XML code being edited. Its template contains the directive as follows:

<ace-editor id="editor" [text]="xml" (textChanged)="onXmlChanged()"></ace-editor>

i.e. the directive's text property is bound to the parent component's xml property, and the directive's textChanged event is handled by the parent's onXmlChanged function.

This being a 2-way databinding, AFAIK I could also try:

<ace-editor id="editor" [(ngModel)]="xml"></ace-editor>

Here is the editor's code:

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

@Component({
    selector: "mit-editor",
    directives: [AceDirective],
    template: `<div>
      <ace-editor id="editor" [text]="xml" (textChanged)="onXmlChanged()"></ace-editor>
    </div>
    `,
    inputs: [
        "xml"
    ]
})
export class EditorComponent { 
    public xml: string;

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

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

Update #1 For some reason, the Plunker does not transpile and load my .ts files other than the preexisting ones, so I continued troubleshooting locally.

As for the question, I found I have to add the $event argument to the call in the template (see my comment). My directive is now:

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);
        });
    }
}

The editor component template contains the directive like this:

<ace-editor id="editor" [text]="xml" (textChanged)="onXmlChanged($event)"></ace-editor>

Anyway, note that if I now try to programmatically set the editor's component xml property, Angular starts and endless loop as the change triggers the event on the ace editor which emits the event which sets the xml property again, and so forth. Probably it's just me doing something wrong, but currently I had to use this hack in the code, and of course I don't like it:):

// ...
export class EditorComponent { 
    private _xml: string;
    private _changeFrozenCount: number;

    public set xml(s: string) {
        this._xml = s; 
    }
    public get xml() : string {
        return this._xml;
    }

    constructor(private editorService: EditorService, private xmlService: XmlService) {
        this._xml = "";
    }

    public onXmlChanged(xml: string) {
        if (this._changeFrozenCount > 0) {
            this._changeFrozenCount--;
            return;
        }
        this._xml = xml;
    }

    public changeXml() {
        this._changeFrozenCount = 1;
        this._xml = "<sample>Hello</sample>"
    }
}

Upvotes: 2

Views: 798

Answers (1)

Ionut Costica
Ionut Costica

Reputation: 1392

You forgot to add

directives: [EditorComponent]

in your app.ts file. Currently the directives array is empty ([]) in your plunker. Just make this change and it works, and if you set onXmlChanged($event) it won't even error out when you type inside the editor :)

Upvotes: 3

Related Questions