From 5c6aa574a443bb8eff2faf85072504020fce1fb3 Mon Sep 17 00:00:00 2001 From: Guenter Schafranek Date: Wed, 24 Jan 2024 15:26:20 +0100 Subject: [PATCH] fix(ngx-jodit): Fixed value handling for reactive forms - Dropped `ngOnChanges` in favour of `@Input()`-setter and subject/observable pipelines - The latter allows to "react" on the jodit editor to be initialized to then apply the (last) value written by the reactive form directive - ... but also need preventing `ExpressionChangedAfterItHasBeenCheckedError` by delaying the value setting (to the next change detection cycle) --- apps/demo/src/app/app.component.html | 18 ++- apps/demo/src/app/app.component.ts | 21 +++- apps/demo/src/app/app.module.ts | 4 +- libs/ngx-jodit/README.md | 7 ++ libs/ngx-jodit/src/lib/ngx-jodit.component.ts | 117 ++++++++++-------- 5 files changed, 105 insertions(+), 62 deletions(-) diff --git a/apps/demo/src/app/app.component.html b/apps/demo/src/app/app.component.html index 76a193a..2148873 100644 --- a/apps/demo/src/app/app.component.html +++ b/apps/demo/src/app/app.component.html @@ -25,7 +25,23 @@ README.

- +
+
+
+

Template driven form

+ +
Value: {{ value | json }}
+
+
+

Reactive form

+
+ +
+
Value: {{ this.formGroup.get("editor")?.value | json }}
+
+
+
+

Options

