Monter le composant standalone

Après l’installation npm et la configuration AOT/linker, importez FloworaEditorComponent depuis @newvexa/flowora/angular. Son sélecteur est flowora-editor ; l’entrée editor est obligatoire. Cet exemple minimal n’utilise pas ELK.

TypeScript · composant autonome minimal
import { Component, OnDestroy } from '@angular/core';
import { Editor, createDemoDocument } from '@newvexa/flowora';
import { FloworaEditorComponent } from '@newvexa/flowora/angular';

@Component({
  selector: 'app-workflow-editor',
  standalone: true,
  imports: [FloworaEditorComponent],
  styles: [':host { display: block; height: 100vh; }'],
  template: '<flowora-editor [editor]="editor" />',
})
export class WorkflowEditorComponent implements OnDestroy {
  readonly editor = new Editor(createDemoDocument());
  ngOnDestroy(): void {
    this.editor.dispose();
  }
}

La hauteur explicite de l’hôte est nécessaire : un canvas dans un conteneur sans hauteur ne constitue pas une intégration complète. Le renderer demande au moins 420 px de hauteur. Dans une mise en page flex, prévoyez la place du header et des panneaux. Créez l’éditeur une seule fois, jamais dans un getter du template.

Un service par écran, pas un singleton partagé par accident

Le service suivant possède le store et expose son snapshot dans un signal Angular. Le providers du composant crée une session pour chaque écran. Un service providedIn: 'root' partagerait au contraire son document entre les consommateurs et vivrait plus longtemps : ne choisissez cette portée que délibérément.

TypeScript · service de session et nettoyage
// workflow-session.service.ts
import { Injectable, OnDestroy, signal } from '@angular/core';
import { Editor, createDemoDocument } from '@newvexa/flowora';
import type { EditorSnapshot } from '@newvexa/flowora';

@Injectable()
export class WorkflowSession implements OnDestroy {
  readonly editor = new Editor(createDemoDocument(), { historyLimit: 64 });
  private readonly state = signal<EditorSnapshot>(this.editor.getSnapshot());
  readonly snapshot = this.state.asReadonly();
  private readonly off = this.editor.subscribe(() => {
    this.state.set(this.editor.getSnapshot());
  });

  addResponse(): void {
    this.editor.addFromCatalog('response', { x: 320, y: 520 });
  }

  ngOnDestroy(): void {
    this.off();
    this.editor.dispose();
  }
}

Le composant Angular nettoie ses propres abonnements et observateurs quand il est détruit ou reçoit un autre store. Il ne devient pas propriétaire de votre instance. Ici, le service appelle son désabonnement puis dispose() ; un composant qui emprunte le store ne doit pas le libérer à sa place.

Personnaliser nœud et inspector

nodeTemplate reçoit $implicit (un EditorNode), selected et preset, qui peut être absent. inspectorTemplate reçoit $implicit et update(patch: NodePatch). Utilisez cette fonction ou editor.updateNode() : aucune mutation directe du snapshot.

Les cartes custom respectent les dimensions du nœud. Les ports et la zone des propriétés restent gérés par le renderer : ne les couvrez pas avec un overlay. Pour un bouton ou champ inclus dans une carte custom, arrêtez pointerdown s’il ne doit pas lancer le déplacement. Appliquez la même précaution à vos formulaires ; ne bloquez pas tous les événements de toute la carte.

TypeScript · écran DI, deux templates et sorties typées
// workflow-page.component.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import type { EditorDocument, EditorNode, NodePatch } from '@newvexa/flowora';
import { FloworaEditorComponent } from '@newvexa/flowora/angular';
import { WorkflowSession } from './workflow-session.service';

