Skip to content

Commit

Permalink
Merge pull request #27 from gschafra/fix/control_value_accessor
Browse files Browse the repository at this point in the history
fix(ngx-jodit): Fixed value handling for reactive forms
  • Loading branch information
julianpoemp authored Jan 24, 2024
2 parents 32923e7 + 5c6aa57 commit fb03272
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 62 deletions.
18 changes: 17 additions & 1 deletion apps/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,23 @@
README</a>.
</p>

<ngx-jodit [(ngModel)]="value" [options]="options" #ngxJodit></ngx-jodit>
<div class="container">
<div class="row">
<div class="col">
<h4>Template driven form</h4>
<ngx-jodit [(ngModel)]="value" [options]="options" #ngxJodit></ngx-jodit>
<div class="py-3">Value: {{ value | json }}</div>
</div>
<div class="col">
<h4>Reactive form</h4>
<form [formGroup]="formGroup" class="my-0">
<ngx-jodit [options]="options" formControlName="editor"></ngx-jodit>
</form>
<div class="py-3">Value: {{ this.formGroup.get("editor")?.value | json }}</div>
</div>
</div>
</div>

<h3 class="pt-3">Options</h3>
<p>
All
Expand Down
21 changes: 15 additions & 6 deletions apps/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
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',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
value = 'Some text';
formGroup = this.formBuilder.group<FormWithJoditEditor>({
editor: 'Some text in a reactive form'
});
_optionsStr = '';

@ViewChild('ngxJodit') ngxJodit?: NgxJoditComponent;
Expand All @@ -36,6 +45,6 @@ export class AppComponent {

options: JoditConfig = {};

constructor() {
constructor(private formBuilder: FormBuilder) {
}
}
4 changes: 2 additions & 2 deletions apps/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
})
Expand Down
7 changes: 7 additions & 0 deletions libs/ngx-jodit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<ngx-jodit [(ngModel)]="value" [options]="options"></ngx-jodit>
```
- Reactive
```angular2html
<form [formGroup]="formGroup">
<ngx-jodit [options]="options" formControlName="editor"></ngx-jodit>
</form>
```

## How to import plugins

Expand Down
117 changes: 64 additions & 53 deletions libs/ngx-jodit/src/lib/ngx-jodit.component.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -30,20 +31,33 @@ 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;

/**
* 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<string> = new BehaviorSubject<string>('');
@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<string>();
Expand All @@ -64,28 +78,29 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O
@Output() joditAfterPaste = new EventEmitter<ClipboardEvent>();
@Output() joditChangeSelection = new EventEmitter<void>();

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 : `<p>${this._value}</p>`;
// Used for delay value assignment to wait for jodit to be initialized
private joditInitializedSubject: BehaviorSubject<boolean> = 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) {
Expand All @@ -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);
});
Expand Down Expand Up @@ -158,17 +176,15 @@ export class NgxJoditComponent implements ControlValueAccessor, AfterViewInit, O
this.jodit.events.on('changeSelection', () => {
this.joditChangeSelection.emit();
});

this.joditInitializedSubject.next(true);
}
}

changeValue(value: string) {
this.valueChange.emit(value);
}

ngOnDestroy() {
this.jodit?.events.destruct();
}

/*
FUNCTIONS RELEVANT FOR ANGULAR FORMS
*/
Expand All @@ -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 : `<p>${this._value}</p>`;
this.onChange(text);
}

setTimeout(() => {
this.inputValueChange = false;
}, 0);
this.valueSubject.next(this.prepareText(text));
}

registerOnChange(fn: (text: string) => void): void {
Expand All @@ -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 : `<p>${text}</p>`;
}
}

0 comments on commit fb03272

Please sign in to comment.