[]previous and next page

Angular

Use CanvasFlow in Angular with one function that mounts into an element and returns signals.

Angular needs no wrapper component: a canvas is an element and a few calls. canvasflow/angular gives you mountCanvasFlow(element, options), which mounts the canvas and the default UI and returns a ref whose state is Angular signals. It needs @angular/core 17 or newer, and works in standalone components and in modules.

A board in one component#

board.component.tsts
import { Component, ElementRef, ViewChild, type AfterViewInit, type OnDestroy } from '@angular/core'
import { mountCanvasFlow, type CanvasFlowRef } from 'canvasflow/angular'

@Component({
  selector: 'app-board',
  standalone: true,
  template: `<div #host class="board"></div>`,
  styles: [`.board { height: 100vh; }`],
})
export class Board implements AfterViewInit, OnDestroy {
  @ViewChild('host') host!: ElementRef<HTMLElement>
  flow!: CanvasFlowRef

  ngAfterViewInit() {
    this.flow = mountCanvasFlow(this.host.nativeElement, { persistenceKey: 'board' })
  }

  ngOnDestroy() {
    this.flow.destroy()
  }
}

Give the element a height, because an editor needs room. destroy() unmounts the canvas and every subscription made through the ref.

Options#

mountCanvasFlow takes everything createCanvasFlow accepts (persistenceKey, dir, readonly, themes, plugins, shapes, tools, storage…), plus:

Option What it does
ui false for no default UI, or an object that configures it (see Customizing the UI).
onMount Called once with (editor, app, ui). Return a function and it runs as a cleanup.

To change the direction later, call flow.app.setDirection('rtl').

Follow the editor#

The ref has signals for the common values, and value(selector) for anything else:

board.component.tsts
export class Board implements AfterViewInit, OnDestroy {
  // …
  selected = computed(() => this.flow?.selectedIds().length ?? 0)
}
On the ref What it is
editor, app, ui The editor, the app and the default UI (null when headless).
selectedIds, tool, canUndo, canRedo, theme, direction Signals, ready-made.
value(selector, isEqual?) A signal of any derived value. It changes only when the result does.
on(event, handler) Listen to an event. Returns a function that stops it.

Because they are signals, they work in templates and in computed() and effect():

toolbar.component.tsts
@Component({
  selector: 'app-toolbar',
  standalone: true,
  template: `
    <div role="toolbar">
      @for (id of tools; track id) {
        <button [attr.aria-pressed]="flow.tool() === id" (click)="flow.editor.setTool(id)">{{ id }}</button>
      }
      <button [disabled]="!flow.canUndo()" (click)="flow.editor.undo()">Undo</button>
    </div>
  `,
})
export class Toolbar {
  @Input({ required: true }) flow!: CanvasFlowRef
  tools = ['select', 'draw', 'rectangle', 'arrow', 'text']
}

Headless#

mountCanvasFlowCanvas(element, options) is the same without the default UI, and never references it, so your bundle does not carry the toolbar. Build your own, as above.

See Building your own UI for the full tour.

Zones and server rendering#

CanvasFlow handles its own input, and its state reaches your templates through signals, so no zone configuration is needed. With server rendering, call mountCanvasFlow from afterNextRender (or from ngAfterViewInit guarded by isPlatformBrowser), so it only runs in the browser.