I am trying to use ngModel to two way bind div's contenteditable input content as follows:
<div id="replyiput" class="btn-input" [(ngModel)]="replyContent" contenteditable="true" data-text="type..." style="outline: none;" ></div>
but it is not working and an error occurs:
EXCEPTION: No value accessor for '' in [ddd in PostContent@64:141]
app.bundle.js:33898 ORIGINAL EXCEPTION: No value accessor for ''
ngModel is a directive which binds input, select and textarea, and stores the required user value in a variable and we can use that variable whenever we require that value. It also is used during validations in a form. We can use ngModel with: input. text.
NgModel expects the bound element to have a value property, which div s don't have. That's why you get the No value accessor error.
This directive is used by itself or as part of a larger form. Use the ngModel selector to activate it. It accepts a domain model as an optional Input . If you have a one-way binding to ngModel with [] syntax, changing the domain model's value in the component class sets the value in the view.
Updated answer (2017-10-09):
Now I have ng-contenteditable module. Its compatibility with Angular forms.
Old answer (2017-05-11): In my case, I can simple to do:
<div
contenteditable="true"
(input)="post.postTitle = $event.target.innerText"
>{{ postTitle }}</div>
Where post - it's object with property postTitle.
First time, after ngOnInit() and get post from backend, I set this.postTitle = post.postTitle in my component.
NgModel expects the bound element to have a value property, which divs don't have. That's why you get the No value accessor error.
You can set up your own equivalent property and event databinding using the textContent property (instead of value) and the input event:
import { Component } from "angular2/core";
@Component({
selector: "my-app",
template: `{{ title }}
<div contenteditable="true" [textContent]="model" (input)="model = $event.target.textContent"></div>
<p>{{ model }}</p>`
})
export class AppComponent {
title = "Angular 2 RC.4";
model = "some text";
constructor() {
console.clear();
}
}
Plunker
I don't know if the input event is supported on all browsers for contenteditable. You could always bind to some keyboard event instead.
Working Plunkr here http://plnkr.co/edit/j9fDFc, but relevant code below.
Binding to and manually updating textContent wasn't working for me, it doesn't handle line breaks (in Chrome, typing after a line break jumps cursor back to the beginning) but I was able to get it work using a contenteditable model directive from https://www.namekdev.net/2016/01/two-way-binding-to-contenteditable-element-in-angular-2/.
I tweaked it to handle multi-line plain text (with \ns, not <br>s) by using white-space: pre-wrap, and updated it to use keyup instead of blur. Note that some solutions to this problem use the input event which isn't supported on IE or Edge on contenteditable elements yet.
Here's the code:
Directive:
import {Directive, ElementRef, Input, Output, EventEmitter, SimpleChanges} from 'angular2/core';
@Directive({
selector: '[contenteditableModel]',
host: {
'(keyup)': 'onKeyup()'
}
})
export class ContenteditableModel {
@Input('contenteditableModel') model: string;
@Output('contenteditableModelChange') update = new EventEmitter();
/**
* By updating this property on keyup, and checking against it during
* ngOnChanges, we can rule out change events fired by our own onKeyup.
* Ideally we would not have to check against the whole string on every
* change, could possibly store a flag during onKeyup and test against that
* flag in ngOnChanges, but implementation details of Angular change detection
* cycle might make this not work in some edge cases?
*/
private lastViewModel: string;
constructor(private elRef: ElementRef) {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['model'] && changes['model'].currentValue !== this.lastViewModel) {
this.lastViewModel = this.model;
this.refreshView();
}
}
/** This should probably be debounced. */
onKeyup() {
var value = this.elRef.nativeElement.innerText;
this.lastViewModel = value;
this.update.emit(value);
}
private refreshView() {
this.elRef.nativeElement.innerText = this.model
}
}
Usage:
import {Component} from 'angular2/core'
import {ContenteditableModel} from './contenteditable-model'
@Component({
selector: 'my-app',
providers: [],
directives: [ContenteditableModel],
styles: [
`div {
white-space: pre-wrap;
/* just for looks: */
border: 1px solid coral;
width: 200px;
min-height: 100px;
margin-bottom: 20px;
}`
],
template: `
<b>contenteditable:</b>
<div contenteditable="true" [(contenteditableModel)]="text"></div>
<b>Output:</b>
<div>{{text}}</div>
<b>Input:</b><br>
<button (click)="text='Success!'">Set model to "Success!"</button>
`
})
export class App {
text: string;
constructor() {
this.text = "This works\nwith multiple\n\nlines"
}
}
Only tested in Chrome and FF on Linux so far.
Here's another version, based on @tobek's answer, which also supports html and pasting:
import {
Directive, ElementRef, Input, Output, EventEmitter, SimpleChanges, OnChanges,
HostListener, Sanitizer, SecurityContext
} from '@angular/core';
@Directive({
selector: '[contenteditableModel]'
})
export class ContenteditableDirective implements OnChanges {
/** Model */
@Input() contenteditableModel: string;
@Output() contenteditableModelChange?= new EventEmitter();
/** Allow (sanitized) html */
@Input() contenteditableHtml?: boolean = false;
constructor(
private elRef: ElementRef,
private sanitizer: Sanitizer
) { }
ngOnChanges(changes: SimpleChanges) {
if (changes['contenteditableModel']) {
// On init: if contenteditableModel is empty, read from DOM in case the element has content
if (changes['contenteditableModel'].isFirstChange() && !this.contenteditableModel) {
this.onInput(true);
}
this.refreshView();
}
}
@HostListener('input') // input event would be sufficient, but isn't supported by IE
@HostListener('blur') // additional fallback
@HostListener('keyup') onInput(trim = false) {
let value = this.elRef.nativeElement[this.getProperty()];
if (trim) {
value = value.replace(/^[\n\s]+/, '');
value = value.replace(/[\n\s]+$/, '');
}
this.contenteditableModelChange.emit(value);
}
@HostListener('paste') onPaste() {
this.onInput();
if (!this.contenteditableHtml) {
// For text-only contenteditable, remove pasted HTML.
// 1 tick wait is required for DOM update
setTimeout(() => {
if (this.elRef.nativeElement.innerHTML !== this.elRef.nativeElement.innerText) {
this.elRef.nativeElement.innerHTML = this.elRef.nativeElement.innerText;
}
});
}
}
private refreshView() {
const newContent = this.sanitize(this.contenteditableModel);
// Only refresh if content changed to avoid cursor loss
// (as ngOnChanges can be triggered an additional time by onInput())
if (newContent !== this.elRef.nativeElement[this.getProperty()]) {
this.elRef.nativeElement[this.getProperty()] = newContent;
}
}
private getProperty(): string {
return this.contenteditableHtml ? 'innerHTML' : 'innerText';
}
private sanitize(content: string): string {
return this.contenteditableHtml ? this.sanitizer.sanitize(SecurityContext.HTML, content) : content;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With