Namek Dev
a developer's log
NamekDev

Two-way binding to contenteditable element in Angular 2

January 23, 2016

contenteditable is a new HTML5 feature where you can edit any text inside DOM elements which are not editable by default (as input or textearea). Angular 2 is gaining it’s momentum right now but couldn’t find a recipe to bind contenteditable element to certain model object. I decided to write a simple Directive that binds element in two-way through element’s innerText  field.

Simple approach

Normally, you can add contenteditable  property to your element and listen for blur event which will call some update.

<span #el contenteditable (blur)="text=el.innerText">{{text}}</span>

But it’s not two-way binding. {{text}}  binds from model to element, and statement set for blur event updates the other way.

There’s also a little drawback hidden in there. Let’s take a look again:

<span #el1 contenteditable (blur)="firstName=el1.innerText">{{firstName}}</span>
<span #el2 contenteditable (blur)="lastName=el2.innerText">{{lastName}}</span>
<span #el3 contenteditable (blur)="nickname=el3.innerText">{{nickname}}</span>
<span #el4 contenteditable (blur)="age=el4.innerText">{{age}}</span>

Having to make multiple references (like #el1 , #el2 ) is not cool - more code and easier to make a mistake.

Let’s find a way to make it a little less reference-polluting and really two-way binded.

new Directive: contenteditableModel

TL; DR http://plnkr.co/edit/8YFTcQ but let’s look what’s in there.

Here’s the usage - we turn the contenteditable  property on and bind it to text  model:

<span contenteditable [(contenteditableModel)]="text"></span>

element -> model

There are two key things to update (our binded) model when contenteditable element changes:

  1. blur  event - triggers when you focus away from your element

    @Directive({ selector: ‘[contenteditableModel]‘, host: { ‘(blur)’: ‘onBlur()’ } })

    onBlur() { var value = this.elRef.nativeElement.innerText this.lastViewModel = value this.update.emit(value) }

Of course, you could add keyup event to update more often. Depends on your needs.

  1. name of event that triggers update of model

Here’s our update event:

@Output('contenteditableModelChange') update = new EventEmitter();

Actually, update  (triggered in onBlur) could be updateModel , doYourStuff  or whatever else. What you need is a proper value for the @Output  directive - it’s a directive name (same as in directive selector) plus “Change” suffix. So here’s contenteditableModelChange . The suffix is what makes a difference.

model -> element

Directive has to implement OnChanges  interface.

ngOnChanges(changes) {
    if (isPropertyUpdated(changes, this.lastViewModel)) {
        this.lastViewModel = this.model
        this.refreshView()
    }
}

private refreshView() {
    this.elRef.nativeElement.innerText = this.model
}

innerText vs innerHTML vs textContent

Personally I needed innerText  but if you need single line text and better performance you may want to use textContent . To read more about differences read on “innerText vs textContent”.

Whole code

Whole code can be found here http://plnkr.co/edit/8YFTcQ or here:

import {Directive, ElementRef, Input, Output} from "angular2/core";
import {EventEmitter} from "angular2/src/facade/async";
import {OnChanges} from "angular2/core";
import {isPropertyUpdated} from "angular2/src/common/forms/directives/shared";

@Directive({
    selector: '[contenteditableModel]',
    host: {
        '(blur)': 'onBlur()'
    }
})
export class ContenteditableModel implements OnChanges {
    @Input('contenteditableModel') model: any;
    @Output('contenteditableModelChange') update = new EventEmitter();

    private lastViewModel: any;


    constructor(private elRef: ElementRef) {
    }

    ngOnChanges(changes) {
        if (isPropertyUpdated(changes, this.lastViewModel)) {
            this.lastViewModel = this.model
            this.refreshView()
        }
    }

    onBlur() {
        var value = this.elRef.nativeElement.innerText
        this.lastViewModel = value
        this.update.emit(value)
    }

    private refreshView() {
        this.elRef.nativeElement.innerText = this.model
    }
}

References

angular2, web
comments powered by Disqus