diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/connection-validation/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/connection-validation/index.md index dbec74e..67a89f5 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/connection-validation/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/connection-validation/index.md @@ -1,7 +1,7 @@ # {{ NgDocPage.title }} -`ngx-vflow` supports realtime sync validation of connections. Validation performs on user attempt to create new edge. By default, every connection is valid, but you can provide `ConnectionSettings` with `validatior` callback where you specify validation logic. +`ngx-vflow` supports real-time synchronous validation of connections. Validation occurs when a user attempts to create a new edge. By default, every connection is valid, but you can provide a `ConnectionSettings` with a `validatior` callback where you specify the validation logic. -For example, in this case validation passes only connection from 1 to 2 node. If `validator` returns `false`, `(onConnect)` even won't be called because there is no valid connection. +For example, in this case, validation only passes connections from node 1 to node 2. If the `validator` returns `false`, the `(onConnect)` event won't be triggered because there is no valid connection. {{ NgDocActions.demo("ConnectionValidationDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/curves/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/curves/index.md index 5f17b9e..bfd6b62 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/curves/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/curves/index.md @@ -1,5 +1,5 @@ # {{ NgDocPage.title }} -It's possible to set curve for both edges and connection. +It's possible to set curve for both the edges and connection. {{ NgDocActions.demo("CurvesDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-background/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-background/index.md index 6e01d4b..b85cc72 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-background/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-background/index.md @@ -1,5 +1,5 @@ # {{ NgDocPage.title }} -You're able to select background color for your flow. Now it limits to only color, but later it will be possible to set more complex backgrounds. To select color just pass it in `[background]` input. +You're able to select background color for your flow. Currently, it is limited to selecting a color, but in the future, it will be possible to set more complex backgrounds. To select a color, simply pass it to the `[background]` input. {{ NgDocActions.demo("CustomBackgroundDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-edges/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-edges/index.md index e8aaa02..30c034f 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-edges/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-edges/index.md @@ -1,11 +1,11 @@ # {{ NgDocPage.title }} -You can customize your edges. To achieve this you need to do this: +You can customize your edges. To achieve this, follow these steps: -1. Change edge type to `template` -2. Create `ng-template` with `edge` selector inside `vflow` -3. Create svg path which you will customize -4. In the `ng-template`, the library provides `let-ctx` with some important data for you, such as the `path` signal with the current path. Additionally, `edge` field contains current edge from one the `[edges]`, from which you can also retrieve custom `data`. Furthermore, you can access `markerStart` and `markerEnd` signals with markers for current `edge`. +1. Change the edge type to `template` +2. Create an `ng-template` with the `edge` selector inside `vflow` +3. Create an SVG path which you will customize +4. In the `ng-template`, the library provides `let-ctx` with important data for you, such as the `path` signal with current path. Additionally, the `edge` field contains current edge from one the `[edges]`, from which you can retrieve custom `data`. Furthermore, you can access `markerStart` and `markerEnd` signals with markers for current `edge`. ## Context diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-handles/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-handles/index.md index 707b245..0b360d9 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-handles/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-handles/index.md @@ -5,8 +5,8 @@ You can pass a `[template]` to `HandleComponent` with custom handle. > **Info** > I't important to note that template must be made with SVG. -- Custom handles are not positioned automatically, but the library provides useful template context property to position your handle. -- Custom handles knows if validation of `ConnectionSettings.validator()` failed or succeed, so you can use `state()` signal in `let-ctx` to add some behavior based on validation result. +- Custom handles are not positioned automatically, but the library provides a useful template context property to position your handle. +- Custom handles know if validation of `ConnectionSettings.validator()` has failed or succeeded, so you can use `state()` signal in `let-ctx` to add some behavior based on validation result. Refer to this interface for `let-ctx` when crafting your handle template: diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/demo/custom-component-nodes-demo.component.ts b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/demo/custom-component-nodes-demo.component.ts new file mode 100644 index 0000000..405dc5b --- /dev/null +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/demo/custom-component-nodes-demo.component.ts @@ -0,0 +1,130 @@ +import { NgIf } from '@angular/common'; +import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, inject } from '@angular/core'; +import { HotToastService } from '@ngneat/hot-toast'; +import { ComponentNodeEvent, CustomNodeComponent, Edge, Node, VflowModule } from 'projects/ngx-vflow-lib/src/public-api'; + +@Component({ + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [VflowModule] +}) +export class CustomComponentNodesDemoComponent { + private toast = inject(HotToastService); + + public nodes: Node[] = [ + { + id: '1', + point: { x: 100, y: 100 }, + type: RedSquareNodeComponent, + data: { + redSquareText: 'Red', + } satisfies RedSquareData + }, + { + id: '2', + point: { x: 250, y: 250 }, + type: BlueSquareNodeComponent, + data: { + blueSquareText: 'Blue', + } satisfies BlueSquareData + }, + ] + + public edges: Edge[] = [ + { + id: '1 -> 2', + source: '1', + target: '2' + } + ] + + // Type-safe! + handleComponentEvent(event: ComponentNodeEvent<[RedSquareNodeComponent, BlueSquareNodeComponent]>) { + if (event.eventName === 'redSquareEvent') { + this.toast.info(event.eventPayload) + } + + if (event.eventName === 'blueSquareEvent') { + this.toast.info(`${event.eventPayload.x + event.eventPayload.y}`) + } + } +} + +// --- Description of red square component node + +interface RedSquareData { + redSquareText: string +} + +@Component({ + template: ` +
+ {{ node.data?.redSquareText }} + + +
+ `, + styles: [` + .red-square { + width: 100px; + height: 100px; + background-color: #DE3163; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + padding-left: 5px; + padding-right: 5px; + } + `], + standalone: true, + imports: [VflowModule] +}) +export class RedSquareNodeComponent extends CustomNodeComponent { + @Output() + redSquareEvent = new EventEmitter() + + onClick() { + this.redSquareEvent.emit('Click from red square') + } +} + +// --- Description of blue square component node + +interface BlueSquareData { + blueSquareText: string +} + +@Component({ + template: ` +
+ {{ node.data?.blueSquareText }} + + +
+ `, + styles: [` + .blue-square { + width: 100px; + height: 100px; + background-color: #0096FF; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + padding-left: 5px; + padding-right: 5px; + } + `], + standalone: true, + imports: [VflowModule] +}) +export class BlueSquareNodeComponent extends CustomNodeComponent { + @Output() + blueSquareEvent = new EventEmitter<{ x: number, y: number }>() + + onClick() { + this.blueSquareEvent.emit({ x: 5, y: 5 }) + } +} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/demo/custom-nodes-demo.component.ts b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/demo/custom-nodes-demo.component.ts index a55d392..ae6e996 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/demo/custom-nodes-demo.component.ts +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/demo/custom-nodes-demo.component.ts @@ -1,12 +1,14 @@ import { NgIf } from '@angular/common'; import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { Node, VflowModule } from 'projects/ngx-vflow-lib/src/public-api'; +import { Edge, Node, VflowModule } from 'projects/ngx-vflow-lib/src/public-api'; @Component({ - template: ` + template: `
{{ ctx.node.data.text }} + +
`, @@ -51,4 +53,12 @@ export class CustomNodesDemoComponent { text: 'Default' }, ] + + public edges: Edge[] = [ + { + id: '1 -> 2', + source: '1', + target: '2' + } + ] } diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/index.md index ebd6f8b..2ecc711 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/index.md @@ -1,11 +1,15 @@ # {{ NgDocPage.title }} > **Warning** -> Be careful with CSS rules applied to node content; custom nodes are implemented with SVG's `foreignObject` element, and Safari has issues with some CSS rules inside `foreignObject`. Therefore, please check this browser when applying complex styling. +> Be careful with CSS rules applied to node content. Custom nodes are implemented with SVG's `foreignObject` element, and Safari has issues with some CSS rules inside `foreignObject`. Therefore, please check this browser when applying complex styling. -This is where things became a lot more interesting. You can create custom nodes with HTML and CSS. +This is where things become a lot more interesting. You can create custom nodes with HTML and CSS. -Do the following steps to archieve this: +## Template nodes + +You can create custom nodes with `ng-template` + +Follow these steps to achieve this: 1. Set `type` of node to `html-template` 2. Provide `ng-template` with `nodeHtml` selector inside `vflow` @@ -13,3 +17,63 @@ Do the following steps to archieve this: 4. You can also pass any data with `data` field on node, and then get it inside `ng-template` {{ NgDocActions.demo("CustomNodesDemoComponent", { expanded: true }) }} + +## Component nodes + +Another approach is to render nodes from components. + +Its benefits: + +- type-safe node data access +- good for complex flows with many different node types + +Its limitations + +- it's harder to manage events because such nodes are rendered dynamically + +How to create component node: + +1. Create a regular angular standalone component +2. Extend with `CustomNodeComponent` (please see the reference of this base component to get an idea of what fields you could use in your custom component node), otherwise it won't work! +3. Pass your data interface to generic of `CustomNodeComponent` to use in component. This `data` comes from `Node` definition +4. Use your new component in `type` field of `Node`. Library will render your node for you + +{{ NgDocActions.demo("CustomComponentNodesDemoComponent", { expanded: true }) }} + +### Handling events + +> **Warning** +> This is an experimental API + +There is a `(onComponentNodeEvent)` event on `VflowComponent`. Here is how it works: + +1. It accumulates every `EventEmitter` of every component node of your flow +2. It emits on every emit of those emitters + +The shape of this accumulator-event contains following useful info: + +```ts +export type AnyComponentNodeEvent = { + nodeId: string // Id of node where event occurs + eventName: string + eventPayload: unknown +} +``` + +The Library also includes `ComponentNodeEvent` helper type to get type-safe event, where you just need to pass an array of your custom components in generic, and this type will infer proper types for `eventName` and `eventPayload`: + +```ts + ... + + handleComponentEvent(event: ComponentNodeEvent<[RedSquareNodeComponent, BlueSquareNodeComponent]>) { + if (event.eventName === 'redSquareEvent') { + console.log(event.eventPayload) + } + + if (event.eventName === 'blueSquareEvent') { + console.log(event.eventPayload.x + event.eventPayload.y) + } + } + + .. +``` diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/ng-doc.page.ts b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/ng-doc.page.ts index 57a666e..faa1132 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/ng-doc.page.ts +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/custom-nodes/ng-doc.page.ts @@ -1,12 +1,13 @@ import { NgDocPage } from '@ng-doc/core'; import ExamplesCategory from '../../ng-doc.category' import { CustomNodesDemoComponent } from './demo/custom-nodes-demo.component'; +import { CustomComponentNodesDemoComponent } from './demo/custom-component-nodes-demo.component'; const TestPage: NgDocPage = { title: `Custom nodes`, mdFile: './index.md', category: ExamplesCategory, - demos: { CustomNodesDemoComponent }, + demos: { CustomNodesDemoComponent, CustomComponentNodesDemoComponent }, order: 2 }; diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-connection/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-connection/index.md index 91874e1..c57e6de 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-connection/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-connection/index.md @@ -1,9 +1,9 @@ # {{ NgDocPage.title }} -Edges are not creating automatically. Basically, these steps needs to be performed to create new edge: +Edges are not creating automatically. To create a new edge, follow these steps: -1. Create handler to `(onConnect)` event -2. This handler accepts `Connection` argument. `Connection` is like `Edge`, but it doesn't exists in flow, you need to "convert" it to new `Edge` -3. `Edge[]` list updated with new edge that was created from `Connection` +1. Create handler to the `(onConnect)` event +2. This handler accepts a `Connection` argument. `Connection` is similar to an `Edge`, but it doesn't exists in the flow, you need to "convert" it into a new `Edge` +3. Update the `Edge[]` list with the new edge that was created from the `Connection`. {{ NgDocActions.demo("DefaultConnectionDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-edges/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-edges/index.md index 9f6b50d..46d397a 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-edges/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/default-edges/index.md @@ -1,5 +1,5 @@ # {{ NgDocPage.title }} -You can link nodes by edges. All you need to do is to create another `Edge[]` array and pass to `vflow` component. Each edge contains id of `source` and `target` nodes, also edge must have it's own `id`. +You can link nodes with edges. All you need to do is to create another `Edge[]` array and pass it to the `vflow` component. Each edge contains the id of the `source` and `target` nodes, and each edge must have its own `id`. {{ NgDocActions.demo("DefaultEdgesDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/draggables/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/draggables/index.md index c118423..f52ab25 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/draggables/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/draggables/index.md @@ -1,5 +1,5 @@ # {{ NgDocPage.title }} -On some nodes you can disable `draggable` behavior. +You can disable `draggable` behavior on certain nodes. {{ NgDocActions.demo("DraggablesDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/handling-changes/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/handling-changes/index.md index 6c24c96..dd2f22a 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/handling-changes/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/handling-changes/index.md @@ -1,12 +1,12 @@ # {{ NgDocPage.title }} > **Info** -> You can observe changes in the toasts. It's also better to see this page in desktop +> You can observe changes in the toasts. For a better experience, view this page on desktop -You can observe a lot of changes of nodes and edges. +You can observe various changes in nodes and edges. Types of `NodeChange`s: -- `position` - new node position (after drag and drop) +- `position` - new node position after drag and drop - `add` - when node was created - `remove` - when node was removed - `select` - when node was selected (also triggers for unselected nodes) @@ -15,10 +15,9 @@ Types of `EdgeChange`s: - `add` - when edge was created - `remove` - when edge was removed - `select` - when edge was selected (also triggers for unselected edges) -- `detached` - when edge became invisible due to unexistance of source or target node. It will help you -to delete such edges from edges list +- `detached` - when edge became invisible due to the absence of the source or target node. Use this to delete such edges from the edges list -There are a couple ways to receive these changes: +There are a several ways to receive these changes: ## From (onNodesChange) and (onEdgesChange) outputs @@ -30,12 +29,12 @@ This is a way to get every possible change. Changes came as non empty arrays: ## From filtered outputs -For your convenience there is a filtering scheme for changes based on `(onNodesChange)` and `(onEdgesChange)` events: +For your convenience, here is the filtering scheme for changes based on the `(onNodesChange)` and `(onEdgesChange)` events: -- `(onNodesChange.[NodeChangeType])` - a list of node changes of certain type -- `(onNodesChange.[EdgeChangeType])` - a list of edge changes of certain type -- `(onNodesChange.[NodeChangeType].[Count])` - a list (`many`) of or single (`single`) node change of certain type -- `(onEdgesChange.[EdgeChangeType].[Count])` - a list (`many`) of or single (`single`) edge change of certain type +- `(onNodesChange.[NodeChangeType])` - a list of node changes of a certain type +- `(onNodesChange.[EdgeChangeType])` - a list of edge changes of a certain type +- `(onNodesChange.[NodeChangeType].[Count])` - a list (`many`) or single (`single`) node change of a certain type +- `(onEdgesChange.[EdgeChangeType].[Count])` - a list (`many`) or single (`single`) edge change of a certain type Where: diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/labels/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/labels/index.md index 046cc37..63309fb 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/labels/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/labels/index.md @@ -1,6 +1,6 @@ # {{ NgDocPage.title }} -You can attach labels to edges by providing `edgeLabels` property to needed `Edge`s. There's 3 slots available for lables on edge: `start`, `center`, `end`. Label is only of a `html-template` type, so you have to provide `` inside `vflow`. +You can attach labels to edges by providing the `edgeLabels` property to the needed `Edge`s. There are three slots available for labels on an edge: `start`, `center`, `end`. The label is only of the `html-template` type, so you have to provide `` inside `vflow`. ## Context diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/markers/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/markers/index.md index 324b898..0d10d67 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/markers/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/markers/index.md @@ -1,9 +1,8 @@ # {{ NgDocPage.title }} -You could create markers for both edges and connections. +You can create markers for both edges and connections. -For edges you could spicify `start` and `end` markers for corresponding parts of edge. Marker is now only two `type`'s - `arrow` and `arrow-closed`. - -For connections you couls only specify end marker by `marker` property in `ConnectionSettings`. +- **Edges**: Specify `start` and `end` markers for corresponding parts of the edge. Currently, markers are limited to two `type`s: `arrow` and `arrow-closed`. +- **Connections**: You can specify an end marker using the `marker` property in `ConnectionSettings`. {{ NgDocActions.demo("MarkersDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/multiple-connection-points/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/multiple-connection-points/index.md index 1b9ce0c..9b52a03 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/multiple-connection-points/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/multiple-connection-points/index.md @@ -1,7 +1,7 @@ # {{ NgDocPage.title }} -Custom components opens an ability to control handles and their positions. +Custom components provide the ability to control handles and their positions. -All you need to do is take `HandleComponent` from library and put in some place of your custom node. This component automatically computes its position based on parent element position. +All you need to do is take `HandleComponent` from library and place it somewhere in your custom node. This component automatically computes its position based on parent element's position. {{ NgDocActions.demo("MultipleConnectionPointsDemoComponent", { expanded: true }) }} diff --git a/projects/ngx-vflow-demo/src/app/categories/examples/pages/selecting/index.md b/projects/ngx-vflow-demo/src/app/categories/examples/pages/selecting/index.md index 5be3486..959e886 100644 --- a/projects/ngx-vflow-demo/src/app/categories/examples/pages/selecting/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/examples/pages/selecting/index.md @@ -1,11 +1,11 @@ # {{ NgDocPage.title }} -Nodes and edges could be selected! +Nodes and edges can be selected! -1. Default nodes and edges are selectable by default, just click and see that one is selected. -2. Custom nodes and edges are not selected by default, you need to mark element that triggers selection by `selectable` directive. +1. Default nodes and edges are selectable by default; just click and see that one is selected. +2. Custom nodes and edges are not selectable by default, you need to mark the element that triggers selection with the `selectable` directive. -> Both custom nodes and edges has `selected()` signal in their template context for applying styles based on this state. +> Both custom nodes and edges have the `selected()` signal in their template context for applying styles based on this state. ### Disable selecting diff --git a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/principles/index.md b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/principles/index.md index f5753b3..b19e786 100644 --- a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/principles/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/principles/index.md @@ -2,6 +2,6 @@ This page contains a list of general principles that impact feature implementation. -- No hidden mutations of your data. For example, the library does not modify any of the `Node` or `Edge` objects you pass in as inputs. Instead, it creates internal models around these entities and operates on them. Any changes to the passed entities can be observed with events. -- This principle also implies that you are responsible for managing invalid data. For instance, if you delete a node, edges corresponding to this node will not be deleted automatically. However, the library will notify you about detached edges so that you can easily delete them. +- No hidden mutations of your data. For example, the library does not modify any of the `Node` or `Edge` objects you pass in as inputs. Instead, it creates internal models around these entities and operates on them. Any changes to the passed entities can be observed through events. +- This principle also implies that you are responsible for managing invalid data. For instance, if you delete a node, the edges corresponding to this node will not be deleted automatically. However, the library will notify you about detached edges so that you can easily delete them. diff --git a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/roadmap/index.md b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/roadmap/index.md index 121328e..89e3d5c 100644 --- a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/roadmap/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/roadmap/index.md @@ -2,19 +2,17 @@ This is a roadmap for `ngx-vflow`: -- allow more than 2 handles on node, and further handles API improvements - subflows - more customization to default nodes - improve consistency across browsers (mainly appeals to Safari) - improve documentation - more curves -- selection mechanism, allow to select one or many nodes - plugin system - minimap +- node rotation/resizing - UI controls for flow - support for layout engines (Dagree, etc.) - more complex background (patterns) -- z-index emulation for selected entities, it's a bit hard for SVG-based renderer - more events for different actions - modal system for context menu - HTML-based renderer as alternative to current SVG-based renderer diff --git a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/demo/all-features-demo.component.ts b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/demo/all-features-demo.component.ts new file mode 100644 index 0000000..65fa75c --- /dev/null +++ b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/demo/all-features-demo.component.ts @@ -0,0 +1,296 @@ +import { publishFacade } from "@angular/compiler" +import { ChangeDetectionStrategy, Component, WritableSignal, signal } from "@angular/core" +import { VflowModule, Node, Edge, CustomNodeComponent, Connection, ComponentNode, SharedNode, DefaultNode, ConnectionSettings } from "projects/ngx-vflow-lib/src/public-api" + +@Component({ + template: ` + + + + + +
Delete
+
+
`, + styles: [` + :host { + width: 100%; + height: 100%; + } + + .label { + width: 60px; + height: 25px; + background-color: #122c26; + border-radius: 5px; + text-align: center; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: true, + imports: [VflowModule] +}) +export class AllFeaturesDemoComponent { + public nodes: Node[] = [ + { + id: '1', + point: { x: 10, y: 200 }, + type: 'default', + text: 'Default' + }, + { + id: '2', + point: { x: 200, y: 10 }, + type: 'default', + text: `Resized`, + width: 100, + height: 100 + }, + { + id: '3', + point: { x: 200, y: 270 }, + type: SimpleCustomNodeComponent, + draggable: false + }, + { + id: '4', + point: { x: 600, y: 150 }, + type: ComplexCustomNodeComponent, + data: { + id: { + one: signal(''), + two: signal(''), + three: signal(''), + }, + } + }, + ] + + public edges: Edge[] = [ + { + id: '1 -> 2', + source: '1', + target: '2', + markers: { end: { type: 'arrow-closed' } } + }, + { + id: '1 -> 3', + source: '1', + target: '3', + curve: 'straight', + markers: { end: { type: 'arrow' } } + }, + { + id: '3 -> 4-three', + source: '3', + target: '4', + targetHandle: 'three', + }, + ] + + public connection: ConnectionSettings = { + validator(connection) { + if (connection.source === '3') { + return false + } + + return true + } + } + + handleConnect(connection: Connection) { + if (connection.target === '4') { + const data = this.nodes + .filter(isComponentNode) + .find(n => n.id === '4') + ?.data as ComplexCustomNodeData + + const sourceNode = this.nodes + .filter(isDefaultNode) + .find(n => n.id === connection.source) + + if (sourceNode) { + if (connection.targetHandle === 'one') { + data.id.one.set(sourceNode.text ?? '') + } + if (connection.targetHandle === 'two') { + data.id.two.set(sourceNode.text ?? '') + } + if (connection.targetHandle === 'three') { + data.id.three.set(sourceNode.text ?? '') + } + } + + } + + const color = randomHex() + this.edges = [...this.edges, { + id: crypto.randomUUID(), + type: 'template', + data: { + color + }, + markers: { + end: { + type: 'arrow-closed', + width: 30, + height: 30, + color + } + }, + edgeLabels: { + center: { + type: 'html-template', + data: { color } + } + }, + ...connection + }] + } + + public deleteEdge(edge: Edge) { + this.edges = this.edges.filter(e => e !== edge) + } +} + +function isComponentNode(n: Node): n is SharedNode & ComponentNode { + return CustomNodeComponent.isPrototypeOf(n.type) +} + +function isDefaultNode(n: Node): n is SharedNode & DefaultNode { + return n.type === 'default' +} + +function randomHex() { + const hexValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B', 'C', 'D', 'E', 'F']; + + let hex = '#'; + + for (let i = 0; i < 6; i++) { + const index = Math.floor(Math.random() * hexValues.length) + hex += hexValues[index]; + } + + return hex +} + +@Component({ + template: `
+ Custom node! + + + +
`, + styles: [` + .node { + width: 150px; + height: 100px; + background: #bbe1fa; + border: 1px solid gray; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + color: black; + } + `], + standalone: true, + imports: [VflowModule] +}) +export class SimpleCustomNodeComponent extends CustomNodeComponent { } + +interface ComplexCustomNodeData { + id: { + one: WritableSignal, + two: WritableSignal, + three: WritableSignal, + } +} + +@Component({ + template: `
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+ + + + + + `, + styles: [` + .node { + width: 150px; + background: #bbe1fa; + border: 1px solid gray; + border-radius: 5px; + color: black; + padding: 10px; + } + + .input { + width: 130px; + } + + .control-wrapper { + margin-bottom: 20px; + } + + .control-wrapper_last { + margin-bottom: 0px; + } + + .handle { + &_idle { + fill: #fff; + } + + &_valid { + fill: green; + } + + &_invalid { + fill: red; + } + } + `], + standalone: true, + imports: [VflowModule] +}) +export class ComplexCustomNodeComponent extends CustomNodeComponent { } diff --git a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/index.md b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/index.md index 511a4cf..4ee9b39 100644 --- a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/index.md +++ b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/index.md @@ -1,7 +1,9 @@ # {{ NgDocPage.title }}
- + +{{ NgDocActions.demoPane("AllFeaturesDemoComponent") }} + `ngx-vflow` is an Angular library for creating node-based applications. It aims to assist you in building anything from a static diagram to a visual editor. You can utilize the default design or apply your own by customizing everything using familiar technologies. diff --git a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/ng-doc.page.ts b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/ng-doc.page.ts index 700de37..19fb810 100644 --- a/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/ng-doc.page.ts +++ b/projects/ngx-vflow-demo/src/app/categories/getting-started/pages/what-is-ngx-vflow/ng-doc.page.ts @@ -1,11 +1,13 @@ import { NgDocPage } from '@ng-doc/core'; import GettingStartedCategory from '../../ng-doc.category' +import { AllFeaturesDemoComponent } from './demo/all-features-demo.component'; const TestPage: NgDocPage = { title: `What is ngx-vflow`, mdFile: './index.md', category: GettingStartedCategory, - order: 1 + order: 1, + demos: { AllFeaturesDemoComponent } }; export default TestPage; diff --git a/projects/ngx-vflow-lib/package.json b/projects/ngx-vflow-lib/package.json index 9ac7424..7001402 100644 --- a/projects/ngx-vflow-lib/package.json +++ b/projects/ngx-vflow-lib/package.json @@ -1,6 +1,6 @@ { "name": "ngx-vflow", - "version": "0.5.0", + "version": "0.6.0", "license": "MIT", "homepage": "https://www.ngx-vflow.org/", "author": "Artem Mangilev", diff --git a/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.html b/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.html index b77e9c1..739cf6e 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.html +++ b/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.html @@ -2,14 +2,18 @@ *ngIf="nodeModel.node.type === 'default'" class="selectable" #nodeContent - width="100" - height="50" + [attr.width]="nodeModel.size().width" + [attr.height]="nodeModel.size().height" (mousedown)="pullNode(); selectNode()" >
@@ -30,6 +34,22 @@ + +
+ +
+
+ - diff --git a/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.scss b/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.scss index e3d029b..462af51 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.scss +++ b/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.scss @@ -7,10 +7,6 @@ } .default-node { - max-width: 100px; - max-height: 100px; - width: 100px; - height: 50px; border: 1.5px solid #1b262c; border-radius: 5px; display: flex; diff --git a/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.ts b/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.ts index 318d5a8..52ea2e1 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.ts +++ b/projects/ngx-vflow-lib/src/lib/vflow/components/node/node.component.ts @@ -24,8 +24,15 @@ export type HandleState = 'valid' | 'invalid' | 'idle' }) export class NodeComponent implements OnInit, AfterViewInit, OnDestroy, WithInjector { public injector = inject(Injector) - protected handleService = inject(HandleService) - protected zone = inject(NgZone) + private handleService = inject(HandleService) + private zone = inject(NgZone) + private draggableService = inject(DraggableService) + private flowStatusService = inject(FlowStatusService) + private flowEntitiesService = inject(FlowEntitiesService) + private nodeRenderingService = inject(NodeRenderingService) + private flowSettingsService = inject(FlowSettingsService) + private selectionService = inject(SelectionService) + private hostRef = inject>(ElementRef) @Input() public nodeModel!: NodeModel @@ -39,19 +46,15 @@ export class NodeComponent implements OnInit, AfterViewInit, OnDestroy, WithInje @ViewChild('htmlWrapper') public htmlWrapperRef!: ElementRef - private draggableService = inject(DraggableService) - private flowStatusService = inject(FlowStatusService) - private flowEntitiesService = inject(FlowEntitiesService) - private nodeRenderingService = inject(NodeRenderingService) - private flowSettingsService = inject(FlowSettingsService) - private selectionService = inject(SelectionService) - private hostRef = inject>(ElementRef) - protected showMagnet = computed(() => this.flowStatusService.status().state === 'connection-start' || this.flowStatusService.status().state === 'connection-validation' ) + protected styleWidth = computed(() => `${this.nodeModel.size().width}px`) + + protected styleHeight = computed(() => `${this.nodeModel.size().height}px`) + private subscription = new Subscription() public ngOnInit() { @@ -77,11 +80,13 @@ export class NodeComponent implements OnInit, AfterViewInit, OnDestroy, WithInje this.setInitialHandles() if (this.nodeModel.node.type === 'default') { - const { width, height } = this.nodeContentRef.nativeElement.getBBox() - this.nodeModel.size.set({ width, height }) + this.nodeModel.size.set({ + width: this.nodeModel.node.width ?? NodeModel.defaultTypeSize.width, + height: this.nodeModel.node.height ?? NodeModel.defaultTypeSize.height + }) } - if (this.nodeModel.node.type === 'html-template') { + if (this.nodeModel.node.type === 'html-template' || this.nodeModel.isComponentType) { const sub = resizable([this.htmlWrapperRef.nativeElement], this.zone) .pipe( startWith(null), diff --git a/projects/ngx-vflow-lib/src/lib/vflow/components/vflow/vflow.component.ts b/projects/ngx-vflow-lib/src/lib/vflow/components/vflow/vflow.component.ts index 2ee4870..c8bd700 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/components/vflow/vflow.component.ts +++ b/projects/ngx-vflow-lib/src/lib/vflow/components/vflow/vflow.component.ts @@ -27,6 +27,7 @@ import { EdgeChange } from '../../types/edge-change.type'; import { NodeRenderingService } from '../../services/node-rendering.service'; import { SelectionService } from '../../services/selection.service'; import { FlowSettingsService } from '../../services/flow-settings.service'; +import { ComponentEventBusService } from '../../services/component-event-bus.service'; const connectionControllerHostDirective = { directive: ConnectionControllerDirective, @@ -79,7 +80,8 @@ const changesControllerHostDirective = { EdgeChangesService, NodeRenderingService, SelectionService, - FlowSettingsService + FlowSettingsService, + ComponentEventBusService ], hostDirectives: [ connectionControllerHostDirective, @@ -94,6 +96,7 @@ export class VflowComponent { private edgesChangeService = inject(EdgeChangesService) private nodeRenderingService = inject(NodeRenderingService) private flowSettingsService = inject(FlowSettingsService) + private componentEventBusService = inject(ComponentEventBusService) private injector = inject(Injector) // #endregion @@ -197,6 +200,16 @@ export class VflowComponent { protected edgeModels = computed(() => this.flowEntitiesService.validEdges()) // #endregion + // #region OUTPUTS + /** + * Event that accumulates all custom node events + * + * @experimental + */ + @Output() + public onComponentNodeEvent = this.componentEventBusService.event$ as any // TODO: research how to remove as any + // #endregion + // #region TEMPLATES @ContentChild(NodeHtmlTemplateDirective) protected nodeHtmlDirective?: NodeHtmlTemplateDirective diff --git a/projects/ngx-vflow-lib/src/lib/vflow/directives/map-context.directive.ts b/projects/ngx-vflow-lib/src/lib/vflow/directives/map-context.directive.ts index 6c39550..ff8064a 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/directives/map-context.directive.ts +++ b/projects/ngx-vflow-lib/src/lib/vflow/directives/map-context.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, Input, OnInit, effect, inject } from '@angular/core'; +import { Directive, ElementRef, Input, OnInit, effect, inject, untracked } from '@angular/core'; import { select } from 'd3-selection'; import { D3ZoomEvent, ZoomBehavior, ZoomTransform, zoom, zoomIdentity } from 'd3-zoom'; import { ViewportService } from '../services/viewport.service'; @@ -28,8 +28,7 @@ export class MapContextDirective implements OnInit { protected viewportForSelection: Partial = {} // under the hood this effect triggers handleZoom, so error throws without this flag - // TODO: hack with timer fixes wrong node scaling (handle positions not matched with content size) - protected manualViewportChangeEffect = effect(() => setTimeout(() => { + protected manualViewportChangeEffect = effect(() => { const viewport = this.viewportService.writableViewport() const state = viewport.state @@ -46,7 +45,12 @@ export class MapContextDirective implements OnInit { // If only pan provided if ((isDefined(state.x) && isDefined(state.y)) && !isDefined(state.zoom)) { - this.rootSvgSelection.call(this.zoomBehavior.translateTo, state.x, state.y) + // remain same zoom value + const zoom = untracked(this.viewportService.readableViewport).zoom + + this.rootSvgSelection.call(this.zoomBehavior.transform, + zoomIdentity.translate(state.x, state.y).scale(zoom) + ) return } @@ -59,7 +63,7 @@ export class MapContextDirective implements OnInit { return } - }), { allowSignalWrites: true }) + }, { allowSignalWrites: true }) protected zoomBehavior!: ZoomBehavior; diff --git a/projects/ngx-vflow-lib/src/lib/vflow/interfaces/component-node-event.interface.ts b/projects/ngx-vflow-lib/src/lib/vflow/interfaces/component-node-event.interface.ts new file mode 100644 index 0000000..1db596f --- /dev/null +++ b/projects/ngx-vflow-lib/src/lib/vflow/interfaces/component-node-event.interface.ts @@ -0,0 +1,32 @@ +import { EventEmitter } from "@angular/core"; +import { CustomNodeComponent } from "../public-components/custom-node.component"; + +type EventInfo = T extends EventEmitter ? U : never; + +type EventKeys = { + [K in keyof T]: T[K] extends EventEmitter ? K : never +}[keyof T]; + +type EventShape = { + [P in K]: { eventName: P, eventPayload: EventInfo } +}[K]; + +type EventsFromComponent = EventShape>; + +/** + * Event of custom component node + * + * Generic accepts array of custom components and merge their event emitters for type-safe + * event handling + * + * @experimental + */ +export type ComponentNodeEvent = { nodeId: string } & { + [I in keyof T]: EventsFromComponent +}[number]; + +export type AnyComponentNodeEvent = { + nodeId: string + eventName: string + eventPayload: unknown +} diff --git a/projects/ngx-vflow-lib/src/lib/vflow/interfaces/node.interface.ts b/projects/ngx-vflow-lib/src/lib/vflow/interfaces/node.interface.ts index ebff413..d1fdc39 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/interfaces/node.interface.ts +++ b/projects/ngx-vflow-lib/src/lib/vflow/interfaces/node.interface.ts @@ -1,8 +1,11 @@ +import { Directive, Input, Type, signal } from "@angular/core" import { Point } from "./point.interface" +import { CustomNodeComponent } from "../public-components/custom-node.component" export type Node = SharedNode & ( DefaultNode | - HtmlTemplateNode + HtmlTemplateNode | + ComponentNode ) export interface SharedNode { id: string @@ -13,9 +16,16 @@ export interface SharedNode { export interface DefaultNode { type: 'default' text?: string + width?: number + height?: number } export interface HtmlTemplateNode { type: 'html-template' data?: T } + +export interface ComponentNode { + type: Type> + data?: T +} diff --git a/projects/ngx-vflow-lib/src/lib/vflow/models/node.model.ts b/projects/ngx-vflow-lib/src/lib/vflow/models/node.model.ts index 5d87640..311c5fc 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/models/node.model.ts +++ b/projects/ngx-vflow-lib/src/lib/vflow/models/node.model.ts @@ -1,17 +1,33 @@ -import { computed, inject, signal } from '@angular/core' +import { Type, computed, inject, signal } from '@angular/core' import { Node } from '../interfaces/node.interface' import { isDefined } from '../utils/is-defined' -import { toObservable } from '@angular/core/rxjs-interop' +import { toObservable, toSignal } from '@angular/core/rxjs-interop' import { HandleModel } from './handle.model' import { FlowEntity } from '../interfaces/flow-entity.interface' import { FlowSettingsService } from '../services/flow-settings.service' +import { BehaviorSubject, animationFrameScheduler, observeOn } from 'rxjs' +import { Point } from '../interfaces/point.interface' +import { CustomNodeComponent } from '../public-components/custom-node.component' export class NodeModel implements FlowEntity { + public static defaultTypeSize = { + width: 100, + height: 50 + } + private flowSettingsService = inject(FlowSettingsService) - public point = signal({ x: 0, y: 0 }) + private internalPoint$ = new BehaviorSubject({ x: 0, y: 0 }) + + private throttledPoint$ = this.internalPoint$.pipe( + observeOn(animationFrameScheduler) + ) + + public point = toSignal(this.throttledPoint$, { + initialValue: this.internalPoint$.getValue() + }) - public point$ = toObservable(this.point) + public point$ = this.throttledPoint$; public size = signal({ width: 0, height: 0 }) @@ -35,11 +51,24 @@ export class NodeModel implements FlowEntity { // disabled for configuration for now public readonly magnetRadius = 20 + public isComponentType = CustomNodeComponent.isPrototypeOf(this.node.type) + + public componentTypeInputs = computed(() => { + return { + node: this.node, + _selected: this.selected() + } + }) + constructor( public node: Node ) { - this.point.set(node.point) + this.setPoint(node.point) if (isDefined(node.draggable)) this.draggable = node.draggable } + + public setPoint(point: Point) { + this.internalPoint$.next(point); + } } diff --git a/projects/ngx-vflow-lib/src/lib/vflow/public-components/custom-node.component.ts b/projects/ngx-vflow-lib/src/lib/vflow/public-components/custom-node.component.ts new file mode 100644 index 0000000..7f84db5 --- /dev/null +++ b/projects/ngx-vflow-lib/src/lib/vflow/public-components/custom-node.component.ts @@ -0,0 +1,63 @@ +import { DestroyRef, Directive, EventEmitter, Input, OnInit, inject, signal } from "@angular/core" +import { ComponentNode, SharedNode } from '../interfaces/node.interface'; +import { ComponentEventBusService } from "../services/component-event-bus.service"; +import { merge, tap } from "rxjs"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; + +@Directive() +export abstract class CustomNodeComponent implements OnInit { + private eventBus = inject(ComponentEventBusService) + + protected destroyRef = inject(DestroyRef) + + /** + * Reference to node bound to this component + */ + @Input() + public node!: SharedNode & ComponentNode + + @Input() + public set _selected(value: boolean) { + this.selected.set(value) + } + + /** + * Signal with selected state of node + */ + public selected = signal(false) + + public ngOnInit(): void { + this.trackEvents() + } + + private trackEvents() { + const props = Object.getOwnPropertyNames(this) + + const emitters = new Map, string>() + for (const prop of props) { + const field = (this as Record)[prop] + + if (field instanceof EventEmitter) { + emitters.set(field, prop) + } + } + + merge( + ...Array.from(emitters.keys()).map(emitter => + emitter.pipe( + tap((event) => { + this.eventBus.pushEvent({ + nodeId: this.node.id, + eventName: emitters.get(emitter)!, + eventPayload: event + }) + })) + ) + ) + .pipe( + takeUntilDestroyed(this.destroyRef) + ) + .subscribe() + }; +} + diff --git a/projects/ngx-vflow-lib/src/lib/vflow/services/component-event-bus.service.ts b/projects/ngx-vflow-lib/src/lib/vflow/services/component-event-bus.service.ts new file mode 100644 index 0000000..e4d7377 --- /dev/null +++ b/projects/ngx-vflow-lib/src/lib/vflow/services/component-event-bus.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs'; +import { AnyComponentNodeEvent, ComponentNodeEvent } from '../interfaces/component-node-event.interface'; +import { CustomNodeComponent } from '../public-components/custom-node.component'; + +@Injectable() +export class ComponentEventBusService { + private _event$ = new Subject() + + public event$ = this._event$.asObservable() + + public pushEvent(event: AnyComponentNodeEvent) { + this._event$.next(event) + } +} diff --git a/projects/ngx-vflow-lib/src/lib/vflow/services/draggable.service.ts b/projects/ngx-vflow-lib/src/lib/vflow/services/draggable.service.ts index 9dead81..5a0f12e 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/services/draggable.service.ts +++ b/projects/ngx-vflow-lib/src/lib/vflow/services/draggable.service.ts @@ -51,7 +51,7 @@ export class DraggableService { }) .on('drag', (event: DragEvent) => { - model.point.set( + model.setPoint( { x: round(event.x + deltaX), y: round(event.y + deltaY) diff --git a/projects/ngx-vflow-lib/src/lib/vflow/services/selection.service.ts b/projects/ngx-vflow-lib/src/lib/vflow/services/selection.service.ts index bae09b0..90f2803 100644 --- a/projects/ngx-vflow-lib/src/lib/vflow/services/selection.service.ts +++ b/projects/ngx-vflow-lib/src/lib/vflow/services/selection.service.ts @@ -1,6 +1,5 @@ import { Injectable, effect, inject, signal, untracked } from '@angular/core'; import { ViewportState } from '../interfaces/viewport.interface'; -import { ViewportService } from './viewport.service'; import { FlowEntitiesService } from './flow-entities.service'; import { FlowEntity } from '../interfaces/flow-entity.interface'; import { Subject, tap } from 'rxjs'; @@ -9,7 +8,10 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export interface ViewportForSelection { start: ViewportState end: ViewportState - target: Element + /** + * Target may not exist if viewport change made programmatically + */ + target?: Element } @Injectable() @@ -22,7 +24,7 @@ export class SelectionService { protected resetSelection = this.viewport$.pipe( tap(({ start, end, target }) => { - if (start && end) { + if (start && end && target) { const delta = SelectionService.delta const diffX = Math.abs(end.x - start.x) diff --git a/projects/ngx-vflow-lib/src/public-api.ts b/projects/ngx-vflow-lib/src/public-api.ts index f0d58a6..48d8e2e 100644 --- a/projects/ngx-vflow-lib/src/public-api.ts +++ b/projects/ngx-vflow-lib/src/public-api.ts @@ -12,6 +12,7 @@ export * from './lib/vflow/interfaces/connection-settings.interface'; export * from './lib/vflow/interfaces/handle-positions.interface'; export * from './lib/vflow/interfaces/marker.interface'; export { ViewportState } from './lib/vflow/interfaces/viewport.interface'; +export * from './lib/vflow/interfaces/component-node-event.interface'; // Types export * from './lib/vflow/types/node-change.type'; @@ -21,6 +22,7 @@ export * from './lib/vflow/types/position.type'; // Components export * from './lib/vflow/components/vflow/vflow.component'; export * from './lib/vflow/components/handle/handle.component'; +export * from './lib/vflow/public-components/custom-node.component'; // Directives export * from './lib/vflow/directives/template.directive';