Skip to content

Commit

Permalink
0.6.0 (#64)
Browse files Browse the repository at this point in the history
* refactor: throttle node drag with rAF

* feat: add width and height props for default node type

* feat: add component node type

* feat: add onComponentNodeEvent api

* feat: add helper type for merging events

* docs: extend doc with component nodes description

* docs: some minor fixes

* feat: add demo page with all features

* docs: improve English

* fix: fix manual zoom

* fix: fix pan

* fix: fix pan

* up version
  • Loading branch information
artem-mangilev authored Jun 26, 2024
1 parent 78c94e6 commit 9b829d1
Show file tree
Hide file tree
Showing 36 changed files with 780 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -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 }) }}
Original file line number Diff line number Diff line change
@@ -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 }) }}
Original file line number Diff line number Diff line change
@@ -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 }) }}
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Original file line number Diff line number Diff line change
@@ -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: `<vflow [nodes]="nodes" [edges]="edges" (onComponentNodeEvent)="handleComponentEvent($event)" />`,
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: `
<div class="red-square" (click)="onClick()">
{{ node.data?.redSquareText }}
<handle type="source" position="right"/>
</div>
`,
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<RedSquareData> {
@Output()
redSquareEvent = new EventEmitter<string>()

onClick() {
this.redSquareEvent.emit('Click from red square')
}
}

// --- Description of blue square component node

interface BlueSquareData {
blueSquareText: string
}

@Component({
template: `
<div class="blue-square" (click)="onClick()">
{{ node.data?.blueSquareText }}
<handle type="target" position="left"/>
</div>
`,
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<BlueSquareData> {
@Output()
blueSquareEvent = new EventEmitter<{ x: number, y: number }>()

onClick() {
this.blueSquareEvent.emit({ x: 5, y: 5 })
}
}
Original file line number Diff line number Diff line change
@@ -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: `<vflow [nodes]="nodes">
template: `<vflow [nodes]="nodes" [edges]="edges">
<ng-template nodeHtml let-ctx>
<div class="custom-node" [class.custom-node_selected]="ctx.selected()">
{{ ctx.node.data.text }}
<handle type="source" position="right"/>
</div>
</ng-template>
</vflow>`,
Expand Down Expand Up @@ -51,4 +53,12 @@ export class CustomNodesDemoComponent {
text: 'Default'
},
]

public edges: Edge[] = [
{
id: '1 -> 2',
source: '1',
target: '2'
}
]
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,79 @@
# {{ 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`
3. Write your HTML inside this template
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)
}
}

..
```
Original file line number Diff line number Diff line change
@@ -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
};

Expand Down
Original file line number Diff line number Diff line change
@@ -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 }) }}
Original file line number Diff line number Diff line change
@@ -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 }) }}
Original file line number Diff line number Diff line change
@@ -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 }) }}
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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

Expand All @@ -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:

Expand Down
Original file line number Diff line number Diff line change
@@ -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 `<ng-template edgeLabelHtml>` 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 `<ng-template edgeLabelHtml>` inside `vflow`.

## Context

Expand Down
Loading

0 comments on commit 9b829d1

Please sign in to comment.