https://github.com/zhongmiao-org/ngx-puzzle
Build dashboards with Angular drag-and-drop
https://github.com/zhongmiao-org/ngx-puzzle
Last synced: about 2 months ago
JSON representation
Build dashboards with Angular drag-and-drop
- Host: GitHub
- URL: https://github.com/zhongmiao-org/ngx-puzzle
- Owner: zhongmiao-org
- License: mit
- Created: 2025-06-26T14:53:59.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2025-08-30T07:19:42.000Z (about 2 months ago)
- Last Synced: 2025-08-30T07:22:26.940Z (about 2 months ago)
- Language: TypeScript
- Homepage:
- Size: 3.65 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
- fucking-awesome-angular - ngx-puzzle - Drag-and-drop dashboard builder for Angular applications. (Third Party Components / Drag and Drop)
- awesome-angular - ngx-puzzle - Drag-and-drop dashboard builder for Angular applications. (Third Party Components / Drag and Drop)
README
# ngx-puzzle
English | [中文文档](README.zh-CN.md)
[](https://www.npmjs.com/package/@zhongmiao/ngx-puzzle)
[](https://www.npmjs.com/package/@zhongmiao/ngx-puzzle)

[](LICENSE)
[](https://github.com/prettier/prettier)

Drag-and-drop dashboard builder for Angular applications. Think of it like a puzzle: compose charts, tables, text, and controls on a canvas to quickly assemble responsive data dashboards. Built with Angular standalone components and signals.
Suitable for rapid prototyping, internal BI dashboards, and data visualization portals.
## Features
- Drag-and-drop editor with snapping layout
- Rich built-in components: chart, table, text, control
- Architecture-first Angular library (standalone, signals, OnPush)
- External data-binding contract to connect real APIs or mock data
- Preview/save via external service hooks
## Installation
```bash
npm install @zhongmiao/ngx-puzzle
# peer deps
# Angular 18+, RxJS 7.8+, ngx-tethys 18.x, echarts 6.x
```
## Compatibility
- Angular: 18+
- RxJS: 7.8+
- ngx-tethys: 18.x (UI dialogs, layout in examples)
- ECharts: 6.x (used by chart components)
See package.json for exact versions.
## Quick Start (standalone)
Use the editor component directly in a standalone host component. Below is a minimal yet practical example adapted from the example app.
```ts
import { Component, inject, OnInit, OnDestroy } from '@angular/core';
import { ThyContent, ThyLayout } from 'ngx-tethys/layout';
import { NgxPuzzleEditorComponent } from 'ngx-puzzle';
import {
NgxPuzzleControlChangeNotification,
NgxPuzzleDataBindingRequest,
NgxPuzzleDataBindingService,
NgxPuzzleExternalService
} from 'ngx-puzzle/core';
import { Subject, takeUntil } from 'rxjs';
import { ThyDialog } from 'ngx-tethys/dialog';
import { ExampleDataSourceDialogComponent } from './data-source-dialog.component';
@Component({
selector: 'example-puzzle',
standalone: true,
template: `
`,
imports: [ThyLayout, ThyContent, NgxPuzzleEditorComponent]
})
export class AppPuzzleComponent implements OnInit, OnDestroy {
private puzzleService = inject(NgxPuzzleExternalService);
private dataBindingService = inject(NgxPuzzleDataBindingService);
private destroy$ = new Subject();
private dialog = inject(ThyDialog);
ngOnInit() {
this.dataBindingService.bindingRequest$.pipe(takeUntil(this.destroy$)).subscribe((request) => this.handleDataBindingRequest(request));
this.dataBindingService.controlChange$
.pipe(takeUntil(this.destroy$))
.subscribe((notification) => this.handleControlChange(notification));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
private handleDataBindingRequest(request: NgxPuzzleDataBindingRequest) {
const initialData: any = {};
if (request.apiSource) {
initialData.type = request.apiSource.method as 'GET' | 'POST';
initialData.url = request.apiSource.url;
if (request.apiSource.method === 'POST' && request.apiSource.params) {
try {
initialData.body = JSON.stringify(request.apiSource.params, null, 2);
} catch {
initialData.body = '';
}
}
}
const ref = this.dialog.open(ExampleDataSourceDialogComponent, {
initialState: {
inputType: initialData.type,
inputUrl: initialData.url,
inputBody: initialData.body
}
});
ref
.afterClosed()
.pipe(takeUntil(this.destroy$))
.subscribe((result: any) => {
if (!result) return;
const apiSource = this.createApiSourceFromDialog(result);
const existed = this.dataBindingService.getComponentDataRequest(request.componentId) || { apiSources: [] };
const streams = existed.apiSources ? [...existed.apiSources] : [];
if (apiSource) streams[request.seriesIndex] = apiSource;
this.dataBindingService.responseBinding({
componentId: request.componentId,
dataRequest: { ...existed, apiSources: streams }
});
});
}
private handleControlChange(notification: NgxPuzzleControlChangeNotification) {
const newSources = [
{ url: '/api/chart-data-1', method: 'POST', params: this.buildParamsFromFilters(notification.controlFilters) },
{ url: '/api/chart-data-2', method: 'POST', params: this.buildParamsFromFilters(notification.controlFilters) }
];
this.dataBindingService.responseBinding({
componentId: notification.componentId,
dataRequest: { apiSources: newSources }
});
}
private createApiSourceFromDialog(result: {
type: 'GET' | 'POST';
url: string;
body?: string;
}): { url: string; method: string; params?: Record } | undefined {
if (result?.url && result.url.trim()) {
const url = result.url.trim();
if (result.type === 'POST') {
let payload: unknown;
try {
payload = result.body ? JSON.parse(result.body) : {};
} catch {
payload = {};
}
return { url, method: 'POST', params: payload as Record };
}
return { url, method: 'GET' };
}
return undefined; // fallback to component mock
}
private buildParamsFromFilters(filters: unknown) {
return { filters };
}
save() {
this.puzzleService.getAllConfigs();
}
preview() {
this.puzzleService.generatePreviewId();
}
}
```
### Data source dialog used above
```ts
import { Component, inject, input, OnInit, signal } from '@angular/core';
import { ThyDialog, ThyDialogBody, ThyDialogFooter, ThyDialogHeader } from 'ngx-tethys/dialog';
import { ThySelect } from 'ngx-tethys/select';
import { ThyOption } from 'ngx-tethys/shared';
import { FormsModule } from '@angular/forms';
import { ThyInputDirective } from 'ngx-tethys/input';
import { ThyButton } from 'ngx-tethys/button';
import { NgIf } from '@angular/common';
@Component({
selector: 'example-data-source-dialog',
standalone: true,
imports: [ThyDialogHeader, ThyDialogBody, ThyDialogFooter, ThySelect, ThyOption, FormsModule, ThyInputDirective, ThyButton, NgIf],
template: `...` // see example app for full template
})
export class ExampleDataSourceDialogComponent implements OnInit {
private dialog = inject(ThyDialog);
inputType = input<'GET' | 'POST'>('GET');
inputUrl = input('');
inputBody = input('');
type = signal<'GET' | 'POST'>('GET');
url = signal('');
body = signal('');
ngOnInit() {
this.type.set(this.inputType() ?? 'GET');
this.url.set(this.inputUrl() ?? '');
this.body.set(this.inputBody() ?? '');
}
confirm() {
this.dialog.close({ type: this.type(), url: this.url(), body: this.body() });
}
close() {
this.dialog.close();
}
}
```
## Architecture Overview
- Standalone components only (no NgModules). Use Angular signals for local state and computed() for derived state.
- OnPush change detection for performance.
- External data binding via NgxPuzzleDataBindingService:
- bindingRequest$: component requests data (includes componentId, seriesIndex, and optional apiSource)
- responseBinding(...): host responds with dataRequest containing apiSources array
- controlChange$: control components notify filter changes; host can update apiSources
- NgxPuzzleExternalService: retrieve/save editor configs and generate preview id.
## Usage Notes and Best Practices
- Prefer signals over mutable state; use set/update, avoid mutate.
- Keep templates simple; use Angular built-in control flow (@if/@for).
- Use host bindings in the decorator's host field, not HostBinding/HostListener decorators.
- Use NgOptimizedImage for static images.
## Run the example
```bash
npm install
npm start
# open http://localhost:4200 and navigate to the example page
```
## Contributing
See CONTRIBUTING.md (and CONTRIBUTING.zh-CN.md for Chinese).
## Acknowledgements
- ngx-tethys (UI components, dialogs, layout used in examples): https://github.com/atinc/ngx-tethys
- Apache ECharts (chart rendering for built-in chart components): https://echarts.apache.org/ and https://github.com/apache/echarts
## License
MIT. See LICENSE.
## Contributors
- ark65 (liuwufangzhou@gmail.com, liuwufangzhou@qq.vip.com)