synergycodes/ng-diagram/ng-diagram-groups

Claude Code and agent skill by synergycodes/ng-diagram

Trust tier 3
  • community
  • ext:[{"type":"template","description":"Custom templates for code generation"}]
Source check pending

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

Use when grouping ng-diagram nodes (isGroup, groupId) - containers, swimlanes, custom group templates, membership by drag and drop or code (canGroup, NgDiagramGroupsService), resizing or deleting groups, or children that move, vanish or overflow.

Install

npx vskill@latest install synergycodes/ng-diagram/ng-diagram-groups

Semi-extensibleMentions customization, but needs skill-specific knowledge. Read the extensible skills standard

SKILL.md

Groups

Group nodes and their children: model wiring, custom group templates, membership by drag and drop and from code, resizing, deleting, moving and fitting groups. Collapse/expand is in ng-diagram-visibility, z-order in ng-diagram-layering, copy/paste in ng-diagram-selection-clipboard.

Rules

  • A group is a node with `isGroup: true`; children point to it with `groupId`. A custom template alone gives no group behavior — type only picks the template.
  • All positions are global (diagram coordinates), for groups and children alike. Joining or leaving a group never changes a position. Dragging a group moves all its descendants, but moving from code moves only what you pass: updateNode(groupId, { position }) and moveNodesBy([group], …) leave the children in place.
  • Give groups `size` and `autoSize: false`. Otherwise the default group template is a fixed 9.0625rem square (145 × 145 px at the default font size), and a custom group template takes the size of its content.
  • Change membership through `NgDiagramGroupsService`. It applies grouping.canGroup, prevents cycles and fires groupMembershipChanged. Writing groupId yourself skips the first two.
  • Deleting a group deletes its whole subtree — with deleteSelection always, with NgDiagramModelService.deleteNodes since 1.4.0. To keep the children, remove them from the group first, in a separate awaited call.
  • Groups never resize themselves. Children can stick out after a move or a group resize; only resize.allowResizeBelowChildrenBounds: false stops a resize below the children.

Model shape

typescript
import { initializeModel, type GroupNode, type Node } from 'ng-diagram';
export const model = initializeModel({
nodes: [
// A group with an explicit size; without size + autoSize: false the default group is a fixed 145 × 145 px box
{ id: 'lane', isGroup: true, position: { x: 0, y: 0 }, size: { width: 500, height: 300 }, autoSize: false, data: {} },
// Children keep global coordinates
{ id: 'a', groupId: 'lane', position: { x: 40, y: 60 }, data: { label: 'A' } },
// Nested group
{ id: 'sub', isGroup: true, groupId: 'lane', position: { x: 220, y: 40 }, size: { width: 240, height: 200 }, autoSize: false, data: {} },
{ id: 'b', groupId: 'sub', position: { x: 260, y: 100 }, data: { label: 'B' } },
],
});
// Typed arrays: GroupNode requires `highlighted`
export const group: GroupNode = {
id: 'g2', isGroup: true, highlighted: false, position: { x: 600, y: 0 }, size: { width: 300, height: 200 }, autoSize: false, data: {},
};
export const nodes: Node[] = [group, { id: 'c', groupId: 'g2', position: { x: 640, y: 60 }, data: {} }];
// Narrow the Node union with `in` (`node.isGroup` alone does not compile)
export const isGroupNode = (node: Node): node is GroupNode => 'isGroup' in node && node.isGroup;

Custom group template and config

