Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
<f-flow fDraggable (fLoaded)="onLoaded()">
@if (background === 'circle') {
<f-background>
<f-circle-pattern></f-circle-pattern>
</f-background>
} @else if (background === 'rect') {
<f-background>
<f-rect-pattern></f-rect-pattern>
</f-background>
<f-flow fDraggable (fLoaded)="loaded()">
@switch (background()) {
@case ('circle') {
<f-background>
<f-circle-pattern />
</f-background>
}
@case ('rect') {
<f-background>
<f-rect-pattern />
</f-background>
}
@case ('custom') {
<f-background>
<custom-background-example />
</f-background>
}
}
<f-canvas>
<f-connection fOutputId="output1" fInputId="input1" fBehavior="floating"></f-connection>
<f-canvas fZoom>
<f-connection fOutputId="output1" fInputId="input1" fBehavior="floating" />
<div fNode [fNodePosition]="{ x: 0, y: 0 }" fDragHandle>
<div fNodeOutput fOutputId="output1" class="right"></div>
I'm a node
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, signal, viewChild } from '@angular/core';
import { FCanvasComponent, FFlowModule } from '@foblex/flow';
import { MatFormField, MatLabel } from '@angular/material/form-field';
import { MatOption } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { FlowBackground } from './custom-background-example/custom-background-example';

