synergycodes/ng-diagram/ng-diagram-model-init

Claude Code and agent skill by synergycodes/ng-diagram

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when creating, loading or swapping an ng-diagram model (initializeModel), waiting for diagramInit/isInitialized, edits lost after a model swap or @if/tab switch, or "Library engine not initialized yet", NG0203, "Measurement timeout reached".

Install

npx vskill@latest install synergycodes/ng-diagram/ng-diagram-model-init

SKILL.md

ng-diagram model initialization

Create models with initializeModel, load and switch them, know when service calls work, wait for initialization, and fix init-time warnings. Save/load format and custom adapters: ng-diagram-persistence. Model fields: ng-diagram-fundamentals (references/model-reference.md).

Rules

  • Create every model with `initializeModel(data?, injector?, options?)`, or initializeModelAdapter(adapter, data?, injector?, options?) for your own ModelAdapter (since 1.1.0). They strip stale runtime fields and add internal ids; an adapter that skips this step can block measurement at init.
  • Pass an `Injector` outside an injection context: after an await, in event handlers and timers, and inside computed, effect and linkedSignal. Field initializers and constructors need none. Without it Angular throws NG0203 (effect() can only be used within an injection context ...).
  • A new model object means a full re-initialization: the engine is destroyed and created again (from the current [config]), every element is measured again, isInitialized() goes false and back to true, and (diagramInit) fires again. Binding the same object again does nothing.
  • Service methods throw `[ngDiagram] Library engine not initialized yet.` until `<ng-diagram>` has created its engine, for example in the constructor or ngOnInit of the component that renders it, or of a toolbar or sidebar declared next to <ng-diagram> in the same template. Start your logic from (diagramInit) or an isInitialized() effect.
  • Calls right after you assign a new model reach the old one. The swap happens on the next change detection, and the old model is then thrown away with your changes. Put the data into initializeModel(...), or make the calls in (diagramInit).
  • A model belongs to one diagram instance. When <ng-diagram> is destroyed or gets another model, the old adapter is destroyed: the default adapter clears its nodes and edges and stops notifying. Create a new model each time the diagram is created again (an @if, a tab, a route), from content you saved before the switch (recipe below).

Lifecycle

  1. <ng-diagram> renders and creates the engine from [model], [config] and [middlewares]. Service methods work from here, but nothing is measured yet.
  2. Nodes, ports and labels render on a hidden canvas; a ResizeObserver measures them.
  3. When every visible element is measured, the init pass runs (edge routing, z-index) and the engine emits diagramInit.
  4. Then isInitialized() becomes true, the (diagramInit) output fires, zoom.zoomToFit.onInit runs if enabled, and the canvas becomes visible.

The nodes(), edges() and metadata() signals of NgDiagramModelService start updating only after step 4.

Static model, react to init

typescript
@Component({
selector: 'app-diagram',
imports: [NgDiagramComponent],
providers: [provideNgDiagram()],
template: `<ng-diagram [model]="model" (diagramInit)="onDiagramInit($event)" />`,
styles: `:host { display: flex; height: 100%; }`,
})
export class DiagramComponent {
private readonly viewport = inject(NgDiagramViewportService);
// Field initializer = injection context, so no injector is needed here
model = initializeModel({
nodes: [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'A' } },
{ id: 'b', position: { x: 300, y: 0 }, data: { label: 'B' } },
],
edges: [{ id: 'a-b', source: 'a', sourcePort: 'port-right', target: 'b', targetPort: 'port-left', data: {} }],
});
async onDiagramInit(event: DiagramInitEvent): Promise<void> {
console.log(event.nodes.map((node) => node.size)); // measured sizes are available here
await this.viewport.zoomToFit({ padding: 40 });
}
}

To fit on every load without code, set zoom: { zoomToFit: { onInit: true } } in [config] (not with virtualization, see Edge cases).

Load from an API or switch diagrams

typescript
@Component({
selector: 'app-diagram-page',
imports: [NgDiagramComponent],
providers: [provideNgDiagram()],
template: `
<ng-diagram [model]="model()" />
@if (!ready()) {
<div class="loading">Loading…</div>
}
`,
styles: `:host { display: flex; height: 100%; position: relative; }`,
})
export class DiagramPageComponent {
private readonly injector = inject(Injector);
private readonly api = inject(DiagramApi); // your data service
readonly ready = inject(NgDiagramService).isInitialized; // false again during every re-init
readonly model = signal(initializeModel());
private currentId: string | null = null;
async open(id: string): Promise<void> {
if (id === this.currentId) return; // every new model means a full re-initialization
this.currentId = id;
const data = await this.api.load(id);
// after an await there is no injection context: pass the injector
this.model.set(initializeModel(data, this.injector));
}
}
  • A signal keeps the swap visible to OnPush and zoneless components.
  • Work that must follow each load (layout, selection, focus) belongs in (diagramInit), which fires after every initialization.

Keep the content when the diagram leaves and comes back

When an @if, a tab or a route removes <ng-diagram>, its model is destroyed and emptied. Save the content while <ng-diagram> still exists, for example in the handler that switches the tab, and create the next model from it. ngOnDestroy of the component that renders <ng-diagram> is too late: Angular destroys <ng-diagram> first, so service calls throw and the default model is already empty. Content that must survive any teardown (a route change) belongs in your own store (ng-diagram-persistence).

