How to use [(ngModel)] on div's contenteditable in angular2?

AngularIonic2Contenteditable

Angular Problem Overview


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 ''

Angular Solutions


Solution 1 - Angular

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}}`
})
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.

Solution 2 - Angular

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.

Solution 3 - Angular

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.

Solution 4 - Angular

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;
  }
}

Solution 5 - Angular

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

Solution 6 - Angular

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;
}

Solution 7 - Angular

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>

Solution 8 - Angular

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionKim WongView Question on Stackoverflow
Solution 1 - AngularMark RajcokView Answer on Stackoverflow
Solution 2 - AngularktretyakView Answer on Stackoverflow
Solution 3 - AngulartobekView Answer on Stackoverflow
Solution 4 - AngularRene HamburgerView Answer on Stackoverflow
Solution 5 - AngularFloView Answer on Stackoverflow
Solution 6 - AngularSahil RalkarView Answer on Stackoverflow
Solution 7 - AngularIsaacView Answer on Stackoverflow
Solution 8 - AngularFAndrewView Answer on Stackoverflow