@Component({
selector: 'background-example',
styleUrls: ['./background-example.component.scss'],
templateUrl: './background-example.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelectModule],
imports: [FFlowModule, MatFormField, MatLabel, MatOption, MatSelectModule, FlowBackground],
})
export class BackgroundExampleComponent {
@ViewChild(FCanvasComponent, { static: true })
public fCanvas!: FCanvasComponent;
private readonly _canvas = viewChild.required(FCanvasComponent);

public background = 'circle';
protected readonly background = signal('custom');

public backgroundOptions = ['circle', 'rect', 'none'];
protected readonly backgroundOptions = ['circle', 'rect', 'custom', 'none'];

public onLoaded(): void {
this.fCanvas.resetScaleAndCenter(false);
protected loaded(): void {
this._canvas()?.resetScaleAndCenter(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<svg:pattern #circlePattern id="circleGrid" patternUnits="userSpaceOnUse">
<svg:circle #circle></svg:circle>
</svg:pattern>
<svg:pattern #rectPattern patternUnits="userSpaceOnUse">
<svg:rect width="100%" height="100%" fill="url(#circleGrid)"></svg:rect>
<svg:line x1="0" y1="0" x2="100%" y2="0" stroke-width="2"></svg:line>
<svg:line x1="0" y1="0" x2="0" y2="100%" stroke-width="2"></svg:line>
</svg:pattern>
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
ChangeDetectionStrategy,
Component,
DestroyRef,
ElementRef,
inject,
input,
numberAttribute,
OnInit,
viewChild,
} from '@angular/core';
import {
F_BACKGROUND_PATTERN,
FChannel,
FChannelHub,
FFlowModule,
IFBackgroundPattern,
notifyOnStart,
} from '@foblex/flow';
import {
ITransformModel,
PointExtensions,
SizeExtensions,
TransformModelExtensions,
} from '@foblex/2d';

@Component({
selector: 'custom-background-example',
templateUrl: './custom-background-example.html',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [FFlowModule],
providers: [{ provide: F_BACKGROUND_PATTERN, useExisting: FlowBackground }],
})
export class FlowBackground implements OnInit, IFBackgroundPattern {
private readonly _destroyRef = inject(DestroyRef);
private readonly _elementReference = inject(ElementRef);

private readonly _stateChanges = new FChannel();

public get hostElement(): HTMLElement {
return this._elementReference.nativeElement;
}

public readonly size = input(80, { transform: numberAttribute });
public readonly circleSize = input(16, { transform: numberAttribute });

private _transform = TransformModelExtensions.default();

private _position = PointExtensions.initialize();

private _size = SizeExtensions.initialize(this.size(), this.size());
private _radius = this.circleSize();

private readonly _circlePattern = viewChild<ElementRef<SVGPatternElement>>('circlePattern');
private readonly _circlePatternCircle = viewChild<ElementRef<SVGCircleElement>>('circle');

private readonly _rectPattern = viewChild<ElementRef<SVGPatternElement>>('rectPattern');

public ngOnInit(): void {
this._listenStateChanges();
}

private _listenStateChanges(): void {
new FChannelHub(this._stateChanges)
.pipe(notifyOnStart())
.listen(this._destroyRef, () => this._redraw());
}

private _redraw(): void {
this._calculatePattern();
this._redrawPattern();
}

private _calculatePattern(): void {
this._position.x = this._transform.position.x + this._transform.scaledPosition.x;
this._position.y = this._transform.position.y + this._transform.scaledPosition.y;
this._size = SizeExtensions.initialize(
this.size() * this._transform.scale,
this.size() * this._transform.scale,
);
this._radius = this.circleSize() * this._transform.scale;
}

private _redrawPattern(): void {
this._redrawRectPattern();
this._redrawCirclePattern();
}

private _redrawRectPattern(): void {
this._rectPattern()?.nativeElement.setAttribute('x', `${this._position.x}`);
this._rectPattern()?.nativeElement.setAttribute('y', `${this._position.y}`);
this._rectPattern()?.nativeElement.setAttribute('width', `${this._size.width}`);
this._rectPattern()?.nativeElement.setAttribute('height', `${this._size.height}`);
}

private _redrawCirclePattern(): void {
this._circlePattern()?.nativeElement.setAttribute('width', `${this._radius}`);
this._circlePattern()?.nativeElement.setAttribute('height', `${this._radius}`);
this._redrawCircleElement();
}

private _redrawCircleElement(): void {
this._circlePatternCircle()?.nativeElement.setAttribute('r', `${this._transform.scale}`);
this._circlePatternCircle()?.nativeElement.setAttribute('cx', `${this._transform.scale / 2}`);
this._circlePatternCircle()?.nativeElement.setAttribute('cy', `${this._transform.scale / 2}`);
}

public setTransform(transform: ITransformModel): void {
this._transform = transform;
this._refresh();
}

private _refresh(): void {
this._stateChanges.notify();
}
}
2 changes: 1 addition & 1 deletion projects/f-flow/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@foblex/flow",
"version": "17.9.7",
"version": "17.9.8",
"description": "An Angular library designed to simplify the creation and manipulation of dynamic flow. Provides components for flows, nodes, and connections, automating node manipulation and inter-node connections.",
"main": "index.js",
"types": "index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { IFBackgroundPattern } from '../../../f-backgroud';

export class AddPatternToBackgroundRequest {
static readonly fToken = Symbol('AddPatternToBackgroundRequest');
constructor(
public fPattern: IFBackgroundPattern | undefined,
) {
}

constructor(public fPattern: IFBackgroundPattern | undefined) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ let uniqueId = 0;
*/
@Injectable()
@FExecutionRegister(AddPatternToBackgroundRequest)
export class AddPatternToBackgroundExecution
implements IExecution<AddPatternToBackgroundRequest, void>
{
export class AddPatternToBackground implements IExecution<AddPatternToBackgroundRequest, void> {
private readonly _store = inject(FComponentsStore);
private readonly _browser = inject(BrowserService);

Expand All @@ -24,21 +22,32 @@ export class AddPatternToBackgroundExecution
}

public handle(request: AddPatternToBackgroundRequest): void {
const children = request.fPattern?.hostElement.getElementsByTagName('pattern') || [];
const pattern = children.length ? children[0] : undefined;
if (pattern) {
const defs = createSVGElement('defs', this._browser);
pattern.id = 'f-background-pattern-' + uniqueId++;
request.fPattern?.hostElement.remove();
defs.appendChild(pattern);
const patterns = this._getPatterns(request.fPattern?.hostElement);
if (!patterns?.length) {
return;
}
const defs = createSVGElement('defs', this._browser);
request.fPattern?.hostElement.remove();

patterns.forEach((pattern) => {
defs.append(pattern);
});

if (patterns.length) {
this._backgroundElement?.firstChild?.appendChild(defs);
patterns[patterns.length - 1].id = 'f-background-pattern-' + uniqueId++;
const lastPatternId = patterns[patterns.length - 1].id;
const rect = createSVGElement('rect', this._browser);
rect.setAttribute('fill', 'url(#' + pattern.id + ')');
rect.setAttribute('fill', 'url(#' + lastPatternId + ')');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
this._backgroundElement.firstChild?.appendChild(rect);
const transform = this._store.fCanvas?.transform || TransformModelExtensions.default();
request.fPattern?.setTransform(transform);
}
}

private _getPatterns(element?: HTMLElement | SVGElement | undefined): SVGPatternElement[] {
return Array.from(element?.getElementsByTagName('pattern') || []);
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './add-pattern-to-background.execution';
export * from './add-pattern-to-background';

export * from './add-pattern-to-background.request';
5 changes: 2 additions & 3 deletions projects/f-flow/src/domain/f-background/providers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AddPatternToBackgroundExecution } from './add-pattern-to-background';
import { AddPatternToBackground } from './add-pattern-to-background';
import { AddBackgroundToStoreExecution } from './add-background-to-store';
import { RemoveBackgroundFromStoreExecution } from './remove-background-from-store';
import { SetBackgroundTransformExecution } from './set-background-transform';
Expand All @@ -8,10 +8,9 @@ import { SetBackgroundTransformExecution } from './set-background-transform';
* It includes executions for adding, removing, and setting the background in the FComponentsStore.
*/
export const F_BACKGROUND_FEATURES = [

AddBackgroundToStoreExecution,

AddPatternToBackgroundExecution,
AddPatternToBackground,

RemoveBackgroundFromStoreExecution,

Expand Down
28 changes: 23 additions & 5 deletions public/markdown/examples/background.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,35 @@

## Description

This guide demonstrates how to add a background to the [canvas](./docs/f-canvas-component). To enable this feature, you need to add the [f-background component](./docs/f-background-component) inside the [f-flow component](./docs/f-flow-component).
There are two default patterns: ‘rect’ and ‘circle’, but you can also create your own custom pattern (instructions are available in the [f-background documentation](./docs/f-background-component)).
The background feature allows you to render an SVG pattern underneath all nodes and connections on the [canvas](./docs/f-canvas-component).
It is useful for creating grids, subtle textures, or branded backgrounds that stay fixed while the user interacts with the flow.

To enable a background, place the [f-background component](./docs/f-background-component) **inside** the [f-flow component](./docs/f-flow-component).
The `f-background` component is responsible for defining and attaching the SVG pattern that will be used as the canvas background.

Backgrounds can work in three modes:

- **`rect`** – built-in rectangular pattern (useful for grid-like backgrounds);
- **`circle`** – built-in circular pattern;
- **`custom`** – a mode that lets you provide your **own SVG pattern**, so you can build complex or branded backgrounds (multiple patterns, custom shapes, textures, etc.).

The `custom` mode is configured via standard SVG `<pattern>` elements and a custom component.
Detailed instructions and API for creating custom patterns are described in the [f-background documentation](./docs/f-background-component).

## Example

The example below shows how to:

- wrap `f-canvas` with `f-flow`;
- add `f-background` to enable a background;
- switch between the built-in `rect` and `circle` patterns;
- use the new `custom` mode with a custom background implementation.

::: ng-component <background-example></background-example> [height]="600"
[component.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.html
[component.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.ts
[component.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/background-example.component.scss
[custom.html] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.html
[custom.ts] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/extensions/background-example/custom-background-example/custom-background-example.ts
[common.scss] <<< https://raw.githubusercontent.com/Foblex/f-flow/main/projects/f-examples/_flow-common.scss
:::



8 changes: 6 additions & 2 deletions src/app/examples.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,13 +883,17 @@ function extensionGroup() {
link: 'background',
text: 'Background',
description:
'Add background shapes and frames to structure the canvas. Layering and styling patterns in Angular.',
'Add SVG background patterns to the flow diagrams in Angular, using built-in rect and circle presets or a fully custom pattern for complex, branded backgrounds.',
image: './previews/examples/background-example.light.png',
image_dark: './previews/examples/background-example.dark.png',
image_width: 821,
image_height: 600,
image_type: 'image/png',
date: new Date('2024-10-06 14:49:44'),
date: new Date('2025-11-29 14:49:44'),
badge: {
text: 'Updated',
type: 'info',
},
},
]);
}
Expand Down
Loading