Skip to content

Commit 2ed554b

Browse files
authored
fix: reduce asynchronous stuff to resolve flickering (#1951)
1 parent 14f1d95 commit 2ed554b

File tree

5 files changed

+87
-74
lines changed

5 files changed

+87
-74
lines changed

projects/ngx-quill/config/src/quill-editor.interfaces.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { InjectionToken } from '@angular/core'
2+
import type { QuillOptions } from 'quill'
3+
import type { Observable } from 'rxjs'
24

35
import { defaultModules } from './quill-defaults'
4-
import type { QuillOptions } from 'quill'
56

67
export interface CustomOption {
78
import: string
@@ -62,6 +63,8 @@ export interface QuillModules {
6263

6364
export type QuillFormat = 'object' | 'json' | 'html' | 'text'
6465

66+
export type QuillBeforeRender = (() => Promise<any>) | (() => Observable<any>)
67+
6568
export interface QuillConfig {
6669
bounds?: HTMLElement | string
6770
customModules?: CustomModule[]
@@ -82,7 +85,7 @@ export interface QuillConfig {
8285
sanitize?: boolean
8386
// A function, which is executed before the Quill editor is rendered, this might be useful
8487
// for lazy-loading CSS.
85-
beforeRender?: () => Promise<any>
88+
beforeRender?: QuillBeforeRender
8689
}
8790

8891
export const QUILL_CONFIG_TOKEN = new InjectionToken<QuillConfig>('config', {

projects/ngx-quill/src/lib/quill-editor.component.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,7 +1457,7 @@ describe('QuillEditor - beforeRender', () => {
14571457
imports: [QuillModule.forRoot(config)],
14581458
})
14591459

1460-
spyOn(config, 'beforeRender')
1460+
spyOn(config, 'beforeRender').and.callThrough()
14611461

14621462
fixture = TestBed.createComponent(BeforeRenderTestComponent)
14631463
fixture.detectChanges()
@@ -1474,11 +1474,11 @@ describe('QuillEditor - beforeRender', () => {
14741474
imports: [QuillModule.forRoot(config)],
14751475
})
14761476

1477-
spyOn(config, 'beforeRender')
1477+
spyOn(config, 'beforeRender').and.callThrough()
14781478

14791479
fixture = TestBed.createComponent(BeforeRenderTestComponent)
14801480
fixture.componentInstance.beforeRender = () => Promise.resolve()
1481-
spyOn(fixture.componentInstance, 'beforeRender')
1481+
spyOn(fixture.componentInstance, 'beforeRender').and.callThrough()
14821482
fixture.detectChanges()
14831483
await fixture.whenStable()
14841484

projects/ngx-quill/src/lib/quill-editor.component.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { debounceTime, mergeMap } from 'rxjs/operators'
3434

3535
import { ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator } from '@angular/forms'
3636

37-
import { CustomModule, CustomOption, defaultModules, QuillModules } from 'ngx-quill/config'
37+
import { CustomModule, CustomOption, defaultModules, QuillBeforeRender, QuillModules } from 'ngx-quill/config'
3838

3939
import type History from 'quill/modules/history'
4040
import type Toolbar from 'quill/modules/toolbar'
@@ -92,7 +92,7 @@ export abstract class QuillEditorBase implements AfterViewInit, ControlValueAcce
9292
readonly formats = input<string[] | null | undefined>(undefined)
9393
readonly customToolbarPosition = input<'top' | 'bottom'>('top')
9494
readonly sanitize = input<boolean | undefined>(undefined)
95-
readonly beforeRender = input<() => Promise<any> | undefined>(undefined)
95+
readonly beforeRender = input<QuillBeforeRender>(undefined)
9696
readonly styles = input<any>(null)
9797
readonly registry = input<QuillOptions['registry']>(
9898
undefined
@@ -223,14 +223,7 @@ export abstract class QuillEditorBase implements AfterViewInit, ControlValueAcce
223223
// this will lead to runtime exceptions, since the code will be executed on DOM nodes that don't exist within the tree.
224224

225225
this.quillSubscription = this.service.getQuill().pipe(
226-
mergeMap((Quill) => {
227-
const promises = [this.service.registerCustomModules(Quill, this.customModules())]
228-
const beforeRender = this.beforeRender() ?? this.service.config.beforeRender
229-
if (beforeRender) {
230-
promises.push(beforeRender())
231-
}
232-
return Promise.all(promises).then(() => Quill)
233-
})
226+
mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))
234227
).subscribe(Quill => {
235228
this.editorElem = this.elementRef.nativeElement.querySelector(
236229
'[quill-editor-element]'

projects/ngx-quill/src/lib/quill-view.component.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,31 @@ import type QuillType from 'quill'
44
import {
55
AfterViewInit,
66
Component,
7+
DestroyRef,
78
ElementRef,
9+
EventEmitter,
810
Inject,
11+
NgZone,
912
OnChanges,
13+
OnDestroy,
14+
Output,
1015
PLATFORM_ID,
1116
Renderer2,
17+
SecurityContext,
1218
SimpleChanges,
1319
ViewEncapsulation,
14-
NgZone,
15-
SecurityContext,
16-
OnDestroy,
17-
input,
18-
EventEmitter,
19-
Output,
2020
inject,
21-
DestroyRef
21+
input
2222
} from '@angular/core'
2323
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
24-
import { Subscription } from 'rxjs'
24+
import { DomSanitizer } from '@angular/platform-browser'
25+
import type { Subscription } from 'rxjs'
2526
import { mergeMap } from 'rxjs/operators'
2627

27-
import { CustomOption, CustomModule, QuillModules } from 'ngx-quill/config'
28+
import { CustomModule, CustomOption, QuillBeforeRender, QuillModules } from 'ngx-quill/config'
2829

2930
import { getFormat, raf$ } from './helpers'
3031
import { QuillService } from './quill.service'
31-
import { DomSanitizer } from '@angular/platform-browser'
3232

3333
@Component({
3434
encapsulation: ViewEncapsulation.None,
@@ -52,7 +52,7 @@ export class QuillViewComponent implements AfterViewInit, OnChanges, OnDestroy {
5252
readonly debug = input<'warn' | 'log' | 'error' | false>(false)
5353
readonly formats = input<string[] | null | undefined>(undefined)
5454
readonly sanitize = input<boolean | undefined>(undefined)
55-
readonly beforeRender = input<() => Promise<any> | undefined>(undefined)
55+
readonly beforeRender = input<QuillBeforeRender>()
5656
readonly strict = input(true)
5757
readonly content = input<any>()
5858
readonly customModules = input<CustomModule[]>([])
@@ -74,7 +74,7 @@ export class QuillViewComponent implements AfterViewInit, OnChanges, OnDestroy {
7474
protected service: QuillService,
7575
protected domSanitizer: DomSanitizer,
7676
@Inject(PLATFORM_ID) protected platformId: any,
77-
) {}
77+
) { }
7878

7979
valueSetter = (quillEditor: QuillType, value: any): any => {
8080
const format = getFormat(this.format(), this.service.config.format)
@@ -114,14 +114,7 @@ export class QuillViewComponent implements AfterViewInit, OnChanges, OnDestroy {
114114
}
115115

116116
this.quillSubscription = this.service.getQuill().pipe(
117-
mergeMap((Quill) => {
118-
const promises = [this.service.registerCustomModules(Quill, this.customModules())]
119-
const beforeRender = this.beforeRender() ?? this.service.config.beforeRender
120-
if (beforeRender) {
121-
promises.push(beforeRender())
122-
}
123-
return Promise.all(promises).then(() => Quill)
124-
})
117+
mergeMap((Quill) => this.service.beforeRender(Quill, this.customModules(), this.beforeRender()))
125118
).subscribe(Quill => {
126119
const modules = Object.assign({}, this.modules() || this.service.config.modules)
127120
modules.toolbar = false
Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import { DOCUMENT } from '@angular/common'
2-
import { Inject, Injectable, Injector, Optional } from '@angular/core'
3-
import { defer, firstValueFrom, isObservable, Observable } from 'rxjs'
4-
import { shareReplay } from 'rxjs/operators'
2+
import { inject, Injectable } from '@angular/core'
3+
import { defer, firstValueFrom, forkJoin, from, isObservable, Observable, of } from 'rxjs'
4+
import { map, shareReplay, tap } from 'rxjs/operators'
55

66
import {
77
CustomModule,
88
defaultModules,
99
QUILL_CONFIG_TOKEN,
10-
QuillConfig,
10+
QuillConfig
1111
} from 'ngx-quill/config'
1212

1313
@Injectable({
1414
providedIn: 'root',
1515
})
1616
export class QuillService {
17+
readonly config = inject(QUILL_CONFIG_TOKEN) || { modules:defaultModules } as QuillConfig
18+
19+
private document = inject(DOCUMENT)
20+
1721
private Quill!: any
18-
private document: Document
22+
1923
private quill$: Observable<any> = defer(async () => {
2024
if (!this.Quill) {
2125
// Quill adds events listeners on import https://github.com/quilljs/quill/blob/develop/core/emitter.js#L8
@@ -54,54 +58,74 @@ export class QuillService {
5458
)
5559
})
5660

57-
return await this.registerCustomModules(
61+
return firstValueFrom(this.registerCustomModules(
5862
this.Quill,
5963
this.config.customModules,
6064
this.config.suppressGlobalRegisterWarning
61-
)
62-
}).pipe(shareReplay({ bufferSize: 1,
63-
refCount: true }))
64-
65-
constructor(
66-
injector: Injector,
67-
@Optional() @Inject(QUILL_CONFIG_TOKEN) public config: QuillConfig
68-
) {
69-
this.document = injector.get(DOCUMENT)
65+
))
66+
}).pipe(
67+
shareReplay({
68+
bufferSize: 1,
69+
refCount: false
70+
})
71+
)
7072

71-
if (!this.config) {
72-
this.config = { modules: defaultModules }
73-
}
74-
}
73+
// A list of custom modules that have already been registered,
74+
// so we don’t need to await their implementation.
75+
private registeredModules = new Set<string>()
7576

7677
getQuill() {
7778
return this.quill$
7879
}
7980

80-
/**
81-
* Marked as internal so it won't be available for `ngx-quill` consumers, this is only
82-
* internal method to be used within the library.
83-
*
84-
* @internal
85-
*/
86-
async registerCustomModules(
81+
/** @internal */
82+
beforeRender(Quill: any, customModules: CustomModule[] | undefined, beforeRender = this.config.beforeRender) {
83+
// This function is called each time the editor needs to be rendered,
84+
// so it operates individually per component. If no custom module needs to be
85+
// registered and no `beforeRender` function is provided, it will emit
86+
// immediately and proceed with the rendering.
87+
const sources = [this.registerCustomModules(Quill, customModules)]
88+
if (beforeRender) {
89+
sources.push(from(beforeRender()))
90+
}
91+
return forkJoin(sources).pipe(map(() => Quill))
92+
}
93+
94+
/** @internal */
95+
private registerCustomModules(
8796
Quill: any,
8897
customModules: CustomModule[] | undefined,
8998
suppressGlobalRegisterWarning?: boolean
90-
): Promise<any> {
91-
if (Array.isArray(customModules)) {
92-
// eslint-disable-next-line prefer-const
93-
for (let { implementation, path } of customModules) {
94-
// The `implementation` might be an observable that resolves the actual implementation,
95-
// e.g. if it should be lazy loaded.
96-
if (isObservable(implementation)) {
97-
implementation = await firstValueFrom(implementation)
98-
}
99-
Quill.register(path, implementation, suppressGlobalRegisterWarning)
99+
) {
100+
if (!Array.isArray(customModules)) {
101+
return of(Quill)
102+
}
103+
104+
const sources: Observable<unknown>[] = []
105+
106+
for (const customModule of customModules) {
107+
const { path, implementation: maybeImplementation } = customModule
108+
109+
// If the module is already registered, proceed to the next module...
110+
if (this.registeredModules.has(path)) {
111+
continue
112+
}
113+
114+
this.registeredModules.add(path)
115+
116+
if (isObservable(maybeImplementation)) {
117+
// If the implementation is an observable, we will wait for it to load and
118+
// then register it with Quill. The caller will wait until the module is registered.
119+
sources.push(maybeImplementation.pipe(
120+
tap((implementation) => {
121+
Quill.register(path, implementation, suppressGlobalRegisterWarning)
122+
})
123+
))
124+
} else {
125+
Quill.register(path, maybeImplementation, suppressGlobalRegisterWarning)
100126
}
101127
}
102128

103-
// Return `Quill` constructor so we'll be able to re-use its return value except of using
104-
// `map` operators, etc.
105-
return Quill
129+
return sources.length > 0 ? forkJoin(sources).pipe(map(() => Quill)) : of(Quill)
106130
}
107131
}

0 commit comments

Comments
 (0)