@Component({
  selector: 'app-workflow-page',
  standalone: true,
  imports: [FloworaEditorComponent],
  providers: [WorkflowSession],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styles: [
    ':host { display: flex; flex-direction: column; height: 100vh; }',
    'flowora-editor { flex: 1; min-height: 420px; }',
    '.card { box-sizing: border-box; height: 100%; padding: 14px 32px 14px 14px; overflow: hidden; border: 1px solid #deded4; border-radius: 10px; background: #fffefd; }',
    '.card strong { display: block; overflow-wrap: anywhere; }',
    '.selected { border-color: #c45c35; }',
  ],
  template: `
    <header>
      <button type="button" (click)="session.addResponse()">Ajouter une réponse</button>
      <span>{{ session.snapshot().document.nodes.length }} nœuds</span>
      <span role="status">{{ persistenceStatus }}</span>
    </header>
    <flowora-editor
      [editor]="session.editor"
      [nodeTemplate]="customNode"
      [inspectorTemplate]="customInspector"
      (saveRequested)="receiveSave($event)"
      (exportRequested)="receiveExport($event)"
    />
    <ng-template #customNode let-node let-selected="selected" let-preset="preset">
      <div class="card" [class.selected]="selected">
        <small>{{ preset?.label ?? node.type }}</small>
        <strong>{{ node.name }}</strong>
      </div>
    </ng-template>
    <ng-template #customInspector let-node let-update="update">
      <label>
        Nom du nœud
        <input #nameInput [value]="node.name"
          (pointerdown)="$event.stopPropagation()"
          (change)="update({ name: nameInput.value })" />
      </label>
      <button type="button" (pointerdown)="$event.stopPropagation()"
        (click)="markReviewed(node, update)">
        Marquer comme relu
      </button>
    </ng-template>
  `,
})
export class WorkflowPageComponent {
  readonly session = inject(WorkflowSession);
  persistenceStatus = 'Aucune sauvegarde durable configurée';
  pendingSave: EditorDocument | null = null;
  pendingExport: EditorDocument | null = null;

  markReviewed(node: EditorNode, update: (patch: NodePatch) => void): void {
    update({ config: { ...node.config, reviewed: true } });
  }

  receiveSave(document: EditorDocument): void {
    this.pendingSave = document;
    this.persistenceStatus = 'Document reçu en mémoire, non enregistré';
  }

  receiveExport(document: EditorDocument): void {
    this.pendingExport = document;
    this.persistenceStatus = 'Export reçu en mémoire, aucun fichier téléchargé';
  }
}

Les deux fichiers précédents forment un écran réutilisable. Dans une application standalone, l’entrée suivante le monte ; le document hôte contient un élément app-workflow-page. Le bundler et le linker doivent traiter le package Angular avant le chargement.

TypeScript · point d’entrée de l’application hôte
// main.ts · dans une application Angular compilée AOT avec linker
import { bootstrapApplication } from '@angular/platform-browser';
import { WorkflowPageComponent } from './workflow-page.component';

bootstrapApplication(WorkflowPageComponent).catch((error: unknown) => {
  console.error(error);
});

Des demandes de persistance, pas un cloud

saveRequested et exportRequested émettent un EditorDocument. Dans cet exemple, les handlers le conservent seulement en mémoire et le disent explicitement : une demande traitée n’est pas une sauvegarde ni un fichier exporté. Remplacez-les par votre stockage et votre téléchargement, par exemple les fonctions du guide JSON/localStorage. Le renderer propose un téléchargement JSON par défaut si aucun handler d’export n’est attaché.

documentChange observe les changements persistables. Regroupez les écritures : un geste peut émettre des changements intermédiaires. Un abonnement ne fournit pas à lui seul une autosauvegarde fiable ou une protection contre le quota.

Entrées facultatives

layoutAdapter accepte un LayoutAdapter ; passez une instance d’ElkLayoutAdapter si le peer ELK est installé. connectionPath accepte une fonction (source: Point, target: Point) => string produisant le chemin SVG. Changer sa courbe ne change pas la validité des ports. Les variables CSS --flowora-accent, --flowora-surface et --flowora-font offrent une base de thème sur l’hôte.

Les composants métier restent dans votre application.

Réutilisez vos formulaires dans les templates, mais conservez secrets, authentification et exécution côté hôte. Le renderer affiche le document courant, pas l’état d’un backend inexistant.