typescript
@Component({
selector: 'app-editor-tabs',
imports: [NgDiagramComponent],
providers: [provideNgDiagram()],
template: `
<button (click)="show('diagram')">Diagram</button>
<button (click)="show('table')">Table</button>
@if (tab() === 'diagram') {
<div class="canvas"><ng-diagram [model]="model()" /></div>
}
`,
styles: `.canvas { display: flex; height: 500px; }`,
})
export class EditorTabsComponent {
private readonly injector = inject(Injector);
private readonly modelService = inject(NgDiagramModelService);
readonly tab = signal<'diagram' | 'table'>('diagram');
readonly model = signal(initializeModel());
private saved: Partial<Model> = {};
show(tab: 'diagram' | 'table'): void {
if (tab === this.tab()) return;
if (this.tab() === 'diagram') {
// <ng-diagram> still exists here; in ngOnDestroy it is already destroyed and its model is empty
this.saved = JSON.parse(this.modelService.toJSON()) as Partial<Model>;
} else {
// no <ng-diagram> exists now: give the next one a fresh model
this.model.set(initializeModel(this.saved, this.injector));
}
this.tab.set(tab);
}
}

Reactive model from a signal (since 1.2.0)

typescript
private readonly injector = inject(Injector);
readonly diagramData = signal<Partial<Model>>({});
// computed is not an injection context: the injector is required
readonly model = computed(() => initializeModel(this.diagramData(), this.injector));

Every new diagramData value is a full re-initialization; setting the same object again is skipped. Do not feed sources that change often: apply small changes with NgDiagramModelService instead (ng-diagram-state-updates). Before 1.2.0, initializeModel inside computed threw NG0602.

Wait for init in another component

typescript
@Component({
selector: 'app-outline',
template: `@for (node of nodes(); track node.id) { <div>{{ node.id }}</div> }`,
})
export class OutlineComponent {
private readonly diagram = inject(NgDiagramService);
private readonly modelService = inject(NgDiagramModelService);
private readonly selection = inject(NgDiagramSelectionService);
readonly nodes = this.modelService.nodes; // [] until the diagram is initialized
constructor() {
// Runs after the first init and again after every re-init (new model)
effect(() => {
if (this.diagram.isInitialized()) {
untracked(() => this.selectFirstNode());
}
});
}
private selectFirstNode(): void {
const first = this.modelService.getModel().getNodes()[0]; // service calls are safe now
if (first) void this.selection.select([first.id]);
}
}

The three ways to wait:

  • (diagramInit) on <ng-diagram>: fires after every initialization and survives model swaps.
  • isInitialized() from NgDiagramService: a signal for templates and effects, in any component under the same provideNgDiagram().
  • addEventListenerOnce('diagramInit', callback): registers on the current engine only. It throws before the engine exists, and a listener added just before a model swap dies with the old engine. Inside it, isInitialized() is still false. Prefer the two above.

Keep runtime fields from saved data (since 1.3.0)

initializeModel() removes runtime fields from every element: on nodes selected, measuredPorts, measuredBounds, computedZIndex, computedHidden; on edges measuredLabels, computedZIndex, computedHidden and sourcePosition/targetPosition of connected ends. InitializeModelOptions replaces these strip functions, at your own risk: wrap the default and add back only what you need.

typescript
// At your own risk: keep node selection from saved data (stripped by default)
model = initializeModel(savedData, undefined, {
stripNodeRuntimeProperties: (node) => ({ ...stripNodeRuntimeProperties(node), selected: node.selected }),
});

Keeping measured fields (measuredPorts, measuredLabels, ...) makes the library skip fresh measurement and breaks the diagram. For the default model the same functions also run in toJSON().

Measurement timeout at init

Initialization waits until every visible node, port and label has a real size (width and height above 0). Elements that never get one keep the canvas hidden until 2-second safety timeouts expire, and the console shows:

  • [InitUpdater] Measurement timeout reached. Some entities may not be measurable (e.g., display: none). ... with { nodes: { expected, measured }, ports: ..., labels: ... }. Init is then forced to finish.
  • [DiagramInitEmitter] Measurement timeout reached from last measurement. Emitting diagramInit event anyway. ..., then Total unmeasured elements: N and lists such as Unmeasured nodes (2): ['a', 'b'] or Unmeasured node ports (1): ['a:port-left']. This one fires 2 s after the last measurement progress, so a broken load can take about 4 s.

Causes and fixes:

CauseFix
Nodes, ports or labels hidden with CSS display: noneUse the hidden flag on nodes and edges or the hidden input on ports and labels (since 1.4.0); hidden elements create no measurement expectations. See ng-diagram-visibility.
<ng-diagram> inside a hidden container (inactive tab, collapsed panel, display: none parent)Create the diagram component, with a new model, only when the container is visible (@if).
A custom ModelAdapter bound without initializeModelAdapter()Pass it through initializeModelAdapter(adapter, data?, injector?) first.
A node, port or label element in your template renders with zero width or heightGive it a size (ng-diagram-custom-nodes, ng-diagram-ports, ng-diagram-custom-edges).

A rare [ngDiagram] Initialization stabilization failed. (console error) also forces init to finish; the message lists the same causes.

Edge cases

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
0

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/synergycodes/ng-diagram/ng-diagram-model-init/badge)](https://verified-skill.com/skills/synergycodes/ng-diagram/ng-diagram-model-init)

Something wrong with this skill or its page? Tell us.