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