All diff --git a/apps/demo/src/app/app.component.ts b/apps/demo/src/app/app.component.ts index 6bba95c..e80afc6 100644 --- a/apps/demo/src/app/app.component.ts +++ b/apps/demo/src/app/app.component.ts @@ -1,15 +1,21 @@ -import {Component, ViewChild} from '@angular/core'; -import {JoditConfig, NgxJoditComponent} from 'ngx-jodit'; -import 'jodit/esm/plugins/bold/bold.js'; import 'jodit/esm/plugins/add-new-line/add-new-line.js'; +import 'jodit/esm/plugins/bold/bold.js'; import 'jodit/esm/plugins/fullsize/fullsize.js'; -import 'jodit/esm/plugins/source/source.js'; import 'jodit/esm/plugins/indent/indent.js'; +import 'jodit/esm/plugins/source/source.js'; + +import { Component, ViewChild } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Jodit } from 'jodit'; import de from 'jodit/esm/langs/de.js'; -import {Jodit} from 'jodit'; +import { JoditConfig, NgxJoditComponent } from 'ngx-jodit'; Jodit.lang.de = de; +interface FormWithJoditEditor { + editor: string; +} + @Component({ selector: 'jodit-root', templateUrl: './app.component.html', @@ -17,6 +23,9 @@ Jodit.lang.de = de; }) export class AppComponent { value = 'Some text'; + formGroup = this.formBuilder.group({ + editor: 'Some text in a reactive form' + }); _optionsStr = ''; @ViewChild('ngxJodit') ngxJodit?: NgxJoditComponent; @@ -36,6 +45,6 @@ export class AppComponent { options: JoditConfig = {}; - constructor() { + constructor(private formBuilder: FormBuilder) { } } diff --git a/apps/demo/src/app/app.module.ts b/apps/demo/src/app/app.module.ts index 82cc495..3fb35de 100644 --- a/apps/demo/src/app/app.module.ts +++ b/apps/demo/src/app/app.module.ts @@ -3,11 +3,11 @@ import {BrowserModule} from '@angular/platform-browser'; import {AppComponent} from './app.component'; import {NgxJoditComponent} from 'ngx-jodit'; -import {FormsModule} from '@angular/forms'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; @NgModule({ declarations: [AppComponent], - imports: [BrowserModule, NgxJoditComponent, FormsModule], + imports: [BrowserModule, NgxJoditComponent, FormsModule, ReactiveFormsModule], providers: [], bootstrap: [AppComponent], }) diff --git a/libs/ngx-jodit/README.md b/libs/ngx-jodit/README.md index d060f76..e718422 100644 --- a/libs/ngx-jodit/README.md +++ b/libs/ngx-jodit/README.md @@ -85,10 +85,17 @@ All [options](https://xdsoft.net/jodit/docs/classes/config.Config.html) from Jod ``` - With AngularForms (make sure to import AngularForms): + - Template driven ```angular2html ``` + - Reactive + ```angular2html +

+ +
+ ``` ## How to import plugins diff --git a/libs/ngx-jodit/src/lib/ngx-jodit.component.ts b/libs/ngx-jodit/src/lib/ngx-jodit.component.ts index d93d209..8f62171 100644 --- a/libs/ngx-jodit/src/lib/ngx-jodit.component.ts +++ b/libs/ngx-jodit/src/lib/ngx-jodit.component.ts @@ -1,21 +1,22 @@ +import { CommonModule } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, EventEmitter, forwardRef, Input, - OnChanges, OnDestroy, Output, - SimpleChanges, ViewChild, } from '@angular/core'; -import {Jodit} from 'jodit'; -import {CommonModule} from '@angular/common'; -import {JoditConfig} from './types'; -import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR} from '@angular/forms'; +import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Jodit } from 'jodit'; +import { BehaviorSubject, combineLatest, delay, distinctUntilChanged, filter, merge, Subscription, withLatestFrom } from 'rxjs'; + +import { JoditConfig } from './types'; @Component({ selector: 'ngx-jodit', @@ -30,7 +31,7 @@ import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR} from '@angular/for styleUrls: ['./ngx-jodit.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, OnDestroy, OnChanges { +export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, OnDestroy { @ViewChild('joditContainer') joditContainer!: ElementRef; jodit?: Jodit; @@ -38,12 +39,25 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O * options for jodit. * You can add more supported options even Typescript doesn't suggest the options. */ - @Input() options?: JoditConfig = {}; + private _options?: JoditConfig = {}; + @Input() set options(value: JoditConfig) { + this._options = value; + + if (value) { + this.initJoditContainer(); + } + } - // value property - _value = ''; + // value property (subject) + private valueSubject: BehaviorSubject = new BehaviorSubject(''); @Input() set value(value: string) { - this._value = value; + const sanitizedText = this.prepareText(value); + this.valueSubject.next(sanitizedText); + this.onChange(sanitizedText); + } + + get value(): string { + return this.valueSubject.getValue(); } @Output() valueChange = new EventEmitter(); @@ -64,28 +78,29 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O @Output() joditAfterPaste = new EventEmitter(); @Output() joditChangeSelection = new EventEmitter(); - private inputValueChange = false; - - ngOnChanges(changes: SimpleChanges) { - if (changes['options']) { - // options changed - const options = changes['options'].currentValue; - - if (options) { - this.initJoditContainer(); - } - } - - if (changes['value'] && changes['value'].currentValue !== changes['value'].previousValue) { - if (this.jodit && !this.inputValueChange) { - this.inputValueChange = true; - this.jodit.value = this.isHTML(this._value) ? this._value : `

${this._value}

`; + // Used for delay value assignment to wait for jodit to be initialized + private joditInitializedSubject: BehaviorSubject = new BehaviorSubject(false); + private valueSubscription?: Subscription; + + constructor( + private readonly cdr: ChangeDetectorRef, + ) { + this.valueSubscription = combineLatest([ + // Handle value changes ... + this.valueSubject.asObservable().pipe(distinctUntilChanged()), + // ...additionally ensuring that the value is reapplied if the editor was not initialized when value was set + this.joditInitializedSubject.pipe(distinctUntilChanged(), filter(initialized => initialized)) + ]).pipe( + // Pass through the latest value in case of editor initialization + withLatestFrom(this.valueSubject), + // Prevent ExpressionChangedAfterItHasBeenCheckedError + delay(0) + ).subscribe(([[_, initialized], text]) => { + if (this.jodit && initialized) { + this.jodit.value = text; + this.onChange(text); } - - setTimeout(() => { - this.inputValueChange = false; - }, 0); - } + }); } isHTML(text: string) { @@ -103,18 +118,21 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O this.initJoditContainer(); } + ngOnDestroy() { + this.valueSubscription?.unsubscribe(); + this.jodit?.events.destruct(); + } + initJoditContainer() { if (this.joditContainer) { if (this.jodit) { this.jodit.destruct(); + this.joditInitializedSubject.next(false); } - this.jodit = Jodit.make(this.joditContainer.nativeElement, this.options); - this.jodit.value = this._value; + this.jodit = Jodit.make(this.joditContainer.nativeElement, this._options); + this.jodit.value = this.valueSubject.getValue(); this.jodit.events.on('change', (text: string) => { - if (!this.inputValueChange) { - this.inputValueChange = true; - this.changeValue(text); - } + this.changeValue(text); this.joditChange.emit(text); this.onChange(text); }); @@ -158,6 +176,8 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O this.jodit.events.on('changeSelection', () => { this.joditChangeSelection.emit(); }); + + this.joditInitializedSubject.next(true); } } @@ -165,10 +185,6 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O this.valueChange.emit(value); } - ngOnDestroy() { - this.jodit?.events.destruct(); - } - /* FUNCTIONS RELEVANT FOR ANGULAR FORMS */ @@ -182,16 +198,7 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O }; writeValue(text: string): void { - if (this.jodit && !this.inputValueChange) { - this.inputValueChange = true; - this._value = text; - this.jodit.value = this.isHTML(this._value) ? this._value : `

${this._value}

`; - this.onChange(text); - } - - setTimeout(() => { - this.inputValueChange = false; - }, 0); + this.valueSubject.next(this.prepareText(text)); } registerOnChange(fn: (text: string) => void): void { @@ -204,8 +211,12 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O setDisabledState?(isDisabled: boolean): void { this.options = { - ...this.options, + ...this._options, disabled: isDisabled }; } + + private prepareText(text: string) { + return this.isHTML(text) ? text : `

${text}

`; + } }