typescript
import { Component, input } from '@angular/core';
import {
NgDiagramGroupHighlightedDirective, NgDiagramNodeResizeAdornmentComponent, NgDiagramNodeSelectedDirective,
type GroupNode, type GroupingConfig, type NgDiagramConfig, type NgDiagramGroupNodeTemplate, type ResizeConfig,
} from 'ng-diagram';
@Component({
selector: 'app-lane-group',
imports: [NgDiagramNodeResizeAdornmentComponent, NgDiagramGroupHighlightedDirective, NgDiagramNodeSelectedDirective],
styles: `
:host { display: block; width: 100%; height: 100%; }
.lane { width: 100%; height: 100%; border: 1px solid #888; border-radius: 8px; }
`,
template: `
<ng-diagram-node-resize-adornment>
<div class="lane" ngDiagramGroupHighlighted ngDiagramNodeSelected [node]="node()">
<div class="header">{{ node().data.title }}</div>
</div>
</ng-diagram-node-resize-adornment>
`,
})
export class LaneGroupComponent implements NgDiagramGroupNodeTemplate<{ title: string }> {
node = input.required<GroupNode<{ title: string }>>();
}
const grouping: Partial<GroupingConfig> = {
// called for drag-and-drop membership, the drop highlight and addToGroup
canGroup: (node, group) => group.type === 'lane' && node.type !== 'lane',
};
const resize: Partial<ResizeConfig> = {
allowResizeBelowChildrenBounds: false, // default true: a group can be resized smaller than its children
getMinNodeSize: (node) => ('isGroup' in node && node.isGroup ? { width: 200, height: 120 } : { width: 80, height: 40 }),
};
export const config = { grouping, resize } satisfies NgDiagramConfig;
  • Register the component in the same NgDiagramNodeTemplateMap as node templates, set that type on the group node, and keep isGroup: true.
  • Build callback sections with their own type (Partial<GroupingConfig>, Partial<ResizeConfig>) as above: NgDiagramConfig drops callback signatures, so inline parameters are TS7006 under strict, and annotated ones still let a wrong return type compile (see ng-diagram-config).
  • ngDiagramGroupHighlighted [node] adds the class ng-diagram-group-highlight while node.highlighted is true (a drag hovers the group). The global styles then draw a highlight border and inset shadow: --ngd-group-border-color-highlight, --ngd-group-box-shadow-color-highlight, --ngd-group-highlight-size.
  • The default group template shows no text (it does not read data). Use a custom template for a title. Its look: --ngd-group-border-color, --ngd-group-border-color-hover, --ngd-group-border-radius.

Membership by drag and drop

  • On drop, the topmost unselected group under the pointer receives the dragged (visible, selected) nodes. When no group is under the pointer, the dropped nodes leave their group, unless their group is part of the drag. How much the node overlaps the group does not matter.
  • While dragging, the group under the pointer gets highlighted: true when canGroup accepts at least one dragged node.
  • grouping.canGroup(node, group) gets the candidate node first, then the group (default () => true). A node rejected on drop keeps its old groupId.
  • A palette drop never joins a group by itself — see the onPaletteDrop recipe below.

Membership and group operations from code

typescript
import { Component, inject } from '@angular/core';
import {
NgDiagramGroupsService, NgDiagramModelService, NgDiagramNodeService, NgDiagramSelectionService,
type GroupMembershipChangedEvent, type PaletteItemDroppedEvent,
} from 'ng-diagram';
@Component({ selector: 'app-group-toolbar', template: '' })
export class GroupToolbarComponent {
private readonly groupsService = inject(NgDiagramGroupsService);
private readonly modelService = inject(NgDiagramModelService);
private readonly nodeService = inject(NgDiagramNodeService);
private readonly selectionService = inject(NgDiagramSelectionService);
// Positions stay as they are (global coordinates); canGroup and cycle checks apply.
async addSelectionTo(groupId: string): Promise<void> {
const ids = this.selectionService.selection().nodes.map((n) => n.id).filter((id) => id !== groupId);
await this.groupsService.addToGroup(groupId, ids);
}
// deleteNodes deletes the whole subtree (since 1.4.0), so detach the children first.
// Use two awaited calls, not one transaction: commands in a transaction see the state before it.
async ungroup(groupId: string): Promise<void> {
const childIds = this.modelService.getChildren(groupId).map((n) => n.id);
await this.groupsService.removeFromGroup(groupId, childIds);
await this.modelService.deleteNodes([groupId]);
}
// moveNodesBy moves only the nodes you pass: include the descendants.
async moveGroup(groupId: string, dx: number, dy: number): Promise<void> {
const group = this.modelService.getNodeById(groupId);
if (!group) return;
await this.nodeService.moveNodesBy([group, ...this.modelService.getChildrenNested(groupId)], { x: dx, y: dy });
}
// Groups never grow on their own. Fit around the visible, measured children only:
// with none, computePartsBounds returns a 0×0 rect at (0, 0).
async fitToChildren(groupId: string, padding = 24): Promise<void> {
const children = this.modelService.getChildrenNested(groupId).filter((n) => !n.computedHidden && n.measuredBounds);
if (children.length === 0) return;
const bounds = this.modelService.computePartsBounds(children, []);
await this.modelService.updateNode(groupId, {
position: { x: bounds.x - padding, y: bounds.y - padding },
size: { width: bounds.width + 2 * padding, height: bounds.height + 2 * padding },
autoSize: false,
});
}
// <ng-diagram (paletteItemDropped)="onPaletteDrop($event)" ...>: a palette drop does not join a group by itself
async onPaletteDrop({ node, dropPosition }: PaletteItemDroppedEvent): Promise<void> {
const group = this.modelService
.getNodesInRange(dropPosition, 1)
.filter((n) => n.id !== node.id && 'isGroup' in n && n.isGroup)
.sort((a, b) => (b.computedZIndex ?? 0) - (a.computedZIndex ?? 0))[0]; // innermost group first
if (group) {
await this.groupsService.addToGroup(group.id, [node.id]);
}
}
// <ng-diagram (groupMembershipChanged)="onMembershipChanged($event)" ...>
onMembershipChanged({ grouped, ungrouped }: GroupMembershipChangedEvent): void {
for (const { targetGroup, nodes } of grouped) {
console.log(`${nodes.length} node(s) added to ${targetGroup.id}`);
}
console.log(`${ungrouped.length} node(s) left their group`);
}
}
  • NgDiagramGroupsService: addToGroup(groupId, nodeIds), removeFromGroup(groupId, nodeIds), highlightGroup(groupId, nodes), highlightGroupClear(). All return a Promise<void> (since 1.3.0).
  • addToGroup skips nodes already in that group, nodes that would create a cycle (a group into itself or into its own descendant) and nodes rejected by canGroup. It does not check that the target has isGroup: true. removeFromGroup changes only nodes whose groupId is the given id.
  • ungroup above makes the children top-level. For a nested group, addToGroup(parentId, childIds) keeps them in the outer group instead (canGroup applies, and a rejected child would be deleted with the group).
  • groupMembershipChanged ({ grouped: { nodes, targetGroup }[], ungrouped }) fires for drag and drop, addToGroup/removeFromGroup and updateNodes that change groupId — not for a single updateNode(id, { groupId }).
  • Queries: getChildren (direct), getChildrenNested (all descendants), getParentHierarchy(nodeId) (closest parent first), isNestedChild(nodeId, groupId).
  • fitToChildren relies on computePartsBounds(children, []), which uses the children's measuredBounds. The filter keeps a collapsed or not yet measured group where it is (without it the group would move to the origin). Before 1.4.0 an empty edge list also pulled the result to the point (0, 0).

Resizing groups

  • With resize.allowResizeBelowChildrenBounds: false a group cannot be resized smaller than the box around its visible children. Children count with their measuredBounds, so their ports and rotation count too.
  • Effectively hidden children never limit the resize (since 1.4.0), so a collapsed group can be resized freely.
  • Since 1.3.0: with resize snapping, a group resized from the bottom/right no longer jumps, and a snapped size that would cut children rounds up to the next snap value that still contains them.
  • NgDiagramNodeService.resizeNode applies the same rules and works on an unselected group since 1.3.0. A resize gesture, or resizeNode on a group with visible children, sets autoSize: false; for an empty (or collapsed) group pass disableAutoSize: true. Handles and snapping: ng-diagram-resize-rotate-snap.

Deleting, copying, hiding and layering

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

Repository stars
583
Repository forks
41
7-day trend score
0

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Extension points

  • templateCustom templates for code generation

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-groups/badge)](https://verified-skill.com/skills/synergycodes/ng-diagram/ng-diagram-groups)

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