My Component:
const noop = () => {}; // does nothing. Signals that no operation is required
@Component({
selector: 'simple-tiny',
template: `<textarea id="{{elementId}}" [attr.name]="name"></textarea>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SimpleTinyComponent),
multi: true
}]
})
export class SimpleTinyComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
@Input() elementId: String;
@Input() name: String;
@Output() onEditorKeyup = new EventEmitter<any>();
constructor(private ngZone: NgZone) {}
editor;
// using '_value' because variable 'value' is already defined by get()/set()
private _value: string = '';
// Callback registered via registerOnTouched (ControlValueAccessor)
private onTouchedCallback: () => void = noop;
// Callback registered via registerOnChange (ControlValueAccessor)
private onChangeCallback: (_: any) => void = noop;
ngAfterViewInit() {
tinymce.init({
selector: '#' + this.elementId,
menubar: false,
height: '480',
plugins: ['link', 'image', 'lists', 'table', 'dynamicTags'],
toolbar1: 'bold italic indent outdent link image',
toolbar2: 'table bullist numlist tags',
skin_url: '/backoffice-ui/assets/skins/lightgray',
setup: editor => {
this.editor = editor;
editor.on('keyup', (e) => {
const content = editor.getContent();
this.onEditorKeyup.emit(content);
this.value = content;
//
// This tells ng to update the model (div in main area) with new HTML in editor pane
// See: https://community.tinymce.com/communityQuestion?id=90661000000IetUAAS
// and: https://blog.thoughtram.io/angular/2016/02/01/zones-in-angular-2.html
//
this.ngZone.run(() => {});
});
// This fires when operator clicks toolbar button, e.g. Bold, Italics, Indent, etc.
editor.on('ExecCommand', (e) => {
const content = editor.getContent();
this.value = content;
});
editor.on('NodeChange', (e) => {
const content = editor.getContent();
this.value = content;
});
}
});
}
/**
* Cleans up text editor when page is unloaded
*/
ngOnDestroy() {
tinymce.remove(this.editor);
}
get value(): any {
return this._value;
}
@Input()
set value(value: any) {
if (value !== this._value) {
this._value = value;
this.onChangeCallback(value);
}
}
/**
* Takes a new value from the form model and writes it into the view, and initializes rich text editor with same value.
*
* @param value
*/
writeValue(value: string)
{
if (value !== undefined)
{
this._value = value;
if (this._value != null)
{
this.editor.setContent(this._value);
}
}
}
registerOnChange(fn: any)
{
this.onChangeCallback = fn;
}
/**
* We don't want anything special when touched.
* Keeping as empty implementation.
*/
registerOnTouched(fn: any)
{
this.onTouchedCallback = fn;
}
}
Selector:
<simple-tiny
name="body"
elementId="my-editor-id"
[(ngModel)]="template.content.bodyText">
</simple-tiny>
So, tinyMCE is initialized within the ngAfterViewInit() to make sure that all of the components and child components are ready. Next the content is set to the value of the ngModel with ControlValueAccessor.writeValue().
This works correctly when one navigates to the page normally, but if the browser is reloaded, an error occurs:
EXCEPTION: Uncaught (in promise): TypeError: Cannot read property 'setContent' of undefined
TypeError: Cannot read property 'setContent' of undefined
at SimpleTinyComponent.writeValue (https://localhost/backoffice-ui/main.bundle.js?v=2017Q1.1.0-145-g0c0c0e9:3741:28)
at https://localhost/backoffice-ui/vendor.bundle.js?v=2017Q1.1.0-145-g0c0c0e9:10557:27
at https://localhost/backoffice-ui/vendor.bundle.js?v=2017Q1.1.0-145-g0c0c0e9:33801:65
at Array.forEach (native)
I traced this to within tinymce.js - the init method runs DOM.bind(window, 'ready', initEditors);
and when the page is reloaded initEditors runs after writeValue so the editor is still undefined.
Here is my work-around for now:
ngAfterViewInit() {
tinymce.init({
selector: '#' + this.elementId,
plugins: ['code', 'link', 'lists', 'paste', 'preview', 'table'],
// ...
});
if (this.value) {
tinymce.activeEditor.setContent(this.value, { format: 'raw' });
}
}
writeValue(value) {
if (value) {
this.value = value;
if (tinymce.activeEditor) {
tinymce.activeEditor.setContent(this.value, { format: 'raw' });
}
}
}
Also had the same problem, my workaround:
import { Component, Input, Output, EventEmitter, AfterViewInit, OnDestroy, forwardRef, ViewChild, NgZone } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
declare var tinymce: any;
@Component({
selector: 'ng-tiny-editor',
templateUrl: './ng-tiny-editor.component.html',
styleUrls: ['./ng-tiny-editor.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NgTinyEditorComponent),
multi: true
}
]
})
export class NgTinyEditorComponent implements AfterViewInit, OnDestroy {
@ViewChild('host') host: any;
editor;
constructor(private zone: NgZone) { }
ngAfterViewInit() {
tinymce.init({
target: this.host.nativeElement,
plugins: ['link', 'paste'],
skin_url: '/manage/dist/assets/tinymce/skins/lightgray',
language_url : '/manage/dist/assets/tinymce/langs/es.js',
setup: editor => {
this.editor = editor;
//this.editor.setContent(this.value);
editor.on('init', () => {
this.value && this.editor.setContent(this.value, {format: 'raw'});
});
editor.on('change', () => {
this.onTouched();
const content = editor.getContent();
this.updateValue(content);
});
},
});
}
_value = '';
get value(): any { return this._value; }
@Input() set value(v) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
updateValue(value: any) {
this.zone.run(() => {
this.value = value;
this.onChange(value);
this.onTouched();
});
}
ngOnDestroy() {
tinymce.remove(this.editor);
}
/**
* Implements ControlValueAccessor
*/
writeValue(value: any) {
this._value = value;
value && this.editor && this.editor.hasVisual && this.editor.setContent(value);
}
onChange(_: any) {}
onTouched() {}
registerOnChange(fn: any) { this.onChange = fn; }
registerOnTouched(fn: any) { this.onTouched = fn; }
}
There is an official Angular wrapper project now (tinymce-angular). Closing this ticket, any Angular related issues should be open in that project.
Most helpful comment
Here is my work-around for now: