diff --git a/apps/angular-demo/src/app/app.component.ts b/apps/angular-demo/src/app/app.component.ts index 02da6f20d..27d3ea706 100644 --- a/apps/angular-demo/src/app/app.component.ts +++ b/apps/angular-demo/src/app/app.component.ts @@ -18,6 +18,7 @@ import { nodeTemplateMap } from './data/node-template'; import { paletteModel } from './data/palette-model'; import { ButtonEdgeComponent } from './edge-template/button-edge/button-edge.component'; import { CustomPolylineEdgeComponent } from './edge-template/custom-polyline-edge/custom-polyline-edge.component'; +import { DashedEdgeComponent } from './edge-template/dashed-edge/dashed-edge.component'; import { LabelledEdgeComponent } from './edge-template/labelled-edge/labelled-edge.component'; import { PaletteComponent } from './palette/palette.component'; import { ToolbarComponent } from './toolbar/toolbar.component'; @@ -37,6 +38,7 @@ export class AppComponent { ['button-edge', ButtonEdgeComponent], ['custom-polyline-edge', CustomPolylineEdgeComponent], ['labelled-edge', LabelledEdgeComponent], + ['dashed-edge', DashedEdgeComponent], ]); config: NgDiagramConfig = { @@ -248,6 +250,16 @@ export class AppComponent { type: 'labelled-edge', routing: 'bezier', }, + { + id: '6', + source: '11', + target: '12', + data: {}, + sourcePort: 'port-right', + targetPort: 'port-left', + type: 'dashed-edge', + routing: 'orthogonal', + }, ], metadata: { viewport: { x: 300, y: 0, scale: 1 }, diff --git a/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.html b/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.html index 2ce50fa80..ece4754d7 100644 --- a/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.html +++ b/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.html @@ -1,10 +1,4 @@ - + diff --git a/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.scss b/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.scss index 9f4a889ea..354c9de2d 100644 --- a/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.scss +++ b/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.scss @@ -1,24 +1,24 @@ -:root { +:host { --border-color: red; --border-color-selected: #9f0000; } -.custom-edge { - &:has(.ng-diagram-edge__path.selected) { - .ng-diagram-edge__path.selected { - stroke: var(--border-color-selected); - } +ng-diagram-base-edge.custom-edge { + --edge-stroke: var(--border-color); +} - button { - border-color: var(--border-color-selected); - } - } +ng-diagram-base-edge.custom-edge.selected { + --edge-stroke: var(--border-color-selected); button { - pointer-events: all; - background-color: white; - border: 2px solid var(--border-color); - border-radius: 4px; - cursor: pointer; + border-color: var(--border-color-selected); } } + +button { + pointer-events: all; + background-color: white; + border: 2px solid var(--border-color); + border-radius: 4px; + cursor: pointer; +} diff --git a/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.ts b/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.ts index 22a0a51e8..1dfae6ee8 100644 --- a/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.ts +++ b/apps/angular-demo/src/app/edge-template/button-edge/button-edge.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, computed, input, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; import { BaseEdgeLabelComponent, Edge, NgDiagramBaseEdgeComponent, NgDiagramEdgeTemplate } from 'ng-diagram'; /** @@ -14,7 +14,6 @@ import { BaseEdgeLabelComponent, Edge, NgDiagramBaseEdgeComponent, NgDiagramEdge templateUrl: './button-edge.component.html', styleUrls: ['./button-edge.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, imports: [NgDiagramBaseEdgeComponent, BaseEdgeLabelComponent], }) export class ButtonEdgeComponent implements NgDiagramEdgeTemplate { diff --git a/apps/angular-demo/src/app/edge-template/dashed-edge/dashed-edge.component.scss b/apps/angular-demo/src/app/edge-template/dashed-edge/dashed-edge.component.scss new file mode 100644 index 000000000..ccbc72ddd --- /dev/null +++ b/apps/angular-demo/src/app/edge-template/dashed-edge/dashed-edge.component.scss @@ -0,0 +1,14 @@ +:host { + --edge-stroke: #666; + --edge-stroke-dasharray: 5 5; +} + +ng-diagram-base-edge.dashed-edge.selected { + --edge-stroke: blue; + --edge-stroke-width: 3; + --edge-stroke-dasharray: 8 4; +} + +ng-diagram-base-edge.dashed-edge:hover:not(.selected) { + --edge-stroke: #999; +} diff --git a/apps/angular-demo/src/app/edge-template/dashed-edge/dashed-edge.component.ts b/apps/angular-demo/src/app/edge-template/dashed-edge/dashed-edge.component.ts new file mode 100644 index 000000000..3f8324e60 --- /dev/null +++ b/apps/angular-demo/src/app/edge-template/dashed-edge/dashed-edge.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { Edge, NgDiagramBaseEdgeComponent, NgDiagramEdgeTemplate } from 'ng-diagram'; + +@Component({ + selector: 'app-dashed-edge', + template: ``, + styleUrls: ['./dashed-edge.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgDiagramBaseEdgeComponent], +}) +export class DashedEdgeComponent implements NgDiagramEdgeTemplate { + edge = input.required(); +} diff --git a/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.html b/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.html index 80c20de10..f7bf132b9 100644 --- a/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.html +++ b/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.html @@ -1,4 +1,4 @@ - +
{{ edge().data['labelPosition'] || 'Label' }} diff --git a/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.scss b/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.scss index 62b3a318d..589d157f3 100644 --- a/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.scss +++ b/apps/angular-demo/src/app/edge-template/labelled-edge/labelled-edge.component.scss @@ -1,24 +1,19 @@ -.labelled-edge { - .edge-label { - background-color: white; - border: 1px solid black; - border-radius: 4px; - padding: 4px 8px; - font-size: 12px; - font-weight: 500; - color: var(--text-color); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); - white-space: nowrap; +ng-diagram-base-edge.labelled-edge { + --edge-stroke: black; +} - span { - display: block; - } - } +.edge-label { + background-color: white; + border: 1px solid black; + border-radius: 4px; + padding: 4px 8px; + font-size: 12px; + font-weight: 500; + color: var(--text-color); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + white-space: nowrap; - &.selected { - .edge-label { - border-color: var(--primary-color); - background-color: var(--primary-light); - } + span { + display: block; } } diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.astro b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.astro new file mode 100644 index 000000000..35a7ff754 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.astro @@ -0,0 +1,5 @@ +--- +import { DiagramComponent } from './diagram.component'; +--- + + \ No newline at end of file diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.component.scss b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.component.scss new file mode 100644 index 000000000..b5e6b69a5 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.component.scss @@ -0,0 +1,16 @@ +// Base state +ng-diagram-base-edge { + --edge-stroke: black; + --edge-stroke-width: 2; +} + +// Selected state +ng-diagram-base-edge.selected { + --edge-stroke: blue; + --edge-stroke-width: 3; +} + +// Hover state +ng-diagram-base-edge:hover:not(.selected) { + --edge-stroke: lightblue; +} diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.component.ts b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.component.ts new file mode 100644 index 000000000..3bc8552f0 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/custom-edge.component.ts @@ -0,0 +1,15 @@ +import { Component, input } from '@angular/core'; +import { + NgDiagramBaseEdgeComponent, + type Edge, + type NgDiagramEdgeTemplate, +} from 'ng-diagram'; + +@Component({ + template: ``, + imports: [NgDiagramBaseEdgeComponent], + styleUrls: ['./custom-edge.component.scss'], +}) +export class CustomEdgeComponent implements NgDiagramEdgeTemplate { + edge = input.required(); +} diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom-edge/diagram.component.scss b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/diagram.component.scss new file mode 100644 index 000000000..573e9b889 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/diagram.component.scss @@ -0,0 +1,4 @@ +.diagram { + display: flex; + height: 20rem; +} diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom-edge/diagram.component.ts b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/diagram.component.ts new file mode 100644 index 000000000..aabc3734d --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom-edge/diagram.component.ts @@ -0,0 +1,79 @@ +import '@angular/compiler'; + +import { Component } from '@angular/core'; +import { + initializeModel, + NgDiagramComponent, + NgDiagramEdgeTemplateMap, + provideNgDiagram, + type NgDiagramConfig, +} from 'ng-diagram'; +import { CustomEdgeComponent } from './custom-edge.component'; + +@Component({ + imports: [NgDiagramComponent], + providers: [provideNgDiagram()], + template: ` +
+ +
+ `, + styleUrls: ['./diagram.component.scss'], +}) +export class DiagramComponent { + config = { + zoom: { + max: 3, + }, + } satisfies NgDiagramConfig; + + edgeTemplateMap = new NgDiagramEdgeTemplateMap([ + ['custom', CustomEdgeComponent], + ]); + + model = initializeModel({ + nodes: [ + { + id: '1', + position: { x: 10, y: 100 }, + data: {}, + }, + { + id: '2', + position: { x: 270, y: 100 }, + data: {}, + }, + { + id: '3', + position: { x: 530, y: 100 }, + data: {}, + }, + ], + edges: [ + { + id: '1', + source: '1', + target: '2', + data: {}, + sourcePort: 'port-right', + targetPort: 'port-left', + targetArrowhead: 'ng-diagram-arrow', + type: 'custom', + }, + { + id: '2', + source: '2', + target: '3', + data: {}, + sourcePort: 'port-right', + targetPort: 'port-left', + targetArrowhead: 'ng-diagram-arrow', + }, + ], + metadata: { viewport: { x: 0, y: 0, scale: 1 } }, + }); +} diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom/custom.astro b/apps/docs/src/components/angular/edges/edge-selection/custom/custom.astro new file mode 100644 index 000000000..35a7ff754 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom/custom.astro @@ -0,0 +1,5 @@ +--- +import { DiagramComponent } from './diagram.component'; +--- + + \ No newline at end of file diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom/diagram.component.scss b/apps/docs/src/components/angular/edges/edge-selection/custom/diagram.component.scss new file mode 100644 index 000000000..cc16b0954 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom/diagram.component.scss @@ -0,0 +1,12 @@ +// @section-start +.custom-diagram { + display: flex; + height: 20rem; + + // @mark-start:styles + --ngd-default-edge-stroke: black; + --ngd-default-edge-stroke-hover: darkgoldenrod; + --ngd-default-edge-stroke-selected: gold; + // @mark-end:styles +} +// @section-end diff --git a/apps/docs/src/components/angular/edges/edge-selection/custom/diagram.component.ts b/apps/docs/src/components/angular/edges/edge-selection/custom/diagram.component.ts new file mode 100644 index 000000000..28e408d6e --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/custom/diagram.component.ts @@ -0,0 +1,53 @@ +import '@angular/compiler'; +import { Component } from '@angular/core'; +import { + initializeModel, + NgDiagramComponent, + provideNgDiagram, + type NgDiagramConfig, +} from 'ng-diagram'; + +@Component({ + imports: [NgDiagramComponent], + providers: [provideNgDiagram()], + template: ` +
+ +
+ `, + styleUrls: ['./diagram.component.scss'], +}) +export class DiagramComponent { + config = { + zoom: { + max: 3, + }, + } satisfies NgDiagramConfig; + + model = initializeModel({ + nodes: [ + { + id: '1', + position: { x: 100, y: 100 }, + data: {}, + }, + { + id: '2', + position: { x: 400, y: 100 }, + data: {}, + }, + ], + edges: [ + { + id: '1', + source: '1', + target: '2', + data: {}, + sourcePort: 'port-right', + targetPort: 'port-left', + targetArrowhead: 'ng-diagram-arrow', + }, + ], + metadata: { viewport: { x: 0, y: 0, scale: 1 } }, + }); +} diff --git a/apps/docs/src/components/angular/edges/edge-selection/default/default.astro b/apps/docs/src/components/angular/edges/edge-selection/default/default.astro new file mode 100644 index 000000000..3df7e17e2 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/default/default.astro @@ -0,0 +1,5 @@ +--- +import { DiagramComponent } from './diagram.component'; +--- + + diff --git a/apps/docs/src/components/angular/edges/edge-selection/default/diagram.component.scss b/apps/docs/src/components/angular/edges/edge-selection/default/diagram.component.scss new file mode 100644 index 000000000..573e9b889 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/default/diagram.component.scss @@ -0,0 +1,4 @@ +.diagram { + display: flex; + height: 20rem; +} diff --git a/apps/docs/src/components/angular/edges/edge-selection/default/diagram.component.ts b/apps/docs/src/components/angular/edges/edge-selection/default/diagram.component.ts new file mode 100644 index 000000000..9579fa985 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/default/diagram.component.ts @@ -0,0 +1,53 @@ +import '@angular/compiler'; +import { Component } from '@angular/core'; +import { + initializeModel, + NgDiagramComponent, + provideNgDiagram, + type NgDiagramConfig, +} from 'ng-diagram'; + +@Component({ + imports: [NgDiagramComponent], + providers: [provideNgDiagram()], + template: ` +
+ +
+ `, + styleUrls: ['./diagram.component.scss'], +}) +export class DiagramComponent { + config = { + zoom: { + max: 3, + }, + } satisfies NgDiagramConfig; + + model = initializeModel({ + nodes: [ + { + id: '1', + position: { x: 100, y: 100 }, + data: {}, + }, + { + id: '2', + position: { x: 400, y: 100 }, + data: {}, + }, + ], + edges: [ + { + id: '1', + source: '1', + target: '2', + data: {}, + sourcePort: 'port-right', + targetPort: 'port-left', + targetArrowhead: 'ng-diagram-arrow', + }, + ], + metadata: { viewport: { x: 0, y: 0, scale: 1 } }, + }); +} diff --git a/apps/docs/src/components/angular/edges/edge-selection/dynamic-edge/dynamic-edge.component.ts b/apps/docs/src/components/angular/edges/edge-selection/dynamic-edge/dynamic-edge.component.ts new file mode 100644 index 000000000..dbde018a7 --- /dev/null +++ b/apps/docs/src/components/angular/edges/edge-selection/dynamic-edge/dynamic-edge.component.ts @@ -0,0 +1,34 @@ +import { Component, computed, input } from '@angular/core'; +import { + NgDiagramBaseEdgeComponent, + type Edge, + type NgDiagramEdgeTemplate, +} from 'ng-diagram'; + +// @section-start +@Component({ + // @mark-substring:[stroke]="edgeColor()" + // @mark-substring:[strokeWidth]="edgeWidth()" + // @mark-start + template: ``, + // @mark-end + imports: [NgDiagramBaseEdgeComponent], +}) +export class DynamicEdgeComponent implements NgDiagramEdgeTemplate { + edge = input.required(); + + // @mark-start + edgeColor = computed(() => { + const edge = this.edge(); + if (edge.selected) return 'yellow'; + if (edge.temporary) return 'gray'; + return 'black'; + }); + edgeWidth = computed(() => (this.edge().selected ? 3 : 2)); + // @mark-end +} +// @section-end diff --git a/apps/docs/src/content/docs/api/Components/NgDiagramBaseEdgeComponent.md b/apps/docs/src/content/docs/api/Components/NgDiagramBaseEdgeComponent.md index d27d802c8..748139375 100644 --- a/apps/docs/src/content/docs/api/Components/NgDiagramBaseEdgeComponent.md +++ b/apps/docs/src/content/docs/api/Components/NgDiagramBaseEdgeComponent.md @@ -42,9 +42,17 @@ Stroke color of the edge. Edge model data has precedence over this property. *** +### strokeDasharray + +> **strokeDasharray**: `InputSignal`\<`undefined` \| `string`\> + +Stroke dash array of the edge (e.g., '5 5' for dashed line, '10 5 2 5' for dash-dot pattern). + +*** + ### strokeOpacity -> **strokeOpacity**: `InputSignal`\<`number`\> +> **strokeOpacity**: `InputSignal`\<`undefined` \| `number`\> Stroke opacity of the edge @@ -52,7 +60,7 @@ Stroke opacity of the edge ### strokeWidth -> **strokeWidth**: `InputSignal`\<`number`\> +> **strokeWidth**: `InputSignal`\<`undefined` \| `number`\> Stroke width of the edge diff --git a/apps/docs/src/content/docs/api/Types/PortLocation.md b/apps/docs/src/content/docs/api/Types/PortLocation.md index 0720749f4..9226721cf 100644 --- a/apps/docs/src/content/docs/api/Types/PortLocation.md +++ b/apps/docs/src/content/docs/api/Types/PortLocation.md @@ -9,7 +9,7 @@ title: "PortLocation" Interface representing the location of a port on a node -## Type Declaration +## Type declaration ### side diff --git a/apps/docs/src/content/docs/internals/edges/custom-edges.mdx b/apps/docs/src/content/docs/internals/edges/custom-edges.mdx index 3a67c813c..02eac2f38 100644 --- a/apps/docs/src/content/docs/internals/edges/custom-edges.mdx +++ b/apps/docs/src/content/docs/internals/edges/custom-edges.mdx @@ -20,6 +20,59 @@ Custom edge types need to be registered in the `edgeTemplateMap` and passed to t +## Styling Custom Edges + +Custom edges can be styled using CSS variables that the `NgDiagramBaseEdgeComponent` exposes. This approach provides a clean way to customize edge appearance without breaking encapsulation. + +### Available CSS Variables + +- `--edge-stroke` - Stroke color +- `--edge-stroke-width` - Stroke width +- `--edge-stroke-opacity` - Stroke opacity +- `--edge-stroke-dasharray` - Stroke dash pattern (e.g., `5 5` for dashed line) +- `--edge-stroke-transition` - Transition for stroke changes (e.g., `stroke 0.1s ease-in-out`) + +### Example: Styled Custom Edge + +```scss +ng-diagram-base-edge { + --edge-stroke: #334155; + --edge-stroke-width: 2; + --edge-stroke-dasharray: 5 5; // Dashed line +} + +ng-diagram-base-edge.selected { + --edge-stroke: #3b82f6; + --edge-stroke-width: 3; + --edge-stroke-dasharray: 8 4; // Different dash pattern when selected +} + +ng-diagram-base-edge:hover:not(.selected) { + --edge-stroke: #64748b; +} +``` + +### Alternative: Component Inputs + +You can also pass styling properties directly to the `NgDiagramBaseEdgeComponent`: + +```typescript +@Component({ + selector: 'custom-edge', + template: ``, + imports: [NgDiagramBaseEdgeComponent], +}) +export class CustomEdgeComponent implements NgDiagramEdgeTemplate { + edge = input.required(); +} +``` + ## Custom Routing Custom routing is an extensible mechanism that allows you to create sophisticated edge paths tailored to your specific needs. You can implement custom routing algorithms, create dynamic paths based on node positions, or define manual waypoints for precise control. diff --git a/apps/docs/src/content/docs/internals/edges/edges.mdx b/apps/docs/src/content/docs/internals/edges/edges.mdx index baf55f016..b6cd5082c 100644 --- a/apps/docs/src/content/docs/internals/edges/edges.mdx +++ b/apps/docs/src/content/docs/internals/edges/edges.mdx @@ -63,6 +63,58 @@ import DefaultEdge from '@components/angular/edges/edges/default-edge/default-ed +#### Styling Default Edges with CSS Variables + +Default edges can be styled using CSS variables. You can customize these in your global styles: + +```scss +// Override default edge styling +ng-diagram-base-edge.default-edge { + --edge-stroke: #334155; + --edge-stroke-width: 3; + --edge-stroke-dasharray: 5 5; // Dashed line + --edge-stroke-transition: stroke 0.2s ease; +} + +ng-diagram-base-edge.default-edge:hover:not(.selected) { + --edge-stroke: #64748b; +} + +ng-diagram-base-edge.default-edge.selected { + --edge-stroke: #3b82f6; + --edge-stroke-width: 4; +} +``` + +**Available CSS variables:** + +- `--edge-stroke` - Stroke color +- `--edge-stroke-width` - Stroke width +- `--edge-stroke-opacity` - Stroke opacity +- `--edge-stroke-dasharray` - Stroke dash pattern (e.g., `5 5` for dashed line) +- `--edge-stroke-transition` - Transition for stroke changes + +The default edge also uses these semantic variables that map to the design system: + +- `--ngd-default-edge-stroke` - Base stroke color (default state) +- `--ngd-default-edge-stroke-hover` - Stroke color on hover +- `--ngd-default-edge-stroke-selected` - Stroke color when selected + +### Available Host Classes + +The base edge component exposes these classes for styling: + +- `.selected` - Applied when the edge is selected +- `.temporary` - Applied when the edge is being drawn (preview state) + +See [Edge Selection](/docs/internals/edges/selection) for more details on customizing selection styles. + +### Dynamic Styling with Computed Properties + +For complex or programmatic styling, you can use computed properties with the base edge inputs: + + + Use Custom Edges for even more control over edge look and functionality + +## Customizing Default Selection Styles + +You can customize edge selection styles. + +Here are a CSS variables you can use to style the selection highlight: + +``` +--ngd-default-edge-stroke // Stroke color for default edge +--ngd-default-edge-stroke-hover // Stroke color for default edge on hover +--ngd-default-edge-stroke-selected // Stroke color for selected default edge +``` + +You can override this variable in your styles to customize the appearance of selected nodes. + + + + + +## Custom Edge Selection + +Custom edges can easily apply selection styles using CSS variables. The base edge component exposes selection state through CSS classes, allowing you to style edges without breaking encapsulation. + +### How It Works + +When an edge is selected, the `NgDiagramBaseEdgeComponent` automatically adds the `.selected` class to its host element. You can use this class to change CSS variables that control the edge appearance. + +### Example: Custom Edge with Selection Styling + + + + + + + +More informaction about CSS variables and styling can be found in the [Edges Customization](/docs/internals/edges/edges/#styling-default-edges-with-css-variables) section. + +### Alternative: Dynamic Styling with Computed Properties + + + +## Further Reading + + + + + + diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge-component.spec.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge-component.spec.ts index 716428145..3d8d419eb 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge-component.spec.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge-component.spec.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Edge, Point } from '../../../../core/src'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Edge, Point } from '../../../../core/src'; import { FlowCoreProviderService, RendererService } from '../../../services'; import { InputEventsRouterService } from '../../../services/input-events/input-events-router.service'; import { BaseEdgeLabelComponent } from '../../edge-label/base-edge-label.component'; @@ -246,11 +246,35 @@ describe('NgDiagramBaseEdgeComponent', () => { expect(component.strokeWidth()).toBe(4); }); - it('should use default values for stroke opacity and width', () => { + it('should use default value for stroke opacity', () => { fixture.componentRef.setInput('edge', mockEdge); fixture.detectChanges(); - expect(component.strokeOpacity()).toBe(1); - expect(component.strokeWidth()).toBe(2); + const pathElement = fixture.nativeElement.querySelector('path'); + expect(pathElement.getAttribute('stroke-opacity')).toBe('var(--edge-stroke-opacity, 1)'); + }); + + it('should use default value for stroke width', () => { + fixture.componentRef.setInput('edge', mockEdge); + fixture.detectChanges(); + + const pathElement = fixture.nativeElement.querySelector('path'); + expect(pathElement.getAttribute('stroke-width')).toBe('var(--edge-stroke-width, 2)'); + }); + + it('should use default value for stroke color', () => { + fixture.componentRef.setInput('edge', mockEdge); + fixture.detectChanges(); + + const pathElement = fixture.nativeElement.querySelector('path'); + expect(pathElement.getAttribute('stroke')).toBe('var(--edge-stroke, var(--ngd-default-edge-stroke))'); + }); + + it('should use default value for stroke dasharray', () => { + fixture.componentRef.setInput('edge', mockEdge); + fixture.detectChanges(); + + const pathElement = fixture.nativeElement.querySelector('path'); + expect(pathElement.getAttribute('stroke-dasharray')).toBe('var(--edge-stroke-dasharray, none)'); }); }); diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.html index 665da4d48..8c2a170ca 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.html +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.html @@ -2,12 +2,13 @@ diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.scss index 29c09023d..b69e34e9f 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.scss +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.scss @@ -10,5 +10,6 @@ svg { path { pointer-events: visibleStroke; cursor: pointer; + transition: var(--edge-stroke-transition, none); } } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.ts index 791f7dfe2..a982dd782 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/base-edge/base-edge.component.ts @@ -18,6 +18,10 @@ import { FlowCoreProviderService } from '../../../services'; { directive: ZIndexDirective, inputs: ['data: edge'] }, { directive: EdgeSelectionDirective, inputs: ['targetData: edge'] }, ], + host: { + '[class.selected]': 'selected()', + '[class.temporary]': 'temporary()', + }, }) export class NgDiagramBaseEdgeComponent { private readonly flowCoreProvider = inject(FlowCoreProviderService); @@ -50,16 +54,19 @@ export class NgDiagramBaseEdgeComponent { /** * Stroke opacity of the edge */ - strokeOpacity = input(1); + strokeOpacity = input(); /** * Stroke width of the edge */ - strokeWidth = input(2); + strokeWidth = input(); - readonly points = computed(() => { - return this.edge().points ?? []; - }); + /** + * Stroke dash array of the edge (e.g., '5 5' for dashed line, '10 5 2 5' for dash-dot pattern). + */ + strokeDasharray = input(); + + readonly points = computed(() => this.edge().points ?? []); readonly path = computed(() => { const edge = this.edge(); diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.html index 3f05d3966..1cc863cfb 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.html +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.html @@ -1,5 +1 @@ - + diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.scss index 8738d4827..d910a821d 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.scss +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.scss @@ -1,21 +1,16 @@ -.default-edge { - &:not(:hover) { - .ng-diagram-edge__path { - transition: stroke 0.1s ease-in-out; - } - } +ng-diagram-base-edge.default-edge { + --edge-stroke: var(--ngd-default-edge-stroke); + --edge-stroke-transition: stroke 0.1s ease-in-out; +} - &:hover { - .ng-diagram-edge__path:not(.selected):not(.temporary) { - stroke: var(--ngd-default-edge-stroke-hover); - } - } +ng-diagram-base-edge.default-edge:hover:not(.selected):not(.temporary) { + --edge-stroke: var(--ngd-default-edge-stroke-hover); +} - .ng-diagram-edge__path.selected { - stroke: var(--ngd-default-edge-stroke-selected); - } +ng-diagram-base-edge.default-edge.selected { + --edge-stroke: var(--ngd-default-edge-stroke-selected); +} - .ng-diagram-edge__path.temporary { - stroke-opacity: 0.5; - } +ng-diagram-base-edge.default-edge.temporary { + --edge-stroke-opacity: 0.5; } diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.ts index 95cf0549e..01f3ecd55 100644 --- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.ts +++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/edge/default-edge/default-edge.component.ts @@ -1,16 +1,15 @@ -import { ChangeDetectionStrategy, Component, input, ViewEncapsulation } from '@angular/core'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; import { Edge } from '../../../../core/src'; import { NgDiagramEdgeTemplate } from '../../../types'; +import { BaseEdgeLabelComponent } from '../../edge-label/base-edge-label.component'; import { NgDiagramBaseEdgeComponent } from '../base-edge/base-edge.component'; @Component({ selector: 'ng-diagram-default-edge', - standalone: true, templateUrl: './default-edge.component.html', styleUrls: ['./default-edge.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [NgDiagramBaseEdgeComponent], - encapsulation: ViewEncapsulation.None, + imports: [NgDiagramBaseEdgeComponent, BaseEdgeLabelComponent], }) export class NgDiagramDefaultEdgeComponent implements NgDiagramEdgeTemplate { edge = input.required();