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