Atelier Logo

Atelier

Guides

Angular — detailed integration

Standalone components, NgModule, reactive forms, validation patterns, and the wrapper component pattern.

Angular — detailed integration

Standalone component

AtelierEditorComponent is a standalone Angular component implementing ControlValueAccessor. Place it in the imports array — not declarations:

@Component({
  standalone: true,
  imports: [FormsModule, AtelierEditorComponent], // ← imports, not declarations
  // ...
})

NgModule (pre-standalone apps)

For apps that haven't migrated to standalone components, the standalone component still works — it goes in the module's imports array (not declarations):

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AtelierEditorComponent } from '@innosoft/atelier-editor-angular';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    FormsModule,
    AtelierEditorComponent // standalone component goes in imports[]
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Reactive forms

AtelierEditorComponent is a ControlValueAccessor, so it works with any form abstraction:

// Reactive form
import { FormControl } from '@angular/forms';
const contentCtrl = new FormControl<string>('', { nonNullable: true });
// In template: [formControl]="contentCtrl"
<!-- Template-driven with ngModel -->
<atelier-editor [(ngModel)]="htmlContent" name="content"></atelier-editor>

<!-- Reactive with FormControl -->
<atelier-editor [formControl]="contentCtrl"></atelier-editor>

<!-- Read-only through form -->
<atelier-editor [formControl]="contentCtrl" [disabled]="true"></atelier-editor>

Calling contentCtrl.disable() / contentCtrl.enable() propagates through setDisabledState() and dims the editor surface automatically.

Validation pattern (content + button state)

A common requirement is to keep a "Next" or "Submit" button disabled until the user has typed a minimum number of characters. There are two traps to avoid:

Trap 1 — circular dependency with Validators.required. If you put Validators.required on the content form field and the editor is not live-synced to the form, the form is always invalid, so the button is always disabled, so the action that would set content is never reachable.

Trap 2 — using a live-computed validation value but starting it non-zero. Starting a character-count check at, say, 30 means a < 15 check is already satisfied, masking the fact that the form's content field is still empty.

Correct pattern:

@Component({
  /* ... */
})
export class DocFormComponent {
  // 1. No Validators.required on content — validation is done via validationVal
  openForm = this._fb.group({
    nameAr: [null, [Validators.pattern(Patterns.OnlyArCharacters)]],
    nameEn: [null, [Validators.pattern(Patterns.OnlyEnCharacters)]],
    content: [null] // no required validator
  });

  // 2. Start at 0 so the button is disabled until the user types
  validationVal = 0;

  @ViewChild('editor') private editorRef!: TextAtelierComponent;

  // 3. Update validationVal whenever the editor content changes
  onEditorContentChange(html: string): void {
    this.validationVal = html
      .replace(/<[^>]+>|[\s]+/gm, '')
      .replace(/&nbsp;/g, ' ')
      .trim().length;
  }

  // 4. In EDIT mode — seed validationVal immediately from API content so the
  //    button is enabled without requiring the user to type first
  ngOnInit(): void {
    this._route.params
      .pipe(switchMap(p => (p['reqId'] ? this._service.getById(p['reqId']) : [])))
      .subscribe({
        next: res => {
          this.openForm.patchValue(res.data);
          // Seed validationVal from loaded content
          const apiContent = res.data?.content || '';
          this.validationVal = apiContent
            .replace(/<[^>]+>|[\s]+/gm, '')
            .replace(/&nbsp;/g, ' ')
            .trim().length;
        }
      });
  }

  // 5. The button checks validationVal only (form.invalid only catches pattern violations)
  //    Template: [disabled]="openForm.invalid || validationVal < 15"

  // 6. On submit: read HTML from the editor, set the form field, then navigate
  previewDoc(): void {
    const html = this.editorRef.exportHTML();
    this.openForm.controls['content'].setValue(html, { emitEvent: false });

    if (this.openForm.valid && this.validationVal >= 15) {
      this.openDocContent = html;
      this.stepper.next();
    }
  }
}

Wrapper component pattern

When embedding the editor in a host component that receives content via @Input and reports changes via @Output, follow these rules to avoid an infinite loop (see Preventing the infinite update loop):

// text-atelier.component.ts
@Component({
  standalone: true,
  selector: 'app-text-atelier',
  imports: [AtelierEditorComponent, FormsModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <atelier-editor
      #editor
      [starterKit]="true"
      [bold]="true"
      [italic]="true"
      [underline]="true"
      [rtl]="isRtl"
      [lang]="'ar'"
      [licenseKey]="licenseKey"
      [labels]="activeLabels"
      [ngModel]="editorContent"
      (ngModelChange)="onContentChange($event)"
    ></atelier-editor>
  `
})
export class TextAtelierComponent implements OnChanges {
  @ViewChild('editor') private editorRef!: AtelierEditorComponent;
  @Input() initialContent = '';
  @Output() contentChange = new EventEmitter<string>();

  readonly licenseKey = '...';
  isRtl = true;

  // IMPORTANT: always a stable readonly reference — never a getter returning {}
  private readonly emptyLabels: Partial<Record<string, string>> = {};
  get activeLabels(): Partial<Record<string, string>> {
    return this.emptyLabels;
  }

  editorContent = '';

  ngOnChanges(changes: SimpleChanges): void {
    if (changes['initialContent']) {
      const incoming = changes['initialContent'].currentValue as string;
      // Only update if different — prevents overwriting user edits
      if (incoming && incoming !== this.editorContent) {
        this.editorContent = incoming;
      }
    }
  }

  onContentChange(value: string): void {
    this.editorContent = value;
    this.contentChange.emit(value); // ← propagate to parent for validationVal
  }

  // Call this from the parent when you need the final HTML (e.g. on submit)
  exportHTML(): string {
    return this.editorRef.getHTML();
  }
}

Parent usage:

<app-text-atelier
  #editor
  [initialContent]="openForm.get('content')?.value || ''"
  (contentChange)="onEditorContentChange($event)"
></app-text-atelier>

On this page