Two-way binding to contenteditable element in Angular 2
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:
-
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.
- 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
}
}