70

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 ''
danday74
  • 52,471
  • 49
  • 232
  • 283
Kim Wong
  • 2,027
  • 4
  • 17
  • 22

8 Answers8

123

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.

Robouste
  • 3,020
  • 4
  • 33
  • 55
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Thank you for your answer. But it is not a two way binding. When the user type something in the input, the "model" var will not change. – Kim Wong Feb 14 '16 at 12:23
  • 2
    @KimWong, the `model` var is definitely changing in the Plunker I provided. That's why I put `{{model}}` in the view/template, so that we can see it change when we edit the div. – Mark Rajcok Feb 15 '16 at 15:47
  • Thx! This helps. For some reason angular2rc1 does not like textContent, using innerText instead works fine. – Meir Jul 04 '16 at 09:45
  • Big help thank you! Any way to easily support multiline and resist the text from being generated at the beginning of the line i.e. backwards? – prozac Jul 12 '16 at 03:51
  • @ZachBotterman, if I add `styles: ['div { white-space: pre}' ]` to the component metadata it sort of works. I have to type shift-enter for the newline to not be replaced. Tested on Chrome only. – Mark Rajcok Jul 12 '16 at 18:18
  • @MarkRajcok Thanks for the reply. Will check asap – prozac Jul 18 '16 at 07:56
  • I ran into the same issue, got it working in a slightly different way answered below. – tobek Dec 21 '16 at 01:58
  • 9
    Regardless of the event used to trigger model=$event.target.textContent, this currently doesn't work properly on Firefox and Edge. The cursor is always set at index 0 when typing. You should be aware of this. – Lys Apr 03 '17 at 14:00
  • Thank you for your answer. I have a little more complex use case. I also have an attribute level directive in the same div and I need to have 2 way binding for "model". So that my directive will get the updated model and if I change the model from directive, it's reflected in UI. How do I do that? – user911 May 17 '17 at 00:13
  • 8
    guys, anyone know how to sort out so the cursor index to not be set at 0 all the time? – Chris Tarasovs Jun 04 '17 at 17:35
  • 1
    This doesnot work in IE. Just open the plunker in IE. – Ziggler Dec 14 '17 at 01:18
  • 3
    currently this is only useful for typing backwards – szaman Mar 05 '18 at 19:56
  • @MarkRajcok Great solution. But your plunker do not work fine in firefox. It types backward in firefox. I have added post for this: https://stackoverflow.com/questions/51206076/contenteditable-div-is-not-working-properly-with-angular-two-way-binding-in-fire Need your help. – Always_a_learner Jul 18 '18 at 10:11
17

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.

ktretyak
  • 27,251
  • 11
  • 40
  • 63
16

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.

tobek
  • 4,349
  • 3
  • 32
  • 41
  • 1
    Tested on Firefox with Windows as well, under Ionic 2, and your code works there as well. Thanks! – Cel Apr 20 '17 at 08:55
12

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;
  }
}
Rene Hamburger
  • 2,003
  • 16
  • 17
  • Thanks, but to avoid **ExpressionChangedAfterItHasBeenCheckedError** please use asynchronous `EventEmitter` in output `@Output() contenteditableModelChange?= new EventEmitter(true);` [reference](https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4) to article. Maybe you can update your code. – Atiris Sep 22 '17 at 07:56
7

I've fiddled around with this solutions and will use the following solution in my project now:

<div #topicTitle contenteditable="true" [textContent]="model" (input)="model=topicTitle.innerText"></div>

I prefer using the template reference variable to the "$event" stuff.

Related link: https://angular.io/guide/user-input#get-user-input-from-a-template-reference-variable

Flo
  • 189
  • 2
  • 7
  • 1
    I used this solution on an editable TD as well. {{model}} as suggested by some other solutions gave me issues while typing. It would dynamically update the text and I would get some garbled gibberish, with this solution that didn't happen – p0enkie Mar 12 '18 at 16:13
  • I like the solution, but it also types backwards in firefox. – illnr Dec 12 '18 at 10:20
3

In contenteditable, I've achieved the two way binding with help of the blur event and innerHTML attribute.

in .html:

<div placeholder="Write your message.."(blur)="getContent($event.target.innerHTML)" contenteditable [innerHTML]="content"></div>

In .ts:

getContent(innerText){
  this.content = innerText;
}
Sahil Ralkar
  • 2,331
  • 23
  • 25
  • This works fine when you use the innerHTML of $event.target in your save method (like I usually do). Here it is consistent: innerHTML both ways. – Jan Croonen Jun 13 '23 at 00:05
0

Here is a simple solution if what you are binding to is a string, no events necessary. Just put a text box input inside the table cell and bind to that. Then format your text box to transparent

HTML:

<tr *ngFor="let x of tableList">
    <td>
        <input type="text" [(ngModel)]="x.value" [ngModelOptions]="{standalone: true}">
    </td>
</tr>
Isaac
  • 21
  • 4
0

For me, it was enough to use javascript, without the ts object. HTML:

 <div
        id="custom-input"
        placeholder="Schreiben..."
</div>

TS:

  • to get the input value: document.getElementById("custom-input").innerHTML

  • to set the input value: document.getElementById("custom-input").innerHTML = "myValue"

And everything works perfectly. I was forced to use a div instead of ionic ion-textarea because I had problems with autosize. With ion-textarea I was able to make autosize only with js. Now I make autosize with CSS, which I think is better.

FAndrew
  • 248
  • 4
  • 22