Preventing the infinite update loop
The most common Text Atelier integration mistake, and the four things that trigger it.
Preventing the infinite update loop
This is the most common integration mistake. The editor emits an editor-change event when
content changes. Your code writes the new HTML back into the binding. If that triggers another
change, you get an endless cycle that either freezes the page or causes rapid re-renders
("infinite loading").
The current wrappers include internal guards against the most common forms of this loop, but you must avoid the four external triggers described below.
Root cause A — object-identity instability
labels and theme accept objects. The underlying Lit element compares them by reference.
If a new object is created on every render cycle, Lit sees a changed input, re-renders, the
editor fires editor-change, your framework updates state, triggers another render — and the
cycle repeats.
Wrong — Angular (getter returning a new object):
// Every call returns a different object reference → infinite change detection loop
get activeLabels(): Partial<Record<string, string>> {
return {}; // ← new object on every CD cycle
}Wrong — React (inline object):
// New object identity on every render
<AtelierEditor labels={{ bold: 'Bold' }} />Correct — Angular:
// Private readonly field — created once, never replaced
private readonly emptyLabels: Partial<Record<string, string>> = {};
get activeLabels(): Partial<Record<string, string>> {
return this.emptyLabels; // ← same reference every time
}Correct — React:
// Module scope: created once per module load
const LABELS = { bold: 'Bold' } as const;
// Or useMemo when the values depend on state/props
const labels = useMemo(() => ({ bold: readOnly ? 'Text' : 'Bold' }), [readOnly]);Correct — Vue:
// Outside setup() or in a computed with stable dependencies
const labels = computed(() => ({ bold: 'Gras' }));
// — or define at module scope if truly static
const LABELS = { bold: 'Gras' } as const;Root cause B — ControlValueAccessor echo cycle (Angular)
Specific to Angular's ngModel / reactive forms integration. The cycle looks like:
writeValue(html) sets [src] on the web component
→ web component fires `editor-change`
→ AtelierEditorComponent.onEditorChange calls onChange()
→ Angular updates the form model
→ Angular calls writeValue() again
→ repeat foreverThis is handled internally by AtelierEditorComponent using an isWritingValue flag that gates
onEditorChange during writeValue execution — make sure you're on package version ≥ 0.2.1. If
you bypass the wrapper and use <atelier-editor-element> directly, replicate the guard:
private isWritingValue = false;
onEditorChange(e: Event): void {
if (this.isWritingValue) return;
this.onChange((e as CustomEvent<string>).detail);
}
writeValue(value: string | null | undefined): void {
this.isWritingValue = true;
this.rawHtmlContent = value ?? '';
Promise.resolve().then(() => { this.isWritingValue = false; });
}Root cause C — transforming content inside the change handler
The editor tracks its current HTML internally. When you write a value back via the binding, the
element compares it to the tracked value. A transformed string (trimmed, reformatted, sanitized)
never matches, so the editor resets its content — which fires editor-change again — which you
transform again — infinite loop.
// Wrong — any transformation breaks identity
onEditorChange(html: string): void {
this.content = html.trim();
this.content = DOMPurify.sanitize(html);
}
// Correct — store verbatim, transform only on save
onEditorChange(html: string): void {
this.content = html;
}
saveToServer(): void {
const safeHtml = DOMPurify.sanitize(this.content);
this._api.save(safeHtml).subscribe();
}Root cause D — remounting on content change
Keying a component off the content value (or using *ngIf / conditional rendering on the
content) destroys and recreates the editor on every change. This looks like an infinite "loading"
flash — the editor mounts, fires editor-change with initial content, that updates the key, the
component unmounts and remounts, repeat.
<!-- Wrong — Angular: *ngIf tied to content truthy-ness -->
<atelier-editor *ngIf="content" [(ngModel)]="content"></atelier-editor>{/* Wrong — React: key tied to content, remounts on every keystroke */}
<AtelierEditor key={content} src={content} onChange={e => setContent(e.detail)} />Correct: mount the editor once and keep it mounted. Let it manage its own content through the binding.
<!-- Angular: always rendered, controlled through ngModel / formControl -->
<atelier-editor [(ngModel)]="content"></atelier-editor>{/* React: stable mount, content flows through src prop */}
<AtelierEditor src={content} onChange={e => setContent(e.detail)} />If your editor freezes or re-renders endlessly while typing, it's almost always Root cause A or C. Check for inline object/array props first, then check your change handler for any string transformation.
