@toolbox-web/grid-angular 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/fesm2022/toolbox-web-grid-angular-features-clipboard.mjs +30 -0
- package/fesm2022/toolbox-web-grid-angular-features-clipboard.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-column-virtualization.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-column-virtualization.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-context-menu.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-context-menu.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-editing.mjs +33 -0
- package/fesm2022/toolbox-web-grid-angular-features-editing.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-export.mjs +29 -0
- package/fesm2022/toolbox-web-grid-angular-features-export.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-filtering.mjs +29 -0
- package/fesm2022/toolbox-web-grid-angular-features-filtering.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-grouping-columns.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-grouping-columns.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-grouping-rows.mjs +25 -0
- package/fesm2022/toolbox-web-grid-angular-features-grouping-rows.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-master-detail.mjs +25 -0
- package/fesm2022/toolbox-web-grid-angular-features-master-detail.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-multi-sort.mjs +40 -0
- package/fesm2022/toolbox-web-grid-angular-features-multi-sort.mjs.map +1 -0
- package/{features/pinned-columns.d.ts → fesm2022/toolbox-web-grid-angular-features-pinned-columns.mjs} +11 -2
- package/fesm2022/toolbox-web-grid-angular-features-pinned-columns.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-pinned-rows.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-pinned-rows.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-pivot.mjs +25 -0
- package/fesm2022/toolbox-web-grid-angular-features-pivot.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-print.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-print.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-reorder.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-reorder.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-responsive.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-responsive.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-row-reorder.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-row-reorder.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-selection.mjs +30 -0
- package/fesm2022/toolbox-web-grid-angular-features-selection.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-server-side.mjs +25 -0
- package/fesm2022/toolbox-web-grid-angular-features-server-side.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-sorting.mjs +25 -0
- package/fesm2022/toolbox-web-grid-angular-features-sorting.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-tree.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-tree.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-undo-redo.mjs +30 -0
- package/fesm2022/toolbox-web-grid-angular-features-undo-redo.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular-features-visibility.mjs +28 -0
- package/fesm2022/toolbox-web-grid-angular-features-visibility.mjs.map +1 -0
- package/fesm2022/toolbox-web-grid-angular.mjs +3376 -0
- package/fesm2022/toolbox-web-grid-angular.mjs.map +1 -0
- package/package.json +109 -35
- package/types/toolbox-web-grid-angular-features-clipboard.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-clipboard.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-column-virtualization.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-column-virtualization.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-context-menu.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-context-menu.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-editing.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-editing.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-export.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-export.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-filtering.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-filtering.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-grouping-columns.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-grouping-columns.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-grouping-rows.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-grouping-rows.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-master-detail.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-master-detail.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-multi-sort.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-multi-sort.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-pinned-columns.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-pinned-columns.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-pinned-rows.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-pinned-rows.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-pivot.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-pivot.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-print.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-print.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-reorder.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-reorder.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-responsive.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-responsive.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-row-reorder.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-row-reorder.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-selection.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-selection.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-server-side.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-server-side.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-sorting.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-sorting.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-tree.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-tree.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-undo-redo.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-undo-redo.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular-features-visibility.d.ts +3 -0
- package/types/toolbox-web-grid-angular-features-visibility.d.ts.map +1 -0
- package/types/toolbox-web-grid-angular.d.ts +2468 -0
- package/types/toolbox-web-grid-angular.d.ts.map +1 -0
- package/feature-registry-C-cKloXB.js +0 -45
- package/features/clipboard.d.ts +0 -18
- package/features/clipboard.d.ts.map +0 -1
- package/features/clipboard.js +0 -3
- package/features/column-virtualization.d.ts +0 -16
- package/features/column-virtualization.d.ts.map +0 -1
- package/features/column-virtualization.js +0 -3
- package/features/context-menu.d.ts +0 -16
- package/features/context-menu.d.ts.map +0 -1
- package/features/context-menu.js +0 -3
- package/features/editing.d.ts +0 -16
- package/features/editing.d.ts.map +0 -1
- package/features/editing.js +0 -3
- package/features/export.d.ts +0 -17
- package/features/export.d.ts.map +0 -1
- package/features/export.js +0 -3
- package/features/filtering.d.ts +0 -17
- package/features/filtering.d.ts.map +0 -1
- package/features/filtering.js +0 -3
- package/features/grouping-columns.d.ts +0 -16
- package/features/grouping-columns.d.ts.map +0 -1
- package/features/grouping-columns.js +0 -3
- package/features/grouping-rows.d.ts +0 -16
- package/features/grouping-rows.d.ts.map +0 -1
- package/features/grouping-rows.js +0 -3
- package/features/index.d.ts +0 -1
- package/features/index.d.ts.map +0 -1
- package/features/index.js +0 -22
- package/features/master-detail.d.ts +0 -16
- package/features/master-detail.d.ts.map +0 -1
- package/features/master-detail.js +0 -3
- package/features/multi-sort.d.ts +0 -22
- package/features/multi-sort.d.ts.map +0 -1
- package/features/multi-sort.js +0 -3
- package/features/pinned-columns.d.ts.map +0 -1
- package/features/pinned-columns.js +0 -3
- package/features/pinned-rows.d.ts +0 -16
- package/features/pinned-rows.d.ts.map +0 -1
- package/features/pinned-rows.js +0 -3
- package/features/pivot.d.ts +0 -16
- package/features/pivot.d.ts.map +0 -1
- package/features/pivot.js +0 -3
- package/features/print.d.ts +0 -16
- package/features/print.d.ts.map +0 -1
- package/features/print.js +0 -3
- package/features/reorder.d.ts +0 -16
- package/features/reorder.d.ts.map +0 -1
- package/features/reorder.js +0 -3
- package/features/responsive.d.ts +0 -16
- package/features/responsive.d.ts.map +0 -1
- package/features/responsive.js +0 -3
- package/features/row-reorder.d.ts +0 -16
- package/features/row-reorder.d.ts.map +0 -1
- package/features/row-reorder.js +0 -3
- package/features/selection.d.ts +0 -16
- package/features/selection.d.ts.map +0 -1
- package/features/selection.js +0 -3
- package/features/server-side.d.ts +0 -16
- package/features/server-side.d.ts.map +0 -1
- package/features/server-side.js +0 -3
- package/features/sorting.d.ts +0 -1
- package/features/sorting.d.ts.map +0 -1
- package/features/sorting.js +0 -1
- package/features/tree.d.ts +0 -16
- package/features/tree.d.ts.map +0 -1
- package/features/tree.js +0 -3
- package/features/undo-redo.d.ts +0 -18
- package/features/undo-redo.d.ts.map +0 -1
- package/features/undo-redo.js +0 -3
- package/features/visibility.d.ts +0 -16
- package/features/visibility.d.ts.map +0 -1
- package/features/visibility.js +0 -3
- package/index.d.ts +0 -34
- package/index.d.ts.map +0 -1
- package/index.js +0 -1916
- package/lib/angular-column-config.d.ts +0 -140
- package/lib/angular-column-config.d.ts.map +0 -1
- package/lib/angular-grid-adapter.d.ts +0 -220
- package/lib/angular-grid-adapter.d.ts.map +0 -1
- package/lib/base-grid-editor.d.ts +0 -145
- package/lib/base-grid-editor.d.ts.map +0 -1
- package/lib/component-registry.d.ts +0 -63
- package/lib/component-registry.d.ts.map +0 -1
- package/lib/directives/grid-column-editor.directive.d.ts +0 -113
- package/lib/directives/grid-column-editor.directive.d.ts.map +0 -1
- package/lib/directives/grid-column-view.directive.d.ts +0 -69
- package/lib/directives/grid-column-view.directive.d.ts.map +0 -1
- package/lib/directives/grid-detail-view.directive.d.ts +0 -75
- package/lib/directives/grid-detail-view.directive.d.ts.map +0 -1
- package/lib/directives/grid-form-array.directive.d.ts +0 -141
- package/lib/directives/grid-form-array.directive.d.ts.map +0 -1
- package/lib/directives/grid-responsive-card.directive.d.ts +0 -92
- package/lib/directives/grid-responsive-card.directive.d.ts.map +0 -1
- package/lib/directives/grid-tool-panel.directive.d.ts +0 -91
- package/lib/directives/grid-tool-panel.directive.d.ts.map +0 -1
- package/lib/directives/grid.directive.d.ts +0 -808
- package/lib/directives/grid.directive.d.ts.map +0 -1
- package/lib/directives/index.d.ts +0 -9
- package/lib/directives/index.d.ts.map +0 -1
- package/lib/directives/structural-directives.d.ts +0 -174
- package/lib/directives/structural-directives.d.ts.map +0 -1
- package/lib/feature-registry.d.ts +0 -72
- package/lib/feature-registry.d.ts.map +0 -1
- package/lib/grid-type-registry.d.ts +0 -110
- package/lib/grid-type-registry.d.ts.map +0 -1
- package/lib/inject-grid.d.ts +0 -109
- package/lib/inject-grid.d.ts.map +0 -1
- package/lib/interfaces/grid-cell-editor.d.ts +0 -85
- package/lib/interfaces/grid-cell-editor.d.ts.map +0 -1
- package/lib/interfaces/grid-cell-renderer.d.ts +0 -63
- package/lib/interfaces/grid-cell-renderer.d.ts.map +0 -1
- package/lib/interfaces/index.d.ts +0 -5
- package/lib/interfaces/index.d.ts.map +0 -1
|
@@ -0,0 +1,3376 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, ElementRef, contentChild, TemplateRef, effect, Directive, input, InjectionToken, Injectable, makeEnvironmentProviders, EventEmitter, createComponent, signal, afterNextRender, computed, output, EnvironmentInjector, ApplicationRef, ViewContainerRef } from '@angular/core';
|
|
3
|
+
import { FormGroup } from '@angular/forms';
|
|
4
|
+
import { DataGridElement } from '@toolbox-web/grid';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type guard to check if a value is an Angular component class.
|
|
8
|
+
*
|
|
9
|
+
* Detects Angular components by checking for internal Angular markers:
|
|
10
|
+
* - ɵcmp (component definition)
|
|
11
|
+
* - ɵfac (factory function)
|
|
12
|
+
*
|
|
13
|
+
* Also checks if it's an ES6 class (vs function) by inspecting the
|
|
14
|
+
* string representation.
|
|
15
|
+
*/
|
|
16
|
+
function isComponentClass(value) {
|
|
17
|
+
if (typeof value !== 'function' || value.prototype === undefined) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
// Check for Angular component markers (AOT compiled)
|
|
21
|
+
if (Object.prototype.hasOwnProperty.call(value, 'ɵcmp') || Object.prototype.hasOwnProperty.call(value, 'ɵfac')) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
// Check if it's an ES6 class (vs regular function)
|
|
25
|
+
// Class definitions start with "class" in their toString()
|
|
26
|
+
const fnString = Function.prototype.toString.call(value);
|
|
27
|
+
return fnString.startsWith('class ') || fnString.startsWith('class{');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Global registry mapping DOM elements to their templates
|
|
31
|
+
const editorTemplateRegistry = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Gets the editor template registered for a given element.
|
|
34
|
+
* Used by AngularGridAdapter to retrieve templates at render time.
|
|
35
|
+
*/
|
|
36
|
+
function getEditorTemplate(element) {
|
|
37
|
+
return editorTemplateRegistry.get(element);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Directive that captures an `<ng-template>` for use as a cell editor.
|
|
41
|
+
*
|
|
42
|
+
* This enables declarative Angular component usage with proper input bindings
|
|
43
|
+
* that satisfy Angular's AOT compiler.
|
|
44
|
+
*
|
|
45
|
+
* ## Usage
|
|
46
|
+
*
|
|
47
|
+
* ```html
|
|
48
|
+
* <tbw-grid-column field="status" editable>
|
|
49
|
+
* <tbw-grid-column-editor>
|
|
50
|
+
* <ng-template let-value let-row="row" let-onCommit="onCommit" let-onCancel="onCancel">
|
|
51
|
+
* <app-status-select
|
|
52
|
+
* [value]="value"
|
|
53
|
+
* [row]="row"
|
|
54
|
+
* (commit)="onCommit($event)"
|
|
55
|
+
* (cancel)="onCancel()"
|
|
56
|
+
* />
|
|
57
|
+
* </ng-template>
|
|
58
|
+
* </tbw-grid-column-editor>
|
|
59
|
+
* </tbw-grid-column>
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* The template context provides:
|
|
63
|
+
* - `$implicit` / `value`: The cell value
|
|
64
|
+
* - `row`: The full row data object
|
|
65
|
+
* - `column`: The column configuration
|
|
66
|
+
* - `onCommit`: Callback function to commit the new value
|
|
67
|
+
* - `onCancel`: Callback function to cancel editing
|
|
68
|
+
*
|
|
69
|
+
* Import the directive in your component:
|
|
70
|
+
*
|
|
71
|
+
* ```typescript
|
|
72
|
+
* import { GridColumnEditor } from '@toolbox-web/grid-angular';
|
|
73
|
+
*
|
|
74
|
+
* @Component({
|
|
75
|
+
* imports: [GridColumnEditor],
|
|
76
|
+
* // ...
|
|
77
|
+
* })
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
class GridColumnEditor {
|
|
81
|
+
elementRef = inject((ElementRef));
|
|
82
|
+
/**
|
|
83
|
+
* Query for the ng-template content child.
|
|
84
|
+
*/
|
|
85
|
+
template = contentChild((TemplateRef), ...(ngDevMode ? [{ debugName: "template" }] : []));
|
|
86
|
+
/** Effect that triggers when the template is available */
|
|
87
|
+
onTemplateReceived = effect(() => {
|
|
88
|
+
const template = this.template();
|
|
89
|
+
if (template) {
|
|
90
|
+
// Register the template for this element
|
|
91
|
+
editorTemplateRegistry.set(this.elementRef.nativeElement, template);
|
|
92
|
+
}
|
|
93
|
+
}, ...(ngDevMode ? [{ debugName: "onTemplateReceived" }] : []));
|
|
94
|
+
/**
|
|
95
|
+
* Static type guard for template context.
|
|
96
|
+
* Enables type inference in templates.
|
|
97
|
+
*/
|
|
98
|
+
static ngTemplateContextGuard(dir, ctx) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridColumnEditor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
102
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.1", type: GridColumnEditor, isStandalone: true, selector: "tbw-grid-column-editor", queries: [{ propertyName: "template", first: true, predicate: (TemplateRef), descendants: true, isSignal: true }], ngImport: i0 });
|
|
103
|
+
}
|
|
104
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridColumnEditor, decorators: [{
|
|
105
|
+
type: Directive,
|
|
106
|
+
args: [{ selector: 'tbw-grid-column-editor' }]
|
|
107
|
+
}], propDecorators: { template: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
|
|
108
|
+
|
|
109
|
+
// Global registry mapping DOM elements to their templates
|
|
110
|
+
const templateRegistry = new Map();
|
|
111
|
+
/**
|
|
112
|
+
* Gets the template registered for a given element.
|
|
113
|
+
* Used by AngularGridAdapter to retrieve templates at render time.
|
|
114
|
+
*/
|
|
115
|
+
function getViewTemplate(element) {
|
|
116
|
+
return templateRegistry.get(element);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Directive that captures an `<ng-template>` for use as a cell renderer.
|
|
120
|
+
*
|
|
121
|
+
* This enables declarative Angular component usage with proper input bindings
|
|
122
|
+
* that satisfy Angular's AOT compiler.
|
|
123
|
+
*
|
|
124
|
+
* ## Usage
|
|
125
|
+
*
|
|
126
|
+
* ```html
|
|
127
|
+
* <tbw-grid-column field="status">
|
|
128
|
+
* <tbw-grid-column-view>
|
|
129
|
+
* <ng-template let-value let-row="row">
|
|
130
|
+
* <app-status-badge [value]="value" [row]="row" />
|
|
131
|
+
* </ng-template>
|
|
132
|
+
* </tbw-grid-column-view>
|
|
133
|
+
* </tbw-grid-column>
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* The template context provides:
|
|
137
|
+
* - `$implicit` / `value`: The cell value
|
|
138
|
+
* - `row`: The full row data object
|
|
139
|
+
* - `column`: The column configuration
|
|
140
|
+
*
|
|
141
|
+
* Import the directive in your component:
|
|
142
|
+
*
|
|
143
|
+
* ```typescript
|
|
144
|
+
* import { GridColumnView } from '@toolbox-web/grid-angular';
|
|
145
|
+
*
|
|
146
|
+
* @Component({
|
|
147
|
+
* imports: [GridColumnView],
|
|
148
|
+
* // ...
|
|
149
|
+
* })
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
class GridColumnView {
|
|
153
|
+
elementRef = inject((ElementRef));
|
|
154
|
+
/**
|
|
155
|
+
* Query for the ng-template content child.
|
|
156
|
+
*/
|
|
157
|
+
template = contentChild((TemplateRef), ...(ngDevMode ? [{ debugName: "template" }] : []));
|
|
158
|
+
/** Effect that triggers when the template is available */
|
|
159
|
+
onTemplateReceived = effect(() => {
|
|
160
|
+
const template = this.template();
|
|
161
|
+
if (template) {
|
|
162
|
+
// Register the template for this element
|
|
163
|
+
templateRegistry.set(this.elementRef.nativeElement, template);
|
|
164
|
+
}
|
|
165
|
+
}, ...(ngDevMode ? [{ debugName: "onTemplateReceived" }] : []));
|
|
166
|
+
/**
|
|
167
|
+
* Static type guard for template context.
|
|
168
|
+
* Enables type inference in templates.
|
|
169
|
+
*/
|
|
170
|
+
static ngTemplateContextGuard(dir, ctx) {
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridColumnView, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
174
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.1", type: GridColumnView, isStandalone: true, selector: "tbw-grid-column-view", queries: [{ propertyName: "template", first: true, predicate: (TemplateRef), descendants: true, isSignal: true }], ngImport: i0 });
|
|
175
|
+
}
|
|
176
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridColumnView, decorators: [{
|
|
177
|
+
type: Directive,
|
|
178
|
+
args: [{ selector: 'tbw-grid-column-view' }]
|
|
179
|
+
}], propDecorators: { template: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
|
|
180
|
+
|
|
181
|
+
// Global registry mapping DOM elements to their templates
|
|
182
|
+
const detailTemplateRegistry = new Map();
|
|
183
|
+
/**
|
|
184
|
+
* Gets the detail template registered for a given grid element.
|
|
185
|
+
* Used by AngularGridAdapter to retrieve templates at render time.
|
|
186
|
+
*/
|
|
187
|
+
function getDetailTemplate(gridElement) {
|
|
188
|
+
// Look for tbw-grid-detail child and get its template
|
|
189
|
+
const detailElement = gridElement.querySelector('tbw-grid-detail');
|
|
190
|
+
if (detailElement) {
|
|
191
|
+
return detailTemplateRegistry.get(detailElement);
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Gets the configuration for the detail view.
|
|
197
|
+
*/
|
|
198
|
+
function getDetailConfig(gridElement) {
|
|
199
|
+
const detailElement = gridElement.querySelector('tbw-grid-detail');
|
|
200
|
+
if (detailElement) {
|
|
201
|
+
const animationAttr = detailElement.getAttribute('animation');
|
|
202
|
+
let animation = 'slide';
|
|
203
|
+
if (animationAttr === 'false') {
|
|
204
|
+
animation = false;
|
|
205
|
+
}
|
|
206
|
+
else if (animationAttr === 'fade') {
|
|
207
|
+
animation = 'fade';
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
showExpandColumn: detailElement.getAttribute('showExpandColumn') !== 'false',
|
|
211
|
+
animation,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Directive that captures an `<ng-template>` for use as a master-detail row renderer.
|
|
218
|
+
*
|
|
219
|
+
* This enables declarative Angular component usage for expandable detail rows
|
|
220
|
+
* that appear below the main row when expanded.
|
|
221
|
+
*
|
|
222
|
+
* ## Usage
|
|
223
|
+
*
|
|
224
|
+
* ```html
|
|
225
|
+
* <tbw-grid [rows]="rows" [gridConfig]="config">
|
|
226
|
+
* <tbw-grid-detail [showExpandColumn]="true" animation="slide">
|
|
227
|
+
* <ng-template let-row>
|
|
228
|
+
* <app-detail-panel [employee]="row" />
|
|
229
|
+
* </ng-template>
|
|
230
|
+
* </tbw-grid-detail>
|
|
231
|
+
* </tbw-grid>
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* The template context provides:
|
|
235
|
+
* - `$implicit` / `row`: The full row data object
|
|
236
|
+
*
|
|
237
|
+
* Import the directive in your component:
|
|
238
|
+
*
|
|
239
|
+
* ```typescript
|
|
240
|
+
* import { GridDetailView } from '@toolbox-web/grid-angular';
|
|
241
|
+
*
|
|
242
|
+
* @Component({
|
|
243
|
+
* imports: [GridDetailView],
|
|
244
|
+
* // ...
|
|
245
|
+
* })
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
class GridDetailView {
|
|
249
|
+
elementRef = inject((ElementRef));
|
|
250
|
+
/** Whether to show the expand/collapse column. Default: true */
|
|
251
|
+
showExpandColumn = input(true, ...(ngDevMode ? [{ debugName: "showExpandColumn" }] : []));
|
|
252
|
+
/** Animation style for expand/collapse. Default: 'slide' */
|
|
253
|
+
animation = input('slide', ...(ngDevMode ? [{ debugName: "animation" }] : []));
|
|
254
|
+
/**
|
|
255
|
+
* Query for the ng-template content child.
|
|
256
|
+
*/
|
|
257
|
+
template = contentChild((TemplateRef), ...(ngDevMode ? [{ debugName: "template" }] : []));
|
|
258
|
+
/** Effect that triggers when the template is available */
|
|
259
|
+
onTemplateReceived = effect(() => {
|
|
260
|
+
const template = this.template();
|
|
261
|
+
if (template) {
|
|
262
|
+
// Register the template for this element
|
|
263
|
+
detailTemplateRegistry.set(this.elementRef.nativeElement, template);
|
|
264
|
+
}
|
|
265
|
+
}, ...(ngDevMode ? [{ debugName: "onTemplateReceived" }] : []));
|
|
266
|
+
/**
|
|
267
|
+
* Static type guard for template context.
|
|
268
|
+
* Enables type inference in templates.
|
|
269
|
+
*/
|
|
270
|
+
static ngTemplateContextGuard(dir, ctx) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridDetailView, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
274
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.1", type: GridDetailView, isStandalone: true, selector: "tbw-grid-detail", inputs: { showExpandColumn: { classPropertyName: "showExpandColumn", publicName: "showExpandColumn", isSignal: true, isRequired: false, transformFunction: null }, animation: { classPropertyName: "animation", publicName: "animation", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "template", first: true, predicate: (TemplateRef), descendants: true, isSignal: true }], ngImport: i0 });
|
|
275
|
+
}
|
|
276
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridDetailView, decorators: [{
|
|
277
|
+
type: Directive,
|
|
278
|
+
args: [{ selector: 'tbw-grid-detail' }]
|
|
279
|
+
}], propDecorators: { showExpandColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "showExpandColumn", required: false }] }], animation: [{ type: i0.Input, args: [{ isSignal: true, alias: "animation", required: false }] }], template: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
|
|
280
|
+
|
|
281
|
+
// Symbol for storing form context on the grid element
|
|
282
|
+
const FORM_ARRAY_CONTEXT = Symbol('formArrayContext');
|
|
283
|
+
/**
|
|
284
|
+
* Gets the FormArrayContext from a grid element, if present.
|
|
285
|
+
* @internal
|
|
286
|
+
*/
|
|
287
|
+
function getFormArrayContext(gridElement) {
|
|
288
|
+
return gridElement[FORM_ARRAY_CONTEXT];
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Directive that binds a FormArray directly to the grid.
|
|
292
|
+
*
|
|
293
|
+
* This is the recommended way to integrate tbw-grid with Angular Reactive Forms.
|
|
294
|
+
* Use a FormArray of FormGroups for row-level validation and cell-level control access.
|
|
295
|
+
*
|
|
296
|
+
* ## Usage
|
|
297
|
+
*
|
|
298
|
+
* ```typescript
|
|
299
|
+
* import { Component, inject } from '@angular/core';
|
|
300
|
+
* import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
|
301
|
+
* import { Grid, GridFormArray } from '@toolbox-web/grid-angular';
|
|
302
|
+
*
|
|
303
|
+
* @Component({
|
|
304
|
+
* imports: [Grid, GridFormArray, ReactiveFormsModule],
|
|
305
|
+
* template: \`
|
|
306
|
+
* <form [formGroup]="form">
|
|
307
|
+
* <tbw-grid [formArray]="form.controls.rows" [columns]="columns" />
|
|
308
|
+
* </form>
|
|
309
|
+
* \`
|
|
310
|
+
* })
|
|
311
|
+
* export class MyComponent {
|
|
312
|
+
* private fb = inject(FormBuilder);
|
|
313
|
+
*
|
|
314
|
+
* form = this.fb.group({
|
|
315
|
+
* rows: this.fb.array([
|
|
316
|
+
* this.fb.group({ name: 'Alice', age: 30 }),
|
|
317
|
+
* this.fb.group({ name: 'Bob', age: 25 }),
|
|
318
|
+
* ])
|
|
319
|
+
* });
|
|
320
|
+
*
|
|
321
|
+
* columns = [
|
|
322
|
+
* { field: 'name', header: 'Name', editable: true },
|
|
323
|
+
* { field: 'age', header: 'Age', editable: true }
|
|
324
|
+
* ];
|
|
325
|
+
* }
|
|
326
|
+
* ```
|
|
327
|
+
*
|
|
328
|
+
* ## How It Works
|
|
329
|
+
*
|
|
330
|
+
* - **FormArray → Grid**: The grid displays the FormArray's value as rows
|
|
331
|
+
* - **Grid → FormArray**: When a cell is edited, the corresponding FormControl is updated
|
|
332
|
+
* - FormArrayContext is available for accessing cell-level controls
|
|
333
|
+
*
|
|
334
|
+
* ## Features
|
|
335
|
+
*
|
|
336
|
+
* - Works naturally with FormArray inside a FormGroup
|
|
337
|
+
* - Provides cell-level FormControl access for validation
|
|
338
|
+
* - Supports row-level validation state aggregation
|
|
339
|
+
* - Automatically syncs FormArray changes to the grid
|
|
340
|
+
*/
|
|
341
|
+
class GridFormArray {
|
|
342
|
+
elementRef = inject((ElementRef));
|
|
343
|
+
cellCommitListener = null;
|
|
344
|
+
touchListener = null;
|
|
345
|
+
/**
|
|
346
|
+
* The FormArray to bind to the grid.
|
|
347
|
+
*/
|
|
348
|
+
formArray = input.required(...(ngDevMode ? [{ debugName: "formArray" }] : []));
|
|
349
|
+
/**
|
|
350
|
+
* Effect that syncs the FormArray value to the grid rows.
|
|
351
|
+
*/
|
|
352
|
+
syncFormArrayToGrid = effect(() => {
|
|
353
|
+
const formArray = this.formArray();
|
|
354
|
+
const grid = this.elementRef.nativeElement;
|
|
355
|
+
if (grid && formArray) {
|
|
356
|
+
// Get the raw value (including disabled controls)
|
|
357
|
+
grid.rows = formArray.getRawValue();
|
|
358
|
+
}
|
|
359
|
+
}, ...(ngDevMode ? [{ debugName: "syncFormArrayToGrid" }] : []));
|
|
360
|
+
ngOnInit() {
|
|
361
|
+
const grid = this.elementRef.nativeElement;
|
|
362
|
+
if (!grid)
|
|
363
|
+
return;
|
|
364
|
+
// Store the form context on the grid element for other directives to access
|
|
365
|
+
this.#storeFormContext(grid);
|
|
366
|
+
// Intercept cell-commit events to update the FormArray
|
|
367
|
+
this.cellCommitListener = (e) => {
|
|
368
|
+
const detail = e.detail;
|
|
369
|
+
this.#handleCellCommit(detail);
|
|
370
|
+
};
|
|
371
|
+
grid.addEventListener('cell-commit', this.cellCommitListener);
|
|
372
|
+
// Mark FormArray as touched on first interaction
|
|
373
|
+
this.touchListener = () => {
|
|
374
|
+
this.formArray().markAsTouched();
|
|
375
|
+
// Remove after first touch
|
|
376
|
+
if (this.touchListener) {
|
|
377
|
+
grid.removeEventListener('click', this.touchListener);
|
|
378
|
+
this.touchListener = null;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
grid.addEventListener('click', this.touchListener);
|
|
382
|
+
}
|
|
383
|
+
ngOnDestroy() {
|
|
384
|
+
const grid = this.elementRef.nativeElement;
|
|
385
|
+
if (!grid)
|
|
386
|
+
return;
|
|
387
|
+
if (this.cellCommitListener) {
|
|
388
|
+
grid.removeEventListener('cell-commit', this.cellCommitListener);
|
|
389
|
+
}
|
|
390
|
+
if (this.touchListener) {
|
|
391
|
+
grid.removeEventListener('click', this.touchListener);
|
|
392
|
+
}
|
|
393
|
+
this.#clearFormContext(grid);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Checks if the FormArray contains FormGroups.
|
|
397
|
+
*/
|
|
398
|
+
#isFormArrayOfFormGroups() {
|
|
399
|
+
const formArray = this.formArray();
|
|
400
|
+
if (formArray.length === 0)
|
|
401
|
+
return false;
|
|
402
|
+
return formArray.at(0) instanceof FormGroup;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Gets the FormGroup at a specific row index.
|
|
406
|
+
*/
|
|
407
|
+
#getRowFormGroup(rowIndex) {
|
|
408
|
+
const formArray = this.formArray();
|
|
409
|
+
const rowControl = formArray.at(rowIndex);
|
|
410
|
+
return rowControl instanceof FormGroup ? rowControl : undefined;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Stores the FormArrayContext on the grid element.
|
|
414
|
+
*/
|
|
415
|
+
#storeFormContext(grid) {
|
|
416
|
+
const getRowFormGroup = (rowIndex) => this.#getRowFormGroup(rowIndex);
|
|
417
|
+
const context = {
|
|
418
|
+
getRow: (rowIndex) => {
|
|
419
|
+
const formArray = this.formArray();
|
|
420
|
+
const rowControl = formArray.at(rowIndex);
|
|
421
|
+
return rowControl ? rowControl.value : null;
|
|
422
|
+
},
|
|
423
|
+
updateField: (rowIndex, field, value) => {
|
|
424
|
+
const rowFormGroup = getRowFormGroup(rowIndex);
|
|
425
|
+
if (rowFormGroup) {
|
|
426
|
+
const control = rowFormGroup.get(field);
|
|
427
|
+
if (control) {
|
|
428
|
+
control.setValue(value);
|
|
429
|
+
control.markAsDirty();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
getValue: () => {
|
|
434
|
+
return this.formArray().getRawValue();
|
|
435
|
+
},
|
|
436
|
+
hasFormGroups: this.#isFormArrayOfFormGroups(),
|
|
437
|
+
getControl: (rowIndex, field) => {
|
|
438
|
+
const rowFormGroup = getRowFormGroup(rowIndex);
|
|
439
|
+
if (!rowFormGroup)
|
|
440
|
+
return undefined;
|
|
441
|
+
return rowFormGroup.get(field) ?? undefined;
|
|
442
|
+
},
|
|
443
|
+
getRowFormGroup,
|
|
444
|
+
isRowValid: (rowIndex) => {
|
|
445
|
+
const rowFormGroup = getRowFormGroup(rowIndex);
|
|
446
|
+
if (!rowFormGroup)
|
|
447
|
+
return true;
|
|
448
|
+
return rowFormGroup.valid;
|
|
449
|
+
},
|
|
450
|
+
isRowTouched: (rowIndex) => {
|
|
451
|
+
const rowFormGroup = getRowFormGroup(rowIndex);
|
|
452
|
+
if (!rowFormGroup)
|
|
453
|
+
return false;
|
|
454
|
+
return rowFormGroup.touched;
|
|
455
|
+
},
|
|
456
|
+
isRowDirty: (rowIndex) => {
|
|
457
|
+
const rowFormGroup = getRowFormGroup(rowIndex);
|
|
458
|
+
if (!rowFormGroup)
|
|
459
|
+
return false;
|
|
460
|
+
return rowFormGroup.dirty;
|
|
461
|
+
},
|
|
462
|
+
getRowErrors: (rowIndex) => {
|
|
463
|
+
const rowFormGroup = getRowFormGroup(rowIndex);
|
|
464
|
+
if (!rowFormGroup)
|
|
465
|
+
return null;
|
|
466
|
+
const errors = {};
|
|
467
|
+
let hasErrors = false;
|
|
468
|
+
Object.keys(rowFormGroup.controls).forEach((field) => {
|
|
469
|
+
const control = rowFormGroup.get(field);
|
|
470
|
+
if (control?.errors) {
|
|
471
|
+
errors[field] = control.errors;
|
|
472
|
+
hasErrors = true;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
if (rowFormGroup.errors) {
|
|
476
|
+
errors['_group'] = rowFormGroup.errors;
|
|
477
|
+
hasErrors = true;
|
|
478
|
+
}
|
|
479
|
+
return hasErrors ? errors : null;
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
grid[FORM_ARRAY_CONTEXT] = context;
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Clears the FormArrayContext from the grid element.
|
|
486
|
+
*/
|
|
487
|
+
#clearFormContext(grid) {
|
|
488
|
+
delete grid[FORM_ARRAY_CONTEXT];
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Handles cell-commit events by updating the FormControl in the FormGroup.
|
|
492
|
+
*/
|
|
493
|
+
#handleCellCommit(detail) {
|
|
494
|
+
const { rowIndex, field, value } = detail;
|
|
495
|
+
const rowFormGroup = this.#getRowFormGroup(rowIndex);
|
|
496
|
+
if (rowFormGroup) {
|
|
497
|
+
const control = rowFormGroup.get(field);
|
|
498
|
+
if (control) {
|
|
499
|
+
control.setValue(value);
|
|
500
|
+
control.markAsDirty();
|
|
501
|
+
control.markAsTouched();
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridFormArray, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
506
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.1", type: GridFormArray, isStandalone: true, selector: "tbw-grid[formArray]", inputs: { formArray: { classPropertyName: "formArray", publicName: "formArray", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
|
|
507
|
+
}
|
|
508
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridFormArray, decorators: [{
|
|
509
|
+
type: Directive,
|
|
510
|
+
args: [{
|
|
511
|
+
selector: 'tbw-grid[formArray]',
|
|
512
|
+
}]
|
|
513
|
+
}], propDecorators: { formArray: [{ type: i0.Input, args: [{ isSignal: true, alias: "formArray", required: true }] }] } });
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Registry to store responsive card templates by grid element.
|
|
517
|
+
* Used by AngularGridAdapter to create card renderers.
|
|
518
|
+
*/
|
|
519
|
+
const responsiveCardTemplateRegistry = new Map();
|
|
520
|
+
/**
|
|
521
|
+
* Retrieves the responsive card template for a grid element.
|
|
522
|
+
*
|
|
523
|
+
* @param gridElement - The grid element to look up
|
|
524
|
+
* @returns The template reference or undefined if not found
|
|
525
|
+
*/
|
|
526
|
+
function getResponsiveCardTemplate(gridElement) {
|
|
527
|
+
// Find the tbw-grid-responsive-card element inside the grid
|
|
528
|
+
const cardElement = gridElement.querySelector('tbw-grid-responsive-card');
|
|
529
|
+
if (!cardElement)
|
|
530
|
+
return undefined;
|
|
531
|
+
return responsiveCardTemplateRegistry.get(cardElement);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Directive for providing custom Angular templates for responsive card layout.
|
|
535
|
+
*
|
|
536
|
+
* Use this directive to define how each row should render when the grid
|
|
537
|
+
* is in responsive/mobile mode. The template receives the row data and index.
|
|
538
|
+
*
|
|
539
|
+
* ## Usage
|
|
540
|
+
*
|
|
541
|
+
* ```html
|
|
542
|
+
* <tbw-grid [rows]="employees">
|
|
543
|
+
* <tbw-grid-responsive-card>
|
|
544
|
+
* <ng-template let-employee let-idx="index">
|
|
545
|
+
* <div class="employee-card">
|
|
546
|
+
* <img [src]="employee.avatar" alt="">
|
|
547
|
+
* <div class="info">
|
|
548
|
+
* <strong>{{ employee.name }}</strong>
|
|
549
|
+
* <span>{{ employee.department }}</span>
|
|
550
|
+
* </div>
|
|
551
|
+
* </div>
|
|
552
|
+
* </ng-template>
|
|
553
|
+
* </tbw-grid-responsive-card>
|
|
554
|
+
* </tbw-grid>
|
|
555
|
+
* ```
|
|
556
|
+
*
|
|
557
|
+
* ## Important Notes
|
|
558
|
+
*
|
|
559
|
+
* - The ResponsivePlugin must be added to your grid config
|
|
560
|
+
* - The Grid directive will automatically configure the plugin's cardRenderer
|
|
561
|
+
* - Template context provides `$implicit` (row), `row`, and `index`
|
|
562
|
+
*
|
|
563
|
+
* @see ResponsivePlugin
|
|
564
|
+
*/
|
|
565
|
+
class GridResponsiveCard {
|
|
566
|
+
elementRef = inject((ElementRef));
|
|
567
|
+
/**
|
|
568
|
+
* The ng-template containing the card content.
|
|
569
|
+
*/
|
|
570
|
+
template = contentChild((TemplateRef), ...(ngDevMode ? [{ debugName: "template" }] : []));
|
|
571
|
+
/**
|
|
572
|
+
* Effect that registers the template when it becomes available.
|
|
573
|
+
*/
|
|
574
|
+
onTemplateReceived = effect(() => {
|
|
575
|
+
const template = this.template();
|
|
576
|
+
if (template) {
|
|
577
|
+
responsiveCardTemplateRegistry.set(this.elementRef.nativeElement, template);
|
|
578
|
+
}
|
|
579
|
+
}, ...(ngDevMode ? [{ debugName: "onTemplateReceived" }] : []));
|
|
580
|
+
/**
|
|
581
|
+
* Type guard for template context inference.
|
|
582
|
+
*/
|
|
583
|
+
static ngTemplateContextGuard(_directive, context) {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridResponsiveCard, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
587
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.1", type: GridResponsiveCard, isStandalone: true, selector: "tbw-grid-responsive-card", queries: [{ propertyName: "template", first: true, predicate: (TemplateRef), descendants: true, isSignal: true }], ngImport: i0 });
|
|
588
|
+
}
|
|
589
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridResponsiveCard, decorators: [{
|
|
590
|
+
type: Directive,
|
|
591
|
+
args: [{
|
|
592
|
+
selector: 'tbw-grid-responsive-card',
|
|
593
|
+
}]
|
|
594
|
+
}], propDecorators: { template: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
|
|
595
|
+
|
|
596
|
+
// Global registry mapping DOM elements to their templates
|
|
597
|
+
const toolPanelTemplateRegistry = new Map();
|
|
598
|
+
/**
|
|
599
|
+
* Gets the tool panel template registered for a given tool panel element.
|
|
600
|
+
* Used by AngularGridAdapter to retrieve templates at render time.
|
|
601
|
+
*/
|
|
602
|
+
function getToolPanelTemplate(panelElement) {
|
|
603
|
+
return toolPanelTemplateRegistry.get(panelElement);
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Gets all tool panel elements with registered templates within a grid element.
|
|
607
|
+
*/
|
|
608
|
+
function getToolPanelElements(gridElement) {
|
|
609
|
+
const panelElements = gridElement.querySelectorAll('tbw-grid-tool-panel');
|
|
610
|
+
return Array.from(panelElements).filter((el) => toolPanelTemplateRegistry.has(el));
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Directive that captures an `<ng-template>` for use as a custom tool panel.
|
|
614
|
+
*
|
|
615
|
+
* This enables declarative Angular component usage for tool panels
|
|
616
|
+
* that appear in the grid's side panel.
|
|
617
|
+
*
|
|
618
|
+
* ## Usage
|
|
619
|
+
*
|
|
620
|
+
* ```html
|
|
621
|
+
* <tbw-grid [rows]="rows" [gridConfig]="config">
|
|
622
|
+
* <tbw-grid-tool-panel
|
|
623
|
+
* id="quick-filters"
|
|
624
|
+
* title="Quick Filters"
|
|
625
|
+
* icon="🔍"
|
|
626
|
+
* tooltip="Apply quick filters"
|
|
627
|
+
* [order]="10"
|
|
628
|
+
* >
|
|
629
|
+
* <ng-template let-grid>
|
|
630
|
+
* <app-quick-filters [grid]="grid" />
|
|
631
|
+
* </ng-template>
|
|
632
|
+
* </tbw-grid-tool-panel>
|
|
633
|
+
* </tbw-grid>
|
|
634
|
+
* ```
|
|
635
|
+
*
|
|
636
|
+
* The template context provides:
|
|
637
|
+
* - `$implicit` / `grid`: The grid element reference
|
|
638
|
+
*
|
|
639
|
+
* ### Attributes
|
|
640
|
+
*
|
|
641
|
+
* - `id` (required): Unique identifier for the panel
|
|
642
|
+
* - `title` (required): Panel title shown in accordion header
|
|
643
|
+
* - `icon`: Icon for accordion section header (emoji or text)
|
|
644
|
+
* - `tooltip`: Tooltip for accordion section header
|
|
645
|
+
* - `order`: Panel order priority (lower = first, default: 100)
|
|
646
|
+
*
|
|
647
|
+
* Import the directive in your component:
|
|
648
|
+
*
|
|
649
|
+
* ```typescript
|
|
650
|
+
* import { GridToolPanel } from '@toolbox-web/grid-angular';
|
|
651
|
+
*
|
|
652
|
+
* @Component({
|
|
653
|
+
* imports: [GridToolPanel],
|
|
654
|
+
* // ...
|
|
655
|
+
* })
|
|
656
|
+
* ```
|
|
657
|
+
*/
|
|
658
|
+
class GridToolPanel {
|
|
659
|
+
elementRef = inject((ElementRef));
|
|
660
|
+
/** Unique panel identifier (required) */
|
|
661
|
+
id = input.required({ ...(ngDevMode ? { debugName: "id" } : {}), alias: 'id' });
|
|
662
|
+
/** Panel title shown in accordion header (required) */
|
|
663
|
+
title = input.required({ ...(ngDevMode ? { debugName: "title" } : {}), alias: 'title' });
|
|
664
|
+
/** Icon for accordion section header (emoji or text) */
|
|
665
|
+
icon = input(...(ngDevMode ? [undefined, { debugName: "icon" }] : []));
|
|
666
|
+
/** Tooltip for accordion section header */
|
|
667
|
+
tooltip = input(...(ngDevMode ? [undefined, { debugName: "tooltip" }] : []));
|
|
668
|
+
/** Panel order priority (lower = first, default: 100) */
|
|
669
|
+
order = input(100, ...(ngDevMode ? [{ debugName: "order" }] : []));
|
|
670
|
+
/**
|
|
671
|
+
* Query for the ng-template content child.
|
|
672
|
+
*/
|
|
673
|
+
template = contentChild((TemplateRef), ...(ngDevMode ? [{ debugName: "template" }] : []));
|
|
674
|
+
/** Effect that triggers when the template is available */
|
|
675
|
+
onTemplateReceived = effect(() => {
|
|
676
|
+
const template = this.template();
|
|
677
|
+
const element = this.elementRef.nativeElement;
|
|
678
|
+
if (template) {
|
|
679
|
+
// Set attributes from inputs (for light DOM parsing to read)
|
|
680
|
+
element.setAttribute('id', this.id());
|
|
681
|
+
element.setAttribute('title', this.title());
|
|
682
|
+
const icon = this.icon();
|
|
683
|
+
if (icon)
|
|
684
|
+
element.setAttribute('icon', icon);
|
|
685
|
+
const tooltip = this.tooltip();
|
|
686
|
+
if (tooltip)
|
|
687
|
+
element.setAttribute('tooltip', tooltip);
|
|
688
|
+
element.setAttribute('order', String(this.order()));
|
|
689
|
+
// Register the template for this element
|
|
690
|
+
toolPanelTemplateRegistry.set(element, template);
|
|
691
|
+
}
|
|
692
|
+
}, ...(ngDevMode ? [{ debugName: "onTemplateReceived" }] : []));
|
|
693
|
+
/**
|
|
694
|
+
* Static type guard for template context.
|
|
695
|
+
* Enables type inference in templates.
|
|
696
|
+
*/
|
|
697
|
+
static ngTemplateContextGuard(dir, ctx) {
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridToolPanel, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
701
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.1", type: GridToolPanel, isStandalone: true, selector: "tbw-grid-tool-panel", inputs: { id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: true, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, order: { classPropertyName: "order", publicName: "order", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "template", first: true, predicate: (TemplateRef), descendants: true, isSignal: true }], ngImport: i0 });
|
|
702
|
+
}
|
|
703
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridToolPanel, decorators: [{
|
|
704
|
+
type: Directive,
|
|
705
|
+
args: [{ selector: 'tbw-grid-tool-panel' }]
|
|
706
|
+
}], propDecorators: { id: [{ type: i0.Input, args: [{ isSignal: true, alias: "id", required: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: true }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], order: [{ type: i0.Input, args: [{ isSignal: true, alias: "order", required: false }] }], template: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TemplateRef), { isSignal: true }] }] } });
|
|
707
|
+
|
|
708
|
+
// Registries for structural directive templates
|
|
709
|
+
const structuralViewRegistry = new Map();
|
|
710
|
+
const structuralEditorRegistry = new Map();
|
|
711
|
+
/**
|
|
712
|
+
* Gets the view template registered by the structural directive for a given column element.
|
|
713
|
+
* Falls back to the non-structural directive registry.
|
|
714
|
+
*/
|
|
715
|
+
function getStructuralViewTemplate(columnElement) {
|
|
716
|
+
// First check structural directive registry
|
|
717
|
+
const template = structuralViewRegistry.get(columnElement);
|
|
718
|
+
if (template)
|
|
719
|
+
return template;
|
|
720
|
+
// Fall back to the nested element registry
|
|
721
|
+
const viewEl = columnElement.querySelector('tbw-grid-column-view');
|
|
722
|
+
if (viewEl) {
|
|
723
|
+
return getViewTemplate(viewEl);
|
|
724
|
+
}
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Gets the editor template registered by the structural directive for a given column element.
|
|
729
|
+
* Falls back to the non-structural directive registry.
|
|
730
|
+
*/
|
|
731
|
+
function getStructuralEditorTemplate(columnElement) {
|
|
732
|
+
// First check structural directive registry
|
|
733
|
+
const template = structuralEditorRegistry.get(columnElement);
|
|
734
|
+
if (template)
|
|
735
|
+
return template;
|
|
736
|
+
// Fall back to the nested element registry
|
|
737
|
+
const editorEl = columnElement.querySelector('tbw-grid-column-editor');
|
|
738
|
+
if (editorEl) {
|
|
739
|
+
return getEditorTemplate(editorEl);
|
|
740
|
+
}
|
|
741
|
+
return undefined;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Structural directive for cell view rendering.
|
|
745
|
+
*
|
|
746
|
+
* This provides a cleaner syntax for defining custom cell renderers without
|
|
747
|
+
* the nested `<tbw-grid-column-view>` and `<ng-template>` boilerplate.
|
|
748
|
+
*
|
|
749
|
+
* ## Usage
|
|
750
|
+
*
|
|
751
|
+
* ```html
|
|
752
|
+
* <!-- Instead of this verbose syntax: -->
|
|
753
|
+
* <tbw-grid-column field="status">
|
|
754
|
+
* <tbw-grid-column-view>
|
|
755
|
+
* <ng-template let-value let-row="row">
|
|
756
|
+
* <app-status-badge [value]="value" />
|
|
757
|
+
* </ng-template>
|
|
758
|
+
* </tbw-grid-column-view>
|
|
759
|
+
* </tbw-grid-column>
|
|
760
|
+
*
|
|
761
|
+
* <!-- Use this cleaner syntax: -->
|
|
762
|
+
* <tbw-grid-column field="status">
|
|
763
|
+
* <app-status-badge *tbwRenderer="let value; row as row" [value]="value" />
|
|
764
|
+
* </tbw-grid-column>
|
|
765
|
+
* ```
|
|
766
|
+
*
|
|
767
|
+
* ## Template Context
|
|
768
|
+
*
|
|
769
|
+
* The structural directive provides the same context as `GridColumnView`:
|
|
770
|
+
* - `$implicit` / `value`: The cell value (use `let value` or `let-value`)
|
|
771
|
+
* - `row`: The full row data object (use `row as row` or `let-row="row"`)
|
|
772
|
+
* - `column`: The column configuration
|
|
773
|
+
*
|
|
774
|
+
* ## Import
|
|
775
|
+
*
|
|
776
|
+
* ```typescript
|
|
777
|
+
* import { TbwRenderer } from '@toolbox-web/grid-angular';
|
|
778
|
+
*
|
|
779
|
+
* @Component({
|
|
780
|
+
* imports: [TbwRenderer],
|
|
781
|
+
* // ...
|
|
782
|
+
* })
|
|
783
|
+
* ```
|
|
784
|
+
*/
|
|
785
|
+
class TbwRenderer {
|
|
786
|
+
template = inject((TemplateRef));
|
|
787
|
+
elementRef = inject((ElementRef));
|
|
788
|
+
columnElement = null;
|
|
789
|
+
constructor() {
|
|
790
|
+
// Angular structural directives wrap the host element in a comment node.
|
|
791
|
+
// We need to find the parent tbw-grid-column element.
|
|
792
|
+
// Since we're injected into the template, we use an effect to register once the DOM is stable.
|
|
793
|
+
effect(() => {
|
|
794
|
+
this.registerTemplate();
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
registerTemplate() {
|
|
798
|
+
// Find the parent tbw-grid-column element
|
|
799
|
+
// The template's host element may not be in the DOM yet, so we traverse from the comment node
|
|
800
|
+
let parent = this.elementRef.nativeElement?.parentElement;
|
|
801
|
+
while (parent && parent.tagName !== 'TBW-GRID-COLUMN') {
|
|
802
|
+
parent = parent.parentElement;
|
|
803
|
+
}
|
|
804
|
+
if (parent) {
|
|
805
|
+
this.columnElement = parent;
|
|
806
|
+
structuralViewRegistry.set(parent, this.template);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
ngOnDestroy() {
|
|
810
|
+
if (this.columnElement) {
|
|
811
|
+
structuralViewRegistry.delete(this.columnElement);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Static type guard for template context.
|
|
816
|
+
* Uses `any` defaults for ergonomic template usage.
|
|
817
|
+
*/
|
|
818
|
+
static ngTemplateContextGuard(dir, ctx) {
|
|
819
|
+
return true;
|
|
820
|
+
}
|
|
821
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: TbwRenderer, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
822
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.1", type: TbwRenderer, isStandalone: true, selector: "[tbwRenderer]", ngImport: i0 });
|
|
823
|
+
}
|
|
824
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: TbwRenderer, decorators: [{
|
|
825
|
+
type: Directive,
|
|
826
|
+
args: [{ selector: '[tbwRenderer]' }]
|
|
827
|
+
}], ctorParameters: () => [] });
|
|
828
|
+
/**
|
|
829
|
+
* Structural directive for cell editor rendering.
|
|
830
|
+
*
|
|
831
|
+
* This provides a cleaner syntax for defining custom cell editors without
|
|
832
|
+
* the nested `<tbw-grid-column-editor>` and `<ng-template>` boilerplate.
|
|
833
|
+
*
|
|
834
|
+
* ## Usage
|
|
835
|
+
*
|
|
836
|
+
* ```html
|
|
837
|
+
* <!-- Instead of this verbose syntax: -->
|
|
838
|
+
* <tbw-grid-column field="status">
|
|
839
|
+
* <tbw-grid-column-editor>
|
|
840
|
+
* <ng-template let-value let-onCommit="onCommit" let-onCancel="onCancel">
|
|
841
|
+
* <app-status-editor [value]="value" (commit)="onCommit($event)" (cancel)="onCancel()" />
|
|
842
|
+
* </ng-template>
|
|
843
|
+
* </tbw-grid-column-editor>
|
|
844
|
+
* </tbw-grid-column>
|
|
845
|
+
*
|
|
846
|
+
* <!-- Use this cleaner syntax (with auto-wiring - no explicit bindings needed!): -->
|
|
847
|
+
* <tbw-grid-column field="status">
|
|
848
|
+
* <app-status-editor *tbwEditor="let value" [value]="value" />
|
|
849
|
+
* </tbw-grid-column>
|
|
850
|
+
* ```
|
|
851
|
+
*
|
|
852
|
+
* ## Template Context
|
|
853
|
+
*
|
|
854
|
+
* The structural directive provides the same context as `GridColumnEditor`:
|
|
855
|
+
* - `$implicit` / `value`: The cell value
|
|
856
|
+
* - `row`: The full row data object
|
|
857
|
+
* - `column`: The column configuration
|
|
858
|
+
* - `onCommit`: Callback function to commit the new value (optional - auto-wired if component emits `commit` event)
|
|
859
|
+
* - `onCancel`: Callback function to cancel editing (optional - auto-wired if component emits `cancel` event)
|
|
860
|
+
*
|
|
861
|
+
* ## Import
|
|
862
|
+
*
|
|
863
|
+
* ```typescript
|
|
864
|
+
* import { TbwEditor } from '@toolbox-web/grid-angular';
|
|
865
|
+
*
|
|
866
|
+
* @Component({
|
|
867
|
+
* imports: [TbwEditor],
|
|
868
|
+
* // ...
|
|
869
|
+
* })
|
|
870
|
+
* ```
|
|
871
|
+
*/
|
|
872
|
+
class TbwEditor {
|
|
873
|
+
template = inject((TemplateRef));
|
|
874
|
+
elementRef = inject((ElementRef));
|
|
875
|
+
columnElement = null;
|
|
876
|
+
constructor() {
|
|
877
|
+
effect(() => {
|
|
878
|
+
this.registerTemplate();
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
registerTemplate() {
|
|
882
|
+
// Find the parent tbw-grid-column element
|
|
883
|
+
let parent = this.elementRef.nativeElement?.parentElement;
|
|
884
|
+
while (parent && parent.tagName !== 'TBW-GRID-COLUMN') {
|
|
885
|
+
parent = parent.parentElement;
|
|
886
|
+
}
|
|
887
|
+
if (parent) {
|
|
888
|
+
this.columnElement = parent;
|
|
889
|
+
structuralEditorRegistry.set(parent, this.template);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
ngOnDestroy() {
|
|
893
|
+
if (this.columnElement) {
|
|
894
|
+
structuralEditorRegistry.delete(this.columnElement);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Static type guard for template context.
|
|
899
|
+
* Uses `any` defaults for ergonomic template usage.
|
|
900
|
+
*/
|
|
901
|
+
static ngTemplateContextGuard(dir, ctx) {
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: TbwEditor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
905
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.1", type: TbwEditor, isStandalone: true, selector: "[tbwEditor]", ngImport: i0 });
|
|
906
|
+
}
|
|
907
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: TbwEditor, decorators: [{
|
|
908
|
+
type: Directive,
|
|
909
|
+
args: [{ selector: '[tbwEditor]' }]
|
|
910
|
+
}], ctorParameters: () => [] });
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Type-level default registry for Angular applications.
|
|
914
|
+
*
|
|
915
|
+
* Provides application-wide type defaults for renderers and editors
|
|
916
|
+
* that all grids inherit automatically.
|
|
917
|
+
*/
|
|
918
|
+
/**
|
|
919
|
+
* Injection token for providing type defaults at app level.
|
|
920
|
+
*/
|
|
921
|
+
const GRID_TYPE_DEFAULTS = new InjectionToken('GRID_TYPE_DEFAULTS');
|
|
922
|
+
/**
|
|
923
|
+
* Injectable service for managing type-level defaults.
|
|
924
|
+
*
|
|
925
|
+
* Use `provideGridTypeDefaults()` in your app config to set up defaults,
|
|
926
|
+
* or inject this service for dynamic registration.
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* ```typescript
|
|
930
|
+
* // App-level setup (app.config.ts)
|
|
931
|
+
* export const appConfig: ApplicationConfig = {
|
|
932
|
+
* providers: [
|
|
933
|
+
* provideGridTypeDefaults({
|
|
934
|
+
* country: {
|
|
935
|
+
* renderer: CountryCellComponent,
|
|
936
|
+
* editor: CountryEditorComponent
|
|
937
|
+
* },
|
|
938
|
+
* status: {
|
|
939
|
+
* renderer: StatusBadgeComponent
|
|
940
|
+
* }
|
|
941
|
+
* })
|
|
942
|
+
* ]
|
|
943
|
+
* };
|
|
944
|
+
*
|
|
945
|
+
* // Dynamic registration
|
|
946
|
+
* @Component({ ... })
|
|
947
|
+
* export class AppComponent {
|
|
948
|
+
* private registry = inject(GridTypeRegistry);
|
|
949
|
+
*
|
|
950
|
+
* ngOnInit() {
|
|
951
|
+
* this.registry.register('currency', {
|
|
952
|
+
* renderer: CurrencyCellComponent
|
|
953
|
+
* });
|
|
954
|
+
* }
|
|
955
|
+
* }
|
|
956
|
+
* ```
|
|
957
|
+
*/
|
|
958
|
+
class GridTypeRegistry {
|
|
959
|
+
defaults = new Map();
|
|
960
|
+
constructor() {
|
|
961
|
+
// Merge any initial defaults from provider
|
|
962
|
+
const initial = inject(GRID_TYPE_DEFAULTS, { optional: true });
|
|
963
|
+
if (initial) {
|
|
964
|
+
for (const [type, config] of Object.entries(initial)) {
|
|
965
|
+
this.defaults.set(type, config);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Register type-level defaults for a custom type.
|
|
971
|
+
*
|
|
972
|
+
* @param type - The type name (e.g., 'country', 'currency')
|
|
973
|
+
* @param defaults - Renderer/editor configuration
|
|
974
|
+
*/
|
|
975
|
+
register(type, defaults) {
|
|
976
|
+
this.defaults.set(type, defaults);
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Get type defaults for a given type.
|
|
980
|
+
*/
|
|
981
|
+
get(type) {
|
|
982
|
+
return this.defaults.get(type);
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Remove type defaults for a type.
|
|
986
|
+
*/
|
|
987
|
+
unregister(type) {
|
|
988
|
+
this.defaults.delete(type);
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Check if a type has registered defaults.
|
|
992
|
+
*/
|
|
993
|
+
has(type) {
|
|
994
|
+
return this.defaults.has(type);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Get all registered type names.
|
|
998
|
+
*/
|
|
999
|
+
getRegisteredTypes() {
|
|
1000
|
+
return Array.from(this.defaults.keys());
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Convert to TypeDefault for use with grid's typeDefaults.
|
|
1004
|
+
* This is used internally by the adapter.
|
|
1005
|
+
*
|
|
1006
|
+
* @internal
|
|
1007
|
+
*/
|
|
1008
|
+
getAsTypeDefault(type) {
|
|
1009
|
+
const config = this.defaults.get(type);
|
|
1010
|
+
if (!config)
|
|
1011
|
+
return undefined;
|
|
1012
|
+
// Note: The actual renderer/editor functions are created by the adapter
|
|
1013
|
+
// when it calls getTypeDefault() - we just return the config here
|
|
1014
|
+
return {
|
|
1015
|
+
editorParams: config.editorParams,
|
|
1016
|
+
// renderer and editor are handled by the adapter which creates
|
|
1017
|
+
// the actual functions that instantiate Angular components
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridTypeRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1021
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridTypeRegistry, providedIn: 'root' });
|
|
1022
|
+
}
|
|
1023
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridTypeRegistry, decorators: [{
|
|
1024
|
+
type: Injectable,
|
|
1025
|
+
args: [{ providedIn: 'root' }]
|
|
1026
|
+
}], ctorParameters: () => [] });
|
|
1027
|
+
/**
|
|
1028
|
+
* Provides application-level type defaults for all grids.
|
|
1029
|
+
*
|
|
1030
|
+
* @example
|
|
1031
|
+
* ```typescript
|
|
1032
|
+
* // app.config.ts
|
|
1033
|
+
* import { provideGridTypeDefaults } from '@toolbox-web/grid-angular';
|
|
1034
|
+
* import { CountryCellComponent, StatusBadgeComponent } from './components';
|
|
1035
|
+
*
|
|
1036
|
+
* export const appConfig: ApplicationConfig = {
|
|
1037
|
+
* providers: [
|
|
1038
|
+
* provideGridTypeDefaults({
|
|
1039
|
+
* country: { renderer: CountryCellComponent },
|
|
1040
|
+
* status: { renderer: StatusBadgeComponent },
|
|
1041
|
+
* date: { editor: DatePickerComponent }
|
|
1042
|
+
* })
|
|
1043
|
+
* ]
|
|
1044
|
+
* };
|
|
1045
|
+
* ```
|
|
1046
|
+
*/
|
|
1047
|
+
function provideGridTypeDefaults(defaults) {
|
|
1048
|
+
return makeEnvironmentProviders([{ provide: GRID_TYPE_DEFAULTS, useValue: defaults }]);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* Helper to get view template from either structural directive or nested directive.
|
|
1053
|
+
*/
|
|
1054
|
+
function getAnyViewTemplate(element) {
|
|
1055
|
+
// First check structural directive registry (for *tbwRenderer syntax)
|
|
1056
|
+
const structuralTemplate = getStructuralViewTemplate(element);
|
|
1057
|
+
if (structuralTemplate)
|
|
1058
|
+
return structuralTemplate;
|
|
1059
|
+
// Fall back to nested directive (for <tbw-grid-column-view> syntax)
|
|
1060
|
+
return getViewTemplate(element);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Helper to get editor template from either structural directive or nested directive.
|
|
1064
|
+
*/
|
|
1065
|
+
function getAnyEditorTemplate(element) {
|
|
1066
|
+
// First check structural directive registry (for *tbwEditor syntax)
|
|
1067
|
+
// The structural context uses `any` types for better ergonomics, but is compatible with GridEditorContext
|
|
1068
|
+
const structuralTemplate = getStructuralEditorTemplate(element);
|
|
1069
|
+
if (structuralTemplate)
|
|
1070
|
+
return structuralTemplate;
|
|
1071
|
+
// Fall back to nested directive (for <tbw-grid-column-editor> syntax)
|
|
1072
|
+
return getEditorTemplate(element);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Framework adapter that enables zero-boilerplate integration of Angular components
|
|
1076
|
+
* with the grid's light DOM configuration API.
|
|
1077
|
+
*
|
|
1078
|
+
* ## Usage
|
|
1079
|
+
*
|
|
1080
|
+
* **One-time setup in your app:**
|
|
1081
|
+
* ```typescript
|
|
1082
|
+
* import { Component, inject, EnvironmentInjector, ApplicationRef, ViewContainerRef } from '@angular/core';
|
|
1083
|
+
* import { GridElement } from '@toolbox-web/grid';
|
|
1084
|
+
* import { AngularGridAdapter } from '@toolbox-web/grid-angular';
|
|
1085
|
+
*
|
|
1086
|
+
* @Component({
|
|
1087
|
+
* selector: 'app-root',
|
|
1088
|
+
* // ...
|
|
1089
|
+
* })
|
|
1090
|
+
* export class AppComponent {
|
|
1091
|
+
* constructor() {
|
|
1092
|
+
* const injector = inject(EnvironmentInjector);
|
|
1093
|
+
* const appRef = inject(ApplicationRef);
|
|
1094
|
+
* const viewContainerRef = inject(ViewContainerRef);
|
|
1095
|
+
* GridElement.registerAdapter(new AngularGridAdapter(injector, appRef, viewContainerRef));
|
|
1096
|
+
* }
|
|
1097
|
+
* }
|
|
1098
|
+
* ```
|
|
1099
|
+
*
|
|
1100
|
+
* **Declarative configuration in templates (structural directive - recommended):**
|
|
1101
|
+
* ```html
|
|
1102
|
+
* <tbw-grid>
|
|
1103
|
+
* <tbw-grid-column field="status">
|
|
1104
|
+
* <app-status-badge *tbwRenderer="let value; row as row" [value]="value" />
|
|
1105
|
+
* <app-status-editor *tbwEditor="let value" [value]="value" />
|
|
1106
|
+
* </tbw-grid-column>
|
|
1107
|
+
* </tbw-grid>
|
|
1108
|
+
* ```
|
|
1109
|
+
*
|
|
1110
|
+
* **Declarative configuration in templates (nested directive - legacy):**
|
|
1111
|
+
* ```html
|
|
1112
|
+
* <tbw-grid>
|
|
1113
|
+
* <tbw-grid-column field="status">
|
|
1114
|
+
* <tbw-grid-column-view>
|
|
1115
|
+
* <ng-template let-value let-row="row">
|
|
1116
|
+
* <app-status-badge [value]="value" [row]="row" />
|
|
1117
|
+
* </ng-template>
|
|
1118
|
+
* </tbw-grid-column-view>
|
|
1119
|
+
* <tbw-grid-column-editor>
|
|
1120
|
+
* <ng-template let-value let-onCommit="onCommit" let-onCancel="onCancel">
|
|
1121
|
+
* <app-status-select [value]="value" (commit)="onCommit($event)" (cancel)="onCancel()" />
|
|
1122
|
+
* </ng-template>
|
|
1123
|
+
* </tbw-grid-column-editor>
|
|
1124
|
+
* </tbw-grid-column>
|
|
1125
|
+
* </tbw-grid>
|
|
1126
|
+
* ```
|
|
1127
|
+
*
|
|
1128
|
+
* The adapter automatically:
|
|
1129
|
+
* - Detects Angular templates registered by directives (both structural and nested)
|
|
1130
|
+
* - Creates embedded views with cell context (value, row, column)
|
|
1131
|
+
* - Handles editor callbacks (onCommit/onCancel)
|
|
1132
|
+
* - Manages view lifecycle and change detection
|
|
1133
|
+
*/
|
|
1134
|
+
class AngularGridAdapter {
|
|
1135
|
+
injector;
|
|
1136
|
+
appRef;
|
|
1137
|
+
viewContainerRef;
|
|
1138
|
+
viewRefs = [];
|
|
1139
|
+
componentRefs = [];
|
|
1140
|
+
typeRegistry = null;
|
|
1141
|
+
constructor(injector, appRef, viewContainerRef) {
|
|
1142
|
+
this.injector = injector;
|
|
1143
|
+
this.appRef = appRef;
|
|
1144
|
+
this.viewContainerRef = viewContainerRef;
|
|
1145
|
+
// Register globally for directive access
|
|
1146
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1147
|
+
window.__ANGULAR_GRID_ADAPTER__ = this;
|
|
1148
|
+
// Try to get the type registry from the injector
|
|
1149
|
+
try {
|
|
1150
|
+
this.typeRegistry = this.injector.get(GridTypeRegistry, null);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
// GridTypeRegistry not available - type defaults won't be resolved
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Processes an Angular grid configuration, converting component class references
|
|
1158
|
+
* to actual renderer/editor functions.
|
|
1159
|
+
*
|
|
1160
|
+
* Call this method on your gridConfig before passing it to the grid.
|
|
1161
|
+
*
|
|
1162
|
+
* @example
|
|
1163
|
+
* ```typescript
|
|
1164
|
+
* import { AngularGridAdapter, type AngularGridConfig } from '@toolbox-web/grid-angular';
|
|
1165
|
+
*
|
|
1166
|
+
* const config: AngularGridConfig<Employee> = {
|
|
1167
|
+
* columns: [
|
|
1168
|
+
* { field: 'status', renderer: StatusBadgeComponent, editor: StatusEditorComponent },
|
|
1169
|
+
* ],
|
|
1170
|
+
* };
|
|
1171
|
+
*
|
|
1172
|
+
* // In component
|
|
1173
|
+
* constructor() {
|
|
1174
|
+
* const adapter = inject(AngularGridAdapter); // or create new instance
|
|
1175
|
+
* this.processedConfig = adapter.processGridConfig(config);
|
|
1176
|
+
* }
|
|
1177
|
+
* ```
|
|
1178
|
+
*
|
|
1179
|
+
* @param config - Angular grid configuration with possible component class references
|
|
1180
|
+
* @returns Processed GridConfig with actual renderer/editor functions
|
|
1181
|
+
*/
|
|
1182
|
+
processGridConfig(config) {
|
|
1183
|
+
if (!config.columns) {
|
|
1184
|
+
return config;
|
|
1185
|
+
}
|
|
1186
|
+
const processedColumns = config.columns.map((col) => this.processColumn(col));
|
|
1187
|
+
return {
|
|
1188
|
+
...config,
|
|
1189
|
+
columns: processedColumns,
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Processes a single column configuration, converting component class references
|
|
1194
|
+
* to actual renderer/editor functions.
|
|
1195
|
+
*
|
|
1196
|
+
* @param column - Angular column configuration
|
|
1197
|
+
* @returns Processed ColumnConfig
|
|
1198
|
+
*/
|
|
1199
|
+
processColumn(column) {
|
|
1200
|
+
const processed = { ...column };
|
|
1201
|
+
// Convert renderer component class to function
|
|
1202
|
+
if (column.renderer && isComponentClass(column.renderer)) {
|
|
1203
|
+
processed.renderer = this.createComponentRenderer(column.renderer);
|
|
1204
|
+
}
|
|
1205
|
+
// Convert editor component class to function
|
|
1206
|
+
if (column.editor && isComponentClass(column.editor)) {
|
|
1207
|
+
processed.editor = this.createComponentEditor(column.editor);
|
|
1208
|
+
}
|
|
1209
|
+
return processed;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Determines if this adapter can handle the given element.
|
|
1213
|
+
* Checks if a template is registered for this element (structural or nested).
|
|
1214
|
+
*/
|
|
1215
|
+
canHandle(element) {
|
|
1216
|
+
return getAnyViewTemplate(element) !== undefined || getAnyEditorTemplate(element) !== undefined;
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Creates a view renderer function that creates an embedded view
|
|
1220
|
+
* from the registered template and returns its DOM element.
|
|
1221
|
+
*
|
|
1222
|
+
* Returns undefined if no template is registered for this element,
|
|
1223
|
+
* allowing the grid to use its default rendering.
|
|
1224
|
+
*/
|
|
1225
|
+
createRenderer(element) {
|
|
1226
|
+
const template = getAnyViewTemplate(element);
|
|
1227
|
+
if (!template) {
|
|
1228
|
+
// Return undefined so the grid uses default rendering
|
|
1229
|
+
// This is important when only an editor template is provided (no view template)
|
|
1230
|
+
return undefined;
|
|
1231
|
+
}
|
|
1232
|
+
return (ctx) => {
|
|
1233
|
+
// Create the context for the template
|
|
1234
|
+
const context = {
|
|
1235
|
+
$implicit: ctx.value,
|
|
1236
|
+
value: ctx.value,
|
|
1237
|
+
row: ctx.row,
|
|
1238
|
+
column: ctx.column,
|
|
1239
|
+
};
|
|
1240
|
+
// Create embedded view from template
|
|
1241
|
+
const viewRef = this.viewContainerRef.createEmbeddedView(template, context);
|
|
1242
|
+
this.viewRefs.push(viewRef);
|
|
1243
|
+
// Trigger change detection
|
|
1244
|
+
viewRef.detectChanges();
|
|
1245
|
+
// Get the first root node (the component's host element)
|
|
1246
|
+
const rootNode = viewRef.rootNodes[0];
|
|
1247
|
+
return rootNode;
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Creates an editor spec that creates an embedded view.
|
|
1252
|
+
*
|
|
1253
|
+
* **Auto-wiring**: The adapter automatically listens for `commit` and `cancel`
|
|
1254
|
+
* CustomEvents on the rendered component. If the component emits these events,
|
|
1255
|
+
* the adapter will call the grid's commit/cancel functions automatically.
|
|
1256
|
+
*
|
|
1257
|
+
* This means templates can be simplified from:
|
|
1258
|
+
* ```html
|
|
1259
|
+
* <app-editor *tbwEditor="let value; onCommit as onCommit"
|
|
1260
|
+
* [value]="value" (commit)="onCommit($event)" />
|
|
1261
|
+
* ```
|
|
1262
|
+
* To just:
|
|
1263
|
+
* ```html
|
|
1264
|
+
* <app-editor *tbwEditor="let value" [value]="value" />
|
|
1265
|
+
* ```
|
|
1266
|
+
* As long as the component emits `(commit)` with the new value.
|
|
1267
|
+
*/
|
|
1268
|
+
createEditor(element) {
|
|
1269
|
+
const template = getAnyEditorTemplate(element);
|
|
1270
|
+
// Find the parent grid element for FormArray context access
|
|
1271
|
+
const gridElement = element.closest('tbw-grid');
|
|
1272
|
+
if (!template) {
|
|
1273
|
+
// No warning - this can happen during early initialization before Angular
|
|
1274
|
+
// registers structural directive templates. The grid will re-parse columns
|
|
1275
|
+
// after templates are registered.
|
|
1276
|
+
return () => document.createElement('div');
|
|
1277
|
+
}
|
|
1278
|
+
return (ctx) => {
|
|
1279
|
+
// Create simple callback functions (preferred)
|
|
1280
|
+
const onCommit = (value) => ctx.commit(value);
|
|
1281
|
+
const onCancel = () => ctx.cancel();
|
|
1282
|
+
// Create EventEmitters for backwards compatibility (deprecated)
|
|
1283
|
+
const commitEmitter = new EventEmitter();
|
|
1284
|
+
const cancelEmitter = new EventEmitter();
|
|
1285
|
+
commitEmitter.subscribe((value) => ctx.commit(value));
|
|
1286
|
+
cancelEmitter.subscribe(() => ctx.cancel());
|
|
1287
|
+
// Try to get the FormControl from the FormArrayContext
|
|
1288
|
+
let control;
|
|
1289
|
+
if (gridElement) {
|
|
1290
|
+
const formContext = getFormArrayContext(gridElement);
|
|
1291
|
+
if (formContext?.hasFormGroups) {
|
|
1292
|
+
// Find the row index by looking up ctx.row in the grid's rows
|
|
1293
|
+
const gridRows = gridElement.rows;
|
|
1294
|
+
if (gridRows) {
|
|
1295
|
+
const rowIndex = gridRows.indexOf(ctx.row);
|
|
1296
|
+
if (rowIndex >= 0) {
|
|
1297
|
+
control = formContext.getControl(rowIndex, ctx.field);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
// Create the context for the template
|
|
1303
|
+
const context = {
|
|
1304
|
+
$implicit: ctx.value,
|
|
1305
|
+
value: ctx.value,
|
|
1306
|
+
row: ctx.row,
|
|
1307
|
+
column: ctx.column,
|
|
1308
|
+
// Preferred: simple callback functions
|
|
1309
|
+
onCommit,
|
|
1310
|
+
onCancel,
|
|
1311
|
+
// FormControl from FormArray (if available)
|
|
1312
|
+
control,
|
|
1313
|
+
// Deprecated: EventEmitters (for backwards compatibility)
|
|
1314
|
+
commit: commitEmitter,
|
|
1315
|
+
cancel: cancelEmitter,
|
|
1316
|
+
};
|
|
1317
|
+
// Create embedded view from template
|
|
1318
|
+
const viewRef = this.viewContainerRef.createEmbeddedView(template, context);
|
|
1319
|
+
this.viewRefs.push(viewRef);
|
|
1320
|
+
// Trigger change detection
|
|
1321
|
+
viewRef.detectChanges();
|
|
1322
|
+
// Get the first root node (the component's host element)
|
|
1323
|
+
const rootNode = viewRef.rootNodes[0];
|
|
1324
|
+
// Auto-wire: Listen for commit/cancel events on the rendered component.
|
|
1325
|
+
// This allows components to just emit (commit) and (cancel) without
|
|
1326
|
+
// requiring explicit template bindings like (commit)="onCommit($event)".
|
|
1327
|
+
if (rootNode && rootNode.addEventListener) {
|
|
1328
|
+
rootNode.addEventListener('commit', (e) => {
|
|
1329
|
+
const customEvent = e;
|
|
1330
|
+
ctx.commit(customEvent.detail);
|
|
1331
|
+
});
|
|
1332
|
+
rootNode.addEventListener('cancel', () => {
|
|
1333
|
+
ctx.cancel();
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
return rootNode;
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Creates a detail renderer function for MasterDetailPlugin.
|
|
1341
|
+
* Renders Angular templates for expandable detail rows.
|
|
1342
|
+
*/
|
|
1343
|
+
createDetailRenderer(gridElement) {
|
|
1344
|
+
const template = getDetailTemplate(gridElement);
|
|
1345
|
+
if (!template) {
|
|
1346
|
+
return undefined;
|
|
1347
|
+
}
|
|
1348
|
+
return (row) => {
|
|
1349
|
+
// Create the context for the template
|
|
1350
|
+
const context = {
|
|
1351
|
+
$implicit: row,
|
|
1352
|
+
row: row,
|
|
1353
|
+
};
|
|
1354
|
+
// Create embedded view from template
|
|
1355
|
+
const viewRef = this.viewContainerRef.createEmbeddedView(template, context);
|
|
1356
|
+
this.viewRefs.push(viewRef);
|
|
1357
|
+
// Trigger change detection
|
|
1358
|
+
viewRef.detectChanges();
|
|
1359
|
+
// Create a container for the root nodes
|
|
1360
|
+
const container = document.createElement('div');
|
|
1361
|
+
viewRef.rootNodes.forEach((node) => container.appendChild(node));
|
|
1362
|
+
return container;
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Framework adapter hook called by MasterDetailPlugin during attach().
|
|
1367
|
+
* Parses the <tbw-grid-detail> element and returns an Angular template-based renderer.
|
|
1368
|
+
*
|
|
1369
|
+
* This enables MasterDetailPlugin to automatically use Angular templates
|
|
1370
|
+
* without manual configuration in the Grid directive.
|
|
1371
|
+
*/
|
|
1372
|
+
parseDetailElement(detailElement) {
|
|
1373
|
+
// Get the template from the registry for this detail element
|
|
1374
|
+
const template = getDetailTemplate(detailElement.closest('tbw-grid'));
|
|
1375
|
+
if (!template) {
|
|
1376
|
+
return undefined;
|
|
1377
|
+
}
|
|
1378
|
+
// Return a renderer function that creates embedded views
|
|
1379
|
+
// Note: rowIndex is part of the MasterDetailPlugin detailRenderer signature but not needed here
|
|
1380
|
+
return (row) => {
|
|
1381
|
+
const context = {
|
|
1382
|
+
$implicit: row,
|
|
1383
|
+
row: row,
|
|
1384
|
+
};
|
|
1385
|
+
const viewRef = this.viewContainerRef.createEmbeddedView(template, context);
|
|
1386
|
+
this.viewRefs.push(viewRef);
|
|
1387
|
+
viewRef.detectChanges();
|
|
1388
|
+
const container = document.createElement('div');
|
|
1389
|
+
viewRef.rootNodes.forEach((node) => container.appendChild(node));
|
|
1390
|
+
return container;
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Creates a responsive card renderer function for ResponsivePlugin.
|
|
1395
|
+
* Renders Angular templates for card layout in responsive mode.
|
|
1396
|
+
*
|
|
1397
|
+
* @param gridElement - The grid element to look up the template for
|
|
1398
|
+
* @returns A card renderer function or undefined if no template is found
|
|
1399
|
+
*/
|
|
1400
|
+
createResponsiveCardRenderer(gridElement) {
|
|
1401
|
+
const template = getResponsiveCardTemplate(gridElement);
|
|
1402
|
+
if (!template) {
|
|
1403
|
+
return undefined;
|
|
1404
|
+
}
|
|
1405
|
+
return (row, rowIndex) => {
|
|
1406
|
+
// Create the context for the template
|
|
1407
|
+
const context = {
|
|
1408
|
+
$implicit: row,
|
|
1409
|
+
row: row,
|
|
1410
|
+
index: rowIndex,
|
|
1411
|
+
};
|
|
1412
|
+
// Create embedded view from template
|
|
1413
|
+
const viewRef = this.viewContainerRef.createEmbeddedView(template, context);
|
|
1414
|
+
this.viewRefs.push(viewRef);
|
|
1415
|
+
// Trigger change detection
|
|
1416
|
+
viewRef.detectChanges();
|
|
1417
|
+
// Create a container for the root nodes
|
|
1418
|
+
const container = document.createElement('div');
|
|
1419
|
+
viewRef.rootNodes.forEach((node) => container.appendChild(node));
|
|
1420
|
+
return container;
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Creates a tool panel renderer from a light DOM element.
|
|
1425
|
+
* The renderer creates an Angular template-based panel content.
|
|
1426
|
+
*/
|
|
1427
|
+
createToolPanelRenderer(element) {
|
|
1428
|
+
const template = getToolPanelTemplate(element);
|
|
1429
|
+
if (!template) {
|
|
1430
|
+
return undefined;
|
|
1431
|
+
}
|
|
1432
|
+
// Find the parent grid element for context
|
|
1433
|
+
const gridElement = element.closest('tbw-grid');
|
|
1434
|
+
return (container) => {
|
|
1435
|
+
// Create the context for the template
|
|
1436
|
+
const context = {
|
|
1437
|
+
$implicit: gridElement ?? container,
|
|
1438
|
+
grid: gridElement ?? container,
|
|
1439
|
+
};
|
|
1440
|
+
// Create embedded view from template
|
|
1441
|
+
const viewRef = this.viewContainerRef.createEmbeddedView(template, context);
|
|
1442
|
+
this.viewRefs.push(viewRef);
|
|
1443
|
+
// Trigger change detection
|
|
1444
|
+
viewRef.detectChanges();
|
|
1445
|
+
// Append all root nodes to the container
|
|
1446
|
+
viewRef.rootNodes.forEach((node) => container.appendChild(node));
|
|
1447
|
+
// Return cleanup function
|
|
1448
|
+
return () => {
|
|
1449
|
+
const index = this.viewRefs.indexOf(viewRef);
|
|
1450
|
+
if (index > -1) {
|
|
1451
|
+
this.viewRefs.splice(index, 1);
|
|
1452
|
+
}
|
|
1453
|
+
viewRef.destroy();
|
|
1454
|
+
};
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
/**
|
|
1458
|
+
* Gets type-level defaults from the application's GridTypeRegistry.
|
|
1459
|
+
*
|
|
1460
|
+
* This enables application-wide type defaults configured via `provideGridTypeDefaults()`.
|
|
1461
|
+
* The returned TypeDefault contains renderer/editor functions that instantiate
|
|
1462
|
+
* Angular components dynamically.
|
|
1463
|
+
*
|
|
1464
|
+
* @example
|
|
1465
|
+
* ```typescript
|
|
1466
|
+
* // app.config.ts
|
|
1467
|
+
* export const appConfig: ApplicationConfig = {
|
|
1468
|
+
* providers: [
|
|
1469
|
+
* provideGridTypeDefaults({
|
|
1470
|
+
* country: {
|
|
1471
|
+
* renderer: CountryCellComponent,
|
|
1472
|
+
* editor: CountryEditorComponent
|
|
1473
|
+
* }
|
|
1474
|
+
* })
|
|
1475
|
+
* ]
|
|
1476
|
+
* };
|
|
1477
|
+
*
|
|
1478
|
+
* // Any grid with type: 'country' columns will use these components
|
|
1479
|
+
* gridConfig = {
|
|
1480
|
+
* columns: [{ field: 'country', type: 'country' }]
|
|
1481
|
+
* };
|
|
1482
|
+
* ```
|
|
1483
|
+
*/
|
|
1484
|
+
getTypeDefault(type) {
|
|
1485
|
+
if (!this.typeRegistry) {
|
|
1486
|
+
return undefined;
|
|
1487
|
+
}
|
|
1488
|
+
const config = this.typeRegistry.get(type);
|
|
1489
|
+
if (!config) {
|
|
1490
|
+
return undefined;
|
|
1491
|
+
}
|
|
1492
|
+
const typeDefault = {
|
|
1493
|
+
editorParams: config.editorParams,
|
|
1494
|
+
};
|
|
1495
|
+
// Create renderer function that instantiates the Angular component
|
|
1496
|
+
if (config.renderer) {
|
|
1497
|
+
typeDefault.renderer = this.createComponentRenderer(config.renderer);
|
|
1498
|
+
}
|
|
1499
|
+
// Create editor function that instantiates the Angular component
|
|
1500
|
+
if (config.editor) {
|
|
1501
|
+
// Type assertion needed: adapter bridges TRow to core's unknown
|
|
1502
|
+
typeDefault.editor = this.createComponentEditor(config.editor);
|
|
1503
|
+
}
|
|
1504
|
+
return typeDefault;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Creates a renderer function from an Angular component class.
|
|
1508
|
+
* @internal
|
|
1509
|
+
*/
|
|
1510
|
+
createComponentRenderer(componentClass) {
|
|
1511
|
+
return (ctx) => {
|
|
1512
|
+
// Create a host element for the component
|
|
1513
|
+
const hostElement = document.createElement('span');
|
|
1514
|
+
hostElement.style.display = 'contents';
|
|
1515
|
+
// Create the component dynamically
|
|
1516
|
+
const componentRef = createComponent(componentClass, {
|
|
1517
|
+
environmentInjector: this.injector,
|
|
1518
|
+
hostElement,
|
|
1519
|
+
});
|
|
1520
|
+
// Set inputs - components should have value, row, column inputs
|
|
1521
|
+
this.setComponentInputs(componentRef, {
|
|
1522
|
+
value: ctx.value,
|
|
1523
|
+
row: ctx.row,
|
|
1524
|
+
column: ctx.column,
|
|
1525
|
+
});
|
|
1526
|
+
// Attach to app for change detection
|
|
1527
|
+
this.appRef.attachView(componentRef.hostView);
|
|
1528
|
+
this.componentRefs.push(componentRef);
|
|
1529
|
+
// Trigger change detection
|
|
1530
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
1531
|
+
return hostElement;
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* Creates an editor function from an Angular component class.
|
|
1536
|
+
* @internal
|
|
1537
|
+
*/
|
|
1538
|
+
createComponentEditor(componentClass) {
|
|
1539
|
+
return (ctx) => {
|
|
1540
|
+
// Create a host element for the component
|
|
1541
|
+
const hostElement = document.createElement('span');
|
|
1542
|
+
hostElement.style.display = 'contents';
|
|
1543
|
+
// Create the component dynamically
|
|
1544
|
+
const componentRef = createComponent(componentClass, {
|
|
1545
|
+
environmentInjector: this.injector,
|
|
1546
|
+
hostElement,
|
|
1547
|
+
});
|
|
1548
|
+
// Set inputs - components should have value, row, column inputs
|
|
1549
|
+
this.setComponentInputs(componentRef, {
|
|
1550
|
+
value: ctx.value,
|
|
1551
|
+
row: ctx.row,
|
|
1552
|
+
column: ctx.column,
|
|
1553
|
+
});
|
|
1554
|
+
// Attach to app for change detection
|
|
1555
|
+
this.appRef.attachView(componentRef.hostView);
|
|
1556
|
+
this.componentRefs.push(componentRef);
|
|
1557
|
+
// Trigger change detection
|
|
1558
|
+
componentRef.changeDetectorRef.detectChanges();
|
|
1559
|
+
// Subscribe to Angular outputs (commit/cancel) on the component instance.
|
|
1560
|
+
// This works with Angular's output() signal API.
|
|
1561
|
+
const instance = componentRef.instance;
|
|
1562
|
+
this.subscribeToOutput(instance, 'commit', (value) => ctx.commit(value));
|
|
1563
|
+
this.subscribeToOutput(instance, 'cancel', () => ctx.cancel());
|
|
1564
|
+
// Also listen for DOM events as fallback (for components that dispatch CustomEvents)
|
|
1565
|
+
hostElement.addEventListener('commit', (e) => {
|
|
1566
|
+
const customEvent = e;
|
|
1567
|
+
ctx.commit(customEvent.detail);
|
|
1568
|
+
});
|
|
1569
|
+
hostElement.addEventListener('cancel', () => {
|
|
1570
|
+
ctx.cancel();
|
|
1571
|
+
});
|
|
1572
|
+
return hostElement;
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Subscribes to an Angular output on a component instance.
|
|
1577
|
+
* Works with both EventEmitter and OutputEmitterRef (signal outputs).
|
|
1578
|
+
* @internal
|
|
1579
|
+
*/
|
|
1580
|
+
subscribeToOutput(instance, outputName, callback) {
|
|
1581
|
+
const output = instance[outputName];
|
|
1582
|
+
if (!output)
|
|
1583
|
+
return;
|
|
1584
|
+
// Check if it's an Observable-like (EventEmitter or OutputEmitterRef)
|
|
1585
|
+
if (typeof output.subscribe === 'function') {
|
|
1586
|
+
output.subscribe(callback);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Sets component inputs using Angular's setInput API.
|
|
1591
|
+
* @internal
|
|
1592
|
+
*/
|
|
1593
|
+
setComponentInputs(componentRef, inputs) {
|
|
1594
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
1595
|
+
try {
|
|
1596
|
+
componentRef.setInput(key, value);
|
|
1597
|
+
}
|
|
1598
|
+
catch {
|
|
1599
|
+
// Input doesn't exist on component - that's okay, some inputs are optional
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Clean up all view references and component references.
|
|
1605
|
+
* Call this when your app/component is destroyed.
|
|
1606
|
+
*/
|
|
1607
|
+
destroy() {
|
|
1608
|
+
this.viewRefs.forEach((ref) => ref.destroy());
|
|
1609
|
+
this.viewRefs = [];
|
|
1610
|
+
this.componentRefs.forEach((ref) => ref.destroy());
|
|
1611
|
+
this.componentRefs = [];
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/**
|
|
1616
|
+
* Icon configuration registry for Angular applications.
|
|
1617
|
+
*
|
|
1618
|
+
* Provides application-wide icon overrides for all grids via
|
|
1619
|
+
* Angular's dependency injection.
|
|
1620
|
+
*/
|
|
1621
|
+
/**
|
|
1622
|
+
* Injection token for providing icon overrides at app level.
|
|
1623
|
+
*/
|
|
1624
|
+
const GRID_ICONS = new InjectionToken('GRID_ICONS');
|
|
1625
|
+
/**
|
|
1626
|
+
* Injectable service for managing grid icons.
|
|
1627
|
+
*
|
|
1628
|
+
* Use `provideGridIcons()` in your app config to set up icons,
|
|
1629
|
+
* or inject this service for dynamic registration.
|
|
1630
|
+
*
|
|
1631
|
+
* @example
|
|
1632
|
+
* ```typescript
|
|
1633
|
+
* // App-level setup (app.config.ts)
|
|
1634
|
+
* export const appConfig: ApplicationConfig = {
|
|
1635
|
+
* providers: [
|
|
1636
|
+
* provideGridIcons({
|
|
1637
|
+
* expand: '➕',
|
|
1638
|
+
* collapse: '➖',
|
|
1639
|
+
* sortAsc: '↑',
|
|
1640
|
+
* sortDesc: '↓',
|
|
1641
|
+
* })
|
|
1642
|
+
* ]
|
|
1643
|
+
* };
|
|
1644
|
+
*
|
|
1645
|
+
* // Dynamic registration
|
|
1646
|
+
* @Component({ ... })
|
|
1647
|
+
* export class AppComponent {
|
|
1648
|
+
* private registry = inject(GridIconRegistry);
|
|
1649
|
+
*
|
|
1650
|
+
* ngOnInit() {
|
|
1651
|
+
* this.registry.set('filter', '<svg>...</svg>');
|
|
1652
|
+
* }
|
|
1653
|
+
* }
|
|
1654
|
+
* ```
|
|
1655
|
+
*/
|
|
1656
|
+
class GridIconRegistry {
|
|
1657
|
+
icons = new Map();
|
|
1658
|
+
constructor() {
|
|
1659
|
+
// Merge any initial icons from provider
|
|
1660
|
+
const initial = inject(GRID_ICONS, { optional: true });
|
|
1661
|
+
if (initial) {
|
|
1662
|
+
for (const [key, value] of Object.entries(initial)) {
|
|
1663
|
+
this.icons.set(key, value);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Set an icon override.
|
|
1669
|
+
*
|
|
1670
|
+
* @param name - The icon name (e.g., 'expand', 'collapse', 'filter')
|
|
1671
|
+
* @param value - The icon value (string text or SVG markup)
|
|
1672
|
+
*/
|
|
1673
|
+
set(name, value) {
|
|
1674
|
+
this.icons.set(name, value);
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Get an icon value.
|
|
1678
|
+
*/
|
|
1679
|
+
get(name) {
|
|
1680
|
+
return this.icons.get(name);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Remove an icon override.
|
|
1684
|
+
*/
|
|
1685
|
+
remove(name) {
|
|
1686
|
+
this.icons.delete(name);
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Check if an icon has an override.
|
|
1690
|
+
*/
|
|
1691
|
+
has(name) {
|
|
1692
|
+
return this.icons.has(name);
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Get all icon overrides as a GridIcons partial.
|
|
1696
|
+
* Used internally by the adapter.
|
|
1697
|
+
*
|
|
1698
|
+
* @internal
|
|
1699
|
+
*/
|
|
1700
|
+
getAll() {
|
|
1701
|
+
const result = {};
|
|
1702
|
+
for (const [key, value] of this.icons) {
|
|
1703
|
+
result[key] = value;
|
|
1704
|
+
}
|
|
1705
|
+
return result;
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Get all registered icon names.
|
|
1709
|
+
*/
|
|
1710
|
+
getRegisteredIcons() {
|
|
1711
|
+
return Array.from(this.icons.keys());
|
|
1712
|
+
}
|
|
1713
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridIconRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1714
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridIconRegistry, providedIn: 'root' });
|
|
1715
|
+
}
|
|
1716
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: GridIconRegistry, decorators: [{
|
|
1717
|
+
type: Injectable,
|
|
1718
|
+
args: [{ providedIn: 'root' }]
|
|
1719
|
+
}], ctorParameters: () => [] });
|
|
1720
|
+
/**
|
|
1721
|
+
* Provides application-level icon overrides for all grids.
|
|
1722
|
+
*
|
|
1723
|
+
* Available icons to override:
|
|
1724
|
+
* - `expand` - Expand icon for collapsed items (trees, groups, details)
|
|
1725
|
+
* - `collapse` - Collapse icon for expanded items
|
|
1726
|
+
* - `sortAsc` - Sort ascending indicator
|
|
1727
|
+
* - `sortDesc` - Sort descending indicator
|
|
1728
|
+
* - `sortNone` - Sort neutral/unsorted indicator
|
|
1729
|
+
* - `submenuArrow` - Submenu arrow for context menus
|
|
1730
|
+
* - `dragHandle` - Drag handle icon for reordering
|
|
1731
|
+
* - `toolPanel` - Tool panel toggle icon in toolbar
|
|
1732
|
+
* - `filter` - Filter icon in column headers
|
|
1733
|
+
* - `filterActive` - Filter icon when filter is active
|
|
1734
|
+
* - `print` - Print icon for print button
|
|
1735
|
+
*
|
|
1736
|
+
* @example
|
|
1737
|
+
* ```typescript
|
|
1738
|
+
* // app.config.ts
|
|
1739
|
+
* import { provideGridIcons } from '@toolbox-web/grid-angular';
|
|
1740
|
+
*
|
|
1741
|
+
* export const appConfig: ApplicationConfig = {
|
|
1742
|
+
* providers: [
|
|
1743
|
+
* provideGridIcons({
|
|
1744
|
+
* expand: '➕',
|
|
1745
|
+
* collapse: '➖',
|
|
1746
|
+
* sortAsc: '↑',
|
|
1747
|
+
* sortDesc: '↓',
|
|
1748
|
+
* filter: '<svg viewBox="0 0 16 16">...</svg>',
|
|
1749
|
+
* })
|
|
1750
|
+
* ]
|
|
1751
|
+
* };
|
|
1752
|
+
* ```
|
|
1753
|
+
*/
|
|
1754
|
+
function provideGridIcons(icons) {
|
|
1755
|
+
return makeEnvironmentProviders([{ provide: GRID_ICONS, useValue: icons }]);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
/**
|
|
1759
|
+
* Angular inject function for programmatic access to a grid instance.
|
|
1760
|
+
*
|
|
1761
|
+
* This function should be called in the constructor or as a field initializer
|
|
1762
|
+
* of an Angular component that contains a `<tbw-grid>` element.
|
|
1763
|
+
*
|
|
1764
|
+
* ## Usage
|
|
1765
|
+
*
|
|
1766
|
+
* ```typescript
|
|
1767
|
+
* import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
1768
|
+
* import { Grid, injectGrid } from '@toolbox-web/grid-angular';
|
|
1769
|
+
*
|
|
1770
|
+
* @Component({
|
|
1771
|
+
* selector: 'app-my-grid',
|
|
1772
|
+
* imports: [Grid],
|
|
1773
|
+
* schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
1774
|
+
* template: `
|
|
1775
|
+
* <button (click)="handleResize()">Force Layout</button>
|
|
1776
|
+
* <button (click)="handleExport()" [disabled]="!grid.isReady()">Export</button>
|
|
1777
|
+
* <tbw-grid [rows]="rows" [gridConfig]="config"></tbw-grid>
|
|
1778
|
+
* `
|
|
1779
|
+
* })
|
|
1780
|
+
* export class MyGridComponent {
|
|
1781
|
+
* grid = injectGrid<Employee>();
|
|
1782
|
+
*
|
|
1783
|
+
* async handleResize() {
|
|
1784
|
+
* await this.grid.forceLayout();
|
|
1785
|
+
* }
|
|
1786
|
+
*
|
|
1787
|
+
* async handleExport() {
|
|
1788
|
+
* const config = await this.grid.getConfig();
|
|
1789
|
+
* console.log('Exporting with columns:', config?.columns);
|
|
1790
|
+
* }
|
|
1791
|
+
* }
|
|
1792
|
+
* ```
|
|
1793
|
+
*
|
|
1794
|
+
* @returns Object with grid access methods and state signals
|
|
1795
|
+
*/
|
|
1796
|
+
function injectGrid() {
|
|
1797
|
+
const elementRef = inject(ElementRef);
|
|
1798
|
+
// Reactive signals
|
|
1799
|
+
const isReady = signal(false, ...(ngDevMode ? [{ debugName: "isReady" }] : []));
|
|
1800
|
+
const config = signal(null, ...(ngDevMode ? [{ debugName: "config" }] : []));
|
|
1801
|
+
const element = signal(null, ...(ngDevMode ? [{ debugName: "element" }] : []));
|
|
1802
|
+
// Initialize after render
|
|
1803
|
+
afterNextRender(() => {
|
|
1804
|
+
const gridElement = elementRef.nativeElement.querySelector('tbw-grid');
|
|
1805
|
+
if (!gridElement) {
|
|
1806
|
+
console.warn('[injectGrid] No tbw-grid element found in component');
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
element.set(gridElement);
|
|
1810
|
+
// Wait for grid to be ready
|
|
1811
|
+
gridElement.ready?.().then(async () => {
|
|
1812
|
+
isReady.set(true);
|
|
1813
|
+
const effectiveConfig = gridElement.getConfig?.();
|
|
1814
|
+
if (effectiveConfig) {
|
|
1815
|
+
config.set(effectiveConfig);
|
|
1816
|
+
}
|
|
1817
|
+
});
|
|
1818
|
+
});
|
|
1819
|
+
// Computed visible columns
|
|
1820
|
+
const visibleColumns = computed(() => {
|
|
1821
|
+
const currentConfig = config();
|
|
1822
|
+
if (!currentConfig?.columns)
|
|
1823
|
+
return [];
|
|
1824
|
+
return currentConfig.columns.filter((col) => !col.hidden);
|
|
1825
|
+
}, ...(ngDevMode ? [{ debugName: "visibleColumns" }] : []));
|
|
1826
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1827
|
+
// CORE METHODS
|
|
1828
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1829
|
+
const getConfig = async () => {
|
|
1830
|
+
const gridElement = element();
|
|
1831
|
+
if (!gridElement)
|
|
1832
|
+
return null;
|
|
1833
|
+
const effectiveConfig = gridElement.getConfig?.();
|
|
1834
|
+
return effectiveConfig ?? null;
|
|
1835
|
+
};
|
|
1836
|
+
const forceLayout = async () => {
|
|
1837
|
+
const gridElement = element();
|
|
1838
|
+
if (!gridElement)
|
|
1839
|
+
return;
|
|
1840
|
+
await gridElement.forceLayout?.();
|
|
1841
|
+
};
|
|
1842
|
+
const toggleGroup = async (key) => {
|
|
1843
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1844
|
+
const gridElement = element();
|
|
1845
|
+
if (!gridElement)
|
|
1846
|
+
return;
|
|
1847
|
+
await gridElement.toggleGroup?.(key);
|
|
1848
|
+
};
|
|
1849
|
+
const registerStyles = (id, css) => {
|
|
1850
|
+
element()?.registerStyles?.(id, css);
|
|
1851
|
+
};
|
|
1852
|
+
const unregisterStyles = (id) => {
|
|
1853
|
+
element()?.unregisterStyles?.(id);
|
|
1854
|
+
};
|
|
1855
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1856
|
+
// SELECTION CONVENIENCE METHODS
|
|
1857
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1858
|
+
const selectAll = () => {
|
|
1859
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1860
|
+
const gridElement = element();
|
|
1861
|
+
const plugin = gridElement?.getPluginByName?.('selection');
|
|
1862
|
+
if (!plugin) {
|
|
1863
|
+
console.warn('[injectGrid] selectAll requires SelectionPlugin');
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
// Row mode: select all row indices
|
|
1867
|
+
if (plugin.config?.mode === 'row') {
|
|
1868
|
+
const rows = gridElement?.rows ?? [];
|
|
1869
|
+
const allIndices = new Set(rows.map((_, i) => i));
|
|
1870
|
+
plugin.selected = allIndices;
|
|
1871
|
+
plugin.requestAfterRender?.();
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
const clearSelection = () => {
|
|
1875
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1876
|
+
const gridElement = element();
|
|
1877
|
+
const plugin = gridElement?.getPluginByName?.('selection');
|
|
1878
|
+
if (!plugin)
|
|
1879
|
+
return;
|
|
1880
|
+
const mode = plugin.config?.mode;
|
|
1881
|
+
if (mode === 'row') {
|
|
1882
|
+
plugin.selected = new Set();
|
|
1883
|
+
}
|
|
1884
|
+
else if (mode === 'range' || mode === 'cell') {
|
|
1885
|
+
plugin.ranges = [];
|
|
1886
|
+
}
|
|
1887
|
+
plugin.requestAfterRender?.();
|
|
1888
|
+
};
|
|
1889
|
+
const getSelectedIndices = () => {
|
|
1890
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1891
|
+
const gridElement = element();
|
|
1892
|
+
const plugin = gridElement?.getPluginByName?.('selection');
|
|
1893
|
+
if (!plugin)
|
|
1894
|
+
return new Set();
|
|
1895
|
+
if (plugin.config?.mode === 'row') {
|
|
1896
|
+
return new Set(plugin.selected ?? []);
|
|
1897
|
+
}
|
|
1898
|
+
// Range/cell mode: extract unique row indices from ranges
|
|
1899
|
+
const ranges = plugin.ranges ?? [];
|
|
1900
|
+
const indices = new Set();
|
|
1901
|
+
for (const range of ranges) {
|
|
1902
|
+
for (let r = range.startRow; r <= range.endRow; r++) {
|
|
1903
|
+
indices.add(r);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return indices;
|
|
1907
|
+
};
|
|
1908
|
+
const getSelectedRows = () => {
|
|
1909
|
+
const gridElement = element();
|
|
1910
|
+
if (!gridElement)
|
|
1911
|
+
return [];
|
|
1912
|
+
const rows = gridElement.rows ?? [];
|
|
1913
|
+
const indices = getSelectedIndices();
|
|
1914
|
+
return Array.from(indices)
|
|
1915
|
+
.filter((i) => i >= 0 && i < rows.length)
|
|
1916
|
+
.map((i) => rows[i]);
|
|
1917
|
+
};
|
|
1918
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1919
|
+
// EXPORT CONVENIENCE METHODS
|
|
1920
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1921
|
+
const exportToCsv = (filename) => {
|
|
1922
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1923
|
+
const gridElement = element();
|
|
1924
|
+
const plugin = gridElement?.getPluginByName?.('export');
|
|
1925
|
+
if (!plugin) {
|
|
1926
|
+
console.warn('[injectGrid] exportToCsv requires ExportPlugin');
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
plugin.exportToCsv?.(filename);
|
|
1930
|
+
};
|
|
1931
|
+
const exportToJson = (filename) => {
|
|
1932
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1933
|
+
const gridElement = element();
|
|
1934
|
+
const plugin = gridElement?.getPluginByName?.('export');
|
|
1935
|
+
if (!plugin) {
|
|
1936
|
+
console.warn('[injectGrid] exportToJson requires ExportPlugin');
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
plugin.exportToJson?.(filename);
|
|
1940
|
+
};
|
|
1941
|
+
return {
|
|
1942
|
+
element,
|
|
1943
|
+
isReady,
|
|
1944
|
+
config,
|
|
1945
|
+
visibleColumns,
|
|
1946
|
+
getConfig,
|
|
1947
|
+
forceLayout,
|
|
1948
|
+
toggleGroup,
|
|
1949
|
+
registerStyles,
|
|
1950
|
+
unregisterStyles,
|
|
1951
|
+
selectAll,
|
|
1952
|
+
clearSelection,
|
|
1953
|
+
getSelectedIndices,
|
|
1954
|
+
getSelectedRows,
|
|
1955
|
+
exportToCsv,
|
|
1956
|
+
exportToJson,
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/**
|
|
1961
|
+
* Feature Registry for @toolbox-web/grid-angular
|
|
1962
|
+
*
|
|
1963
|
+
* This module provides a synchronous registry for plugin factories.
|
|
1964
|
+
* Features are registered via side-effect imports, enabling tree-shaking
|
|
1965
|
+
* while maintaining the clean input-based API.
|
|
1966
|
+
*
|
|
1967
|
+
* @example
|
|
1968
|
+
* ```typescript
|
|
1969
|
+
* // Import features you need (side-effect imports)
|
|
1970
|
+
* import '@toolbox-web/grid-angular/features/selection';
|
|
1971
|
+
* import '@toolbox-web/grid-angular/features/filtering';
|
|
1972
|
+
*
|
|
1973
|
+
* // Inputs work automatically - no async loading, no HTTP requests
|
|
1974
|
+
* <tbw-grid [selection]="'range'" [filtering]="{ debounceMs: 200 }" />
|
|
1975
|
+
* ```
|
|
1976
|
+
*/
|
|
1977
|
+
/**
|
|
1978
|
+
* Central registry mapping feature names to their plugin factories.
|
|
1979
|
+
* Populated by side-effect feature imports.
|
|
1980
|
+
*/
|
|
1981
|
+
const featureRegistry = new Map();
|
|
1982
|
+
/**
|
|
1983
|
+
* Set of features that have been used without being registered.
|
|
1984
|
+
* Used to show helpful warnings only once per feature.
|
|
1985
|
+
*/
|
|
1986
|
+
const warnedFeatures = new Set();
|
|
1987
|
+
/**
|
|
1988
|
+
* Register a feature's plugin factory.
|
|
1989
|
+
* Called by side-effect feature imports.
|
|
1990
|
+
*
|
|
1991
|
+
* @param name - The feature name (matches the input name on Grid directive)
|
|
1992
|
+
* @param factory - Function that creates the plugin instance
|
|
1993
|
+
*
|
|
1994
|
+
* @example
|
|
1995
|
+
* ```ts
|
|
1996
|
+
* // features/selection.ts
|
|
1997
|
+
* import { SelectionPlugin } from '@toolbox-web/grid/plugins/selection';
|
|
1998
|
+
* import { registerFeature } from '../lib/feature-registry';
|
|
1999
|
+
*
|
|
2000
|
+
* registerFeature('selection', (config) => new SelectionPlugin(config));
|
|
2001
|
+
* ```
|
|
2002
|
+
*/
|
|
2003
|
+
function registerFeature(name, factory) {
|
|
2004
|
+
featureRegistry.set(name, {
|
|
2005
|
+
factory: factory,
|
|
2006
|
+
name,
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Check if a feature is registered.
|
|
2011
|
+
*/
|
|
2012
|
+
function isFeatureRegistered(name) {
|
|
2013
|
+
return featureRegistry.has(name);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Get a registered feature's factory.
|
|
2017
|
+
* Returns undefined if not registered.
|
|
2018
|
+
*/
|
|
2019
|
+
function getFeatureFactory(name) {
|
|
2020
|
+
return featureRegistry.get(name)?.factory;
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Get all registered feature names.
|
|
2024
|
+
* Useful for debugging.
|
|
2025
|
+
*/
|
|
2026
|
+
function getRegisteredFeatures() {
|
|
2027
|
+
return Array.from(featureRegistry.keys());
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* Create a plugin instance for a feature.
|
|
2031
|
+
* Shows a helpful warning if the feature is not registered.
|
|
2032
|
+
*
|
|
2033
|
+
* @param name - Feature name
|
|
2034
|
+
* @param config - Plugin configuration
|
|
2035
|
+
* @returns Plugin instance or undefined if not registered
|
|
2036
|
+
*/
|
|
2037
|
+
function createPluginFromFeature(name, config) {
|
|
2038
|
+
const entry = featureRegistry.get(name);
|
|
2039
|
+
if (!entry) {
|
|
2040
|
+
// Show warning only once per feature in development
|
|
2041
|
+
const isDev = typeof window !== 'undefined' &&
|
|
2042
|
+
(window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
|
|
2043
|
+
if (!warnedFeatures.has(name) && isDev) {
|
|
2044
|
+
warnedFeatures.add(name);
|
|
2045
|
+
console.warn(`[tbw-grid] Feature "${name}" input is set but the feature is not registered.\n` +
|
|
2046
|
+
`Add this import to enable it:\n\n` +
|
|
2047
|
+
` import '@toolbox-web/grid-angular/features/${toKebabCase(name)}';\n`);
|
|
2048
|
+
}
|
|
2049
|
+
return undefined;
|
|
2050
|
+
}
|
|
2051
|
+
return entry.factory(config);
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Convert camelCase to kebab-case for import paths.
|
|
2055
|
+
*/
|
|
2056
|
+
function toKebabCase(str) {
|
|
2057
|
+
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Clear the registry. For testing only.
|
|
2061
|
+
* @internal
|
|
2062
|
+
*/
|
|
2063
|
+
function clearFeatureRegistry() {
|
|
2064
|
+
featureRegistry.clear();
|
|
2065
|
+
warnedFeatures.clear();
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
/**
|
|
2069
|
+
* Base class for grid cell editors.
|
|
2070
|
+
*
|
|
2071
|
+
* Provides common functionality for Angular cell editors:
|
|
2072
|
+
* - Automatic value resolution from FormControl or value input
|
|
2073
|
+
* - Common inputs (value, row, column, control)
|
|
2074
|
+
* - Common outputs (commit, cancel)
|
|
2075
|
+
* - Validation state helpers
|
|
2076
|
+
*
|
|
2077
|
+
* ## Usage
|
|
2078
|
+
*
|
|
2079
|
+
* ```typescript
|
|
2080
|
+
* import { Component } from '@angular/core';
|
|
2081
|
+
* import { BaseGridEditor } from '@toolbox-web/grid-angular';
|
|
2082
|
+
*
|
|
2083
|
+
* @Component({
|
|
2084
|
+
* selector: 'app-my-editor',
|
|
2085
|
+
* template: \`
|
|
2086
|
+
* <input
|
|
2087
|
+
* [value]="currentValue()"
|
|
2088
|
+
* [class.is-invalid]="isInvalid()"
|
|
2089
|
+
* (input)="commitValue($event.target.value)"
|
|
2090
|
+
* (keydown.escape)="cancelEdit()"
|
|
2091
|
+
* />
|
|
2092
|
+
* @if (hasErrors()) {
|
|
2093
|
+
* <div class="error">{{ firstErrorMessage() }}</div>
|
|
2094
|
+
* }
|
|
2095
|
+
* \`
|
|
2096
|
+
* })
|
|
2097
|
+
* export class MyEditorComponent extends BaseGridEditor<MyRow, string> {
|
|
2098
|
+
* // Override to customize error messages
|
|
2099
|
+
* protected override getErrorMessage(errorKey: string): string {
|
|
2100
|
+
* if (errorKey === 'required') return 'This field is required';
|
|
2101
|
+
* if (errorKey === 'minlength') return 'Too short';
|
|
2102
|
+
* return super.getErrorMessage(errorKey);
|
|
2103
|
+
* }
|
|
2104
|
+
* }
|
|
2105
|
+
* ```
|
|
2106
|
+
*
|
|
2107
|
+
* ## Template Syntax
|
|
2108
|
+
*
|
|
2109
|
+
* When using the base class, you only need to pass the control:
|
|
2110
|
+
*
|
|
2111
|
+
* ```html
|
|
2112
|
+
* <tbw-grid-column field="name">
|
|
2113
|
+
* <app-my-editor *tbwEditor="let _; control as control" [control]="control" />
|
|
2114
|
+
* </tbw-grid-column>
|
|
2115
|
+
* ```
|
|
2116
|
+
*
|
|
2117
|
+
* Or without FormArray binding (fallback to value):
|
|
2118
|
+
*
|
|
2119
|
+
* ```html
|
|
2120
|
+
* <tbw-grid-column field="name">
|
|
2121
|
+
* <app-my-editor *tbwEditor="let value" [value]="value" />
|
|
2122
|
+
* </tbw-grid-column>
|
|
2123
|
+
* ```
|
|
2124
|
+
*
|
|
2125
|
+
* @typeParam TRow - The row data type
|
|
2126
|
+
* @typeParam TValue - The cell value type
|
|
2127
|
+
*/
|
|
2128
|
+
class BaseGridEditor {
|
|
2129
|
+
elementRef = inject(ElementRef);
|
|
2130
|
+
// ============================================================================
|
|
2131
|
+
// Inputs
|
|
2132
|
+
// ============================================================================
|
|
2133
|
+
/**
|
|
2134
|
+
* The cell value. Used when FormControl is not available.
|
|
2135
|
+
* When a FormControl is provided, value is derived from control.value instead.
|
|
2136
|
+
*/
|
|
2137
|
+
value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : []));
|
|
2138
|
+
/**
|
|
2139
|
+
* The full row data object.
|
|
2140
|
+
*/
|
|
2141
|
+
row = input(...(ngDevMode ? [undefined, { debugName: "row" }] : []));
|
|
2142
|
+
/**
|
|
2143
|
+
* The column configuration.
|
|
2144
|
+
*/
|
|
2145
|
+
column = input(...(ngDevMode ? [undefined, { debugName: "column" }] : []));
|
|
2146
|
+
/**
|
|
2147
|
+
* The FormControl for this cell, if the grid is bound to a FormArray.
|
|
2148
|
+
* When provided, the editor uses control.value instead of the value input.
|
|
2149
|
+
*/
|
|
2150
|
+
control = input(...(ngDevMode ? [undefined, { debugName: "control" }] : []));
|
|
2151
|
+
// ============================================================================
|
|
2152
|
+
// Outputs
|
|
2153
|
+
// ============================================================================
|
|
2154
|
+
/**
|
|
2155
|
+
* Emits when the user commits a new value.
|
|
2156
|
+
*/
|
|
2157
|
+
commit = output();
|
|
2158
|
+
/**
|
|
2159
|
+
* Emits when the user cancels editing.
|
|
2160
|
+
*/
|
|
2161
|
+
cancel = output();
|
|
2162
|
+
// ============================================================================
|
|
2163
|
+
// Computed State
|
|
2164
|
+
// ============================================================================
|
|
2165
|
+
/**
|
|
2166
|
+
* The current value, derived from FormControl if available, otherwise from value input.
|
|
2167
|
+
* This is the recommended way to get the current value in your editor template.
|
|
2168
|
+
*/
|
|
2169
|
+
currentValue = computed(() => {
|
|
2170
|
+
const ctrl = this.control();
|
|
2171
|
+
if (ctrl) {
|
|
2172
|
+
return ctrl.value;
|
|
2173
|
+
}
|
|
2174
|
+
return this.value();
|
|
2175
|
+
}, ...(ngDevMode ? [{ debugName: "currentValue" }] : []));
|
|
2176
|
+
/**
|
|
2177
|
+
* Whether the control is invalid (has validation errors).
|
|
2178
|
+
* Returns false if no FormControl is available.
|
|
2179
|
+
*/
|
|
2180
|
+
isInvalid = computed(() => {
|
|
2181
|
+
return this.control()?.invalid ?? false;
|
|
2182
|
+
}, ...(ngDevMode ? [{ debugName: "isInvalid" }] : []));
|
|
2183
|
+
/**
|
|
2184
|
+
* Whether the control is dirty (has been modified).
|
|
2185
|
+
* Returns false if no FormControl is available.
|
|
2186
|
+
*/
|
|
2187
|
+
isDirty = computed(() => {
|
|
2188
|
+
return this.control()?.dirty ?? false;
|
|
2189
|
+
}, ...(ngDevMode ? [{ debugName: "isDirty" }] : []));
|
|
2190
|
+
/**
|
|
2191
|
+
* Whether the control has been touched.
|
|
2192
|
+
* Returns false if no FormControl is available.
|
|
2193
|
+
*/
|
|
2194
|
+
isTouched = computed(() => {
|
|
2195
|
+
return this.control()?.touched ?? false;
|
|
2196
|
+
}, ...(ngDevMode ? [{ debugName: "isTouched" }] : []));
|
|
2197
|
+
/**
|
|
2198
|
+
* Whether the control has any validation errors.
|
|
2199
|
+
*/
|
|
2200
|
+
hasErrors = computed(() => {
|
|
2201
|
+
const ctrl = this.control();
|
|
2202
|
+
return ctrl?.errors != null && Object.keys(ctrl.errors).length > 0;
|
|
2203
|
+
}, ...(ngDevMode ? [{ debugName: "hasErrors" }] : []));
|
|
2204
|
+
/**
|
|
2205
|
+
* The first error message from the control's validation errors.
|
|
2206
|
+
* Returns an empty string if no errors.
|
|
2207
|
+
*/
|
|
2208
|
+
firstErrorMessage = computed(() => {
|
|
2209
|
+
const ctrl = this.control();
|
|
2210
|
+
if (!ctrl?.errors)
|
|
2211
|
+
return '';
|
|
2212
|
+
const firstKey = Object.keys(ctrl.errors)[0];
|
|
2213
|
+
return this.getErrorMessage(firstKey, ctrl.errors[firstKey]);
|
|
2214
|
+
}, ...(ngDevMode ? [{ debugName: "firstErrorMessage" }] : []));
|
|
2215
|
+
/**
|
|
2216
|
+
* All error messages from the control's validation errors.
|
|
2217
|
+
*/
|
|
2218
|
+
allErrorMessages = computed(() => {
|
|
2219
|
+
const ctrl = this.control();
|
|
2220
|
+
if (!ctrl?.errors)
|
|
2221
|
+
return [];
|
|
2222
|
+
return Object.entries(ctrl.errors).map(([key, value]) => this.getErrorMessage(key, value));
|
|
2223
|
+
}, ...(ngDevMode ? [{ debugName: "allErrorMessages" }] : []));
|
|
2224
|
+
// ============================================================================
|
|
2225
|
+
// Methods
|
|
2226
|
+
// ============================================================================
|
|
2227
|
+
/**
|
|
2228
|
+
* Commit a new value. Emits the commit output AND dispatches a DOM event.
|
|
2229
|
+
* The DOM event enables the grid's auto-wiring to catch the commit.
|
|
2230
|
+
* Call this when the user confirms their edit.
|
|
2231
|
+
*/
|
|
2232
|
+
commitValue(newValue) {
|
|
2233
|
+
// Emit Angular output for template bindings
|
|
2234
|
+
this.commit.emit(newValue);
|
|
2235
|
+
// Dispatch DOM CustomEvent for grid's auto-wiring
|
|
2236
|
+
// This allows the adapter to catch commits without explicit (commit)="..." bindings
|
|
2237
|
+
this.elementRef.nativeElement.dispatchEvent(new CustomEvent('commit', { detail: newValue, bubbles: true }));
|
|
2238
|
+
}
|
|
2239
|
+
/**
|
|
2240
|
+
* Cancel editing. Emits the cancel output AND dispatches a DOM event.
|
|
2241
|
+
* Call this when the user cancels (e.g., presses Escape).
|
|
2242
|
+
*/
|
|
2243
|
+
cancelEdit() {
|
|
2244
|
+
// Emit Angular output for template bindings
|
|
2245
|
+
this.cancel.emit();
|
|
2246
|
+
// Dispatch DOM CustomEvent for grid's auto-wiring
|
|
2247
|
+
this.elementRef.nativeElement.dispatchEvent(new CustomEvent('cancel', { bubbles: true }));
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Get a human-readable error message for a validation error.
|
|
2251
|
+
* Override this method to customize error messages for your editor.
|
|
2252
|
+
*
|
|
2253
|
+
* @param errorKey - The validation error key (e.g., 'required', 'minlength')
|
|
2254
|
+
* @param errorValue - The error value (e.g., { requiredLength: 5, actualLength: 3 })
|
|
2255
|
+
* @returns A human-readable error message
|
|
2256
|
+
*/
|
|
2257
|
+
getErrorMessage(errorKey, errorValue) {
|
|
2258
|
+
switch (errorKey) {
|
|
2259
|
+
case 'required':
|
|
2260
|
+
return 'This field is required';
|
|
2261
|
+
case 'minlength': {
|
|
2262
|
+
const err = errorValue;
|
|
2263
|
+
return `Minimum length is ${err?.requiredLength ?? 'unknown'}`;
|
|
2264
|
+
}
|
|
2265
|
+
case 'maxlength': {
|
|
2266
|
+
const err = errorValue;
|
|
2267
|
+
return `Maximum length is ${err?.requiredLength ?? 'unknown'}`;
|
|
2268
|
+
}
|
|
2269
|
+
case 'min': {
|
|
2270
|
+
const err = errorValue;
|
|
2271
|
+
return `Minimum value is ${err?.min ?? 'unknown'}`;
|
|
2272
|
+
}
|
|
2273
|
+
case 'max': {
|
|
2274
|
+
const err = errorValue;
|
|
2275
|
+
return `Maximum value is ${err?.max ?? 'unknown'}`;
|
|
2276
|
+
}
|
|
2277
|
+
case 'email':
|
|
2278
|
+
return 'Invalid email address';
|
|
2279
|
+
case 'pattern':
|
|
2280
|
+
return 'Invalid format';
|
|
2281
|
+
default:
|
|
2282
|
+
return `Invalid value (${errorKey})`;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: BaseGridEditor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
2286
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.1", type: BaseGridEditor, isStandalone: true, inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: false, transformFunction: null }, column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: false, transformFunction: null }, control: { classPropertyName: "control", publicName: "control", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { commit: "commit", cancel: "cancel" }, ngImport: i0 });
|
|
2287
|
+
}
|
|
2288
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: BaseGridEditor, decorators: [{
|
|
2289
|
+
type: Directive
|
|
2290
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: false }] }], column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: false }] }], control: [{ type: i0.Input, args: [{ isSignal: true, alias: "control", required: false }] }], commit: [{ type: i0.Output, args: ["commit"] }], cancel: [{ type: i0.Output, args: ["cancel"] }] } });
|
|
2291
|
+
|
|
2292
|
+
/**
|
|
2293
|
+
* Directive that automatically registers the Angular adapter with tbw-grid elements.
|
|
2294
|
+
*
|
|
2295
|
+
* This directive eliminates the need to manually register the adapter in your component
|
|
2296
|
+
* constructor. Simply import this directive and it will handle adapter registration.
|
|
2297
|
+
*
|
|
2298
|
+
* ## Usage
|
|
2299
|
+
*
|
|
2300
|
+
* ```typescript
|
|
2301
|
+
* import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
2302
|
+
* import { Grid } from '@toolbox-web/grid-angular';
|
|
2303
|
+
*
|
|
2304
|
+
* @Component({
|
|
2305
|
+
* selector: 'app-root',
|
|
2306
|
+
* imports: [Grid],
|
|
2307
|
+
* schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
2308
|
+
* template: `
|
|
2309
|
+
* <tbw-grid [rows]="rows" [gridConfig]="config" [customStyles]="myStyles">
|
|
2310
|
+
* <!-- column templates -->
|
|
2311
|
+
* </tbw-grid>
|
|
2312
|
+
* `
|
|
2313
|
+
* })
|
|
2314
|
+
* export class AppComponent {
|
|
2315
|
+
* rows = [...];
|
|
2316
|
+
* config = {...};
|
|
2317
|
+
* myStyles = `.my-class { color: red; }`;
|
|
2318
|
+
* }
|
|
2319
|
+
* ```
|
|
2320
|
+
*
|
|
2321
|
+
* The directive automatically:
|
|
2322
|
+
* - Creates an AngularGridAdapter instance
|
|
2323
|
+
* - Registers it with the GridElement
|
|
2324
|
+
* - Injects custom styles into the grid
|
|
2325
|
+
* - Handles cleanup on destruction
|
|
2326
|
+
*/
|
|
2327
|
+
class Grid {
|
|
2328
|
+
elementRef = inject((ElementRef));
|
|
2329
|
+
injector = inject(EnvironmentInjector);
|
|
2330
|
+
appRef = inject(ApplicationRef);
|
|
2331
|
+
viewContainerRef = inject(ViewContainerRef);
|
|
2332
|
+
iconRegistry = inject(GridIconRegistry, { optional: true });
|
|
2333
|
+
adapter = null;
|
|
2334
|
+
constructor() {
|
|
2335
|
+
// Effect to process angularConfig and apply to grid
|
|
2336
|
+
// This merges feature input plugins with the user's config plugins
|
|
2337
|
+
effect(() => {
|
|
2338
|
+
const config = this.angularConfig();
|
|
2339
|
+
if (!this.adapter)
|
|
2340
|
+
return;
|
|
2341
|
+
// Process the config to convert component classes to actual renderer/editor functions
|
|
2342
|
+
const processedConfig = config ? this.adapter.processGridConfig(config) : {};
|
|
2343
|
+
// Create plugins from feature inputs and merge with config plugins
|
|
2344
|
+
const featurePlugins = this.createFeaturePlugins();
|
|
2345
|
+
const configPlugins = processedConfig.plugins || [];
|
|
2346
|
+
// Merge: feature plugins first, then config plugins
|
|
2347
|
+
const mergedPlugins = [...featurePlugins, ...configPlugins];
|
|
2348
|
+
// Build core config overrides from individual inputs
|
|
2349
|
+
const sortableValue = this.sortable();
|
|
2350
|
+
const filterableValue = this.filterable();
|
|
2351
|
+
const selectableValue = this.selectable();
|
|
2352
|
+
const coreConfigOverrides = {};
|
|
2353
|
+
if (sortableValue !== undefined) {
|
|
2354
|
+
coreConfigOverrides['sortable'] = sortableValue;
|
|
2355
|
+
}
|
|
2356
|
+
if (filterableValue !== undefined) {
|
|
2357
|
+
coreConfigOverrides['filterable'] = filterableValue;
|
|
2358
|
+
}
|
|
2359
|
+
if (selectableValue !== undefined) {
|
|
2360
|
+
coreConfigOverrides['selectable'] = selectableValue;
|
|
2361
|
+
}
|
|
2362
|
+
// Merge icon overrides from registry with any existing icons in config
|
|
2363
|
+
// Registry icons are base, config.icons override them
|
|
2364
|
+
const registryIcons = this.iconRegistry?.getAll();
|
|
2365
|
+
if (registryIcons && Object.keys(registryIcons).length > 0) {
|
|
2366
|
+
const existingIcons = processedConfig?.icons || config?.icons || {};
|
|
2367
|
+
coreConfigOverrides['icons'] = { ...registryIcons, ...existingIcons };
|
|
2368
|
+
}
|
|
2369
|
+
// Apply to the grid element
|
|
2370
|
+
const grid = this.elementRef.nativeElement;
|
|
2371
|
+
grid.gridConfig = {
|
|
2372
|
+
...processedConfig,
|
|
2373
|
+
...coreConfigOverrides,
|
|
2374
|
+
plugins: mergedPlugins.length > 0 ? mergedPlugins : undefined,
|
|
2375
|
+
};
|
|
2376
|
+
});
|
|
2377
|
+
// Effect to sync loading state to the grid element
|
|
2378
|
+
effect(() => {
|
|
2379
|
+
const loadingValue = this.loading();
|
|
2380
|
+
if (loadingValue === undefined)
|
|
2381
|
+
return;
|
|
2382
|
+
const grid = this.elementRef.nativeElement;
|
|
2383
|
+
grid.loading = loadingValue;
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* Custom CSS styles to inject into the grid.
|
|
2388
|
+
* Use this to style custom cell renderers, editors, or detail panels.
|
|
2389
|
+
*
|
|
2390
|
+
* @example
|
|
2391
|
+
* ```typescript
|
|
2392
|
+
* // In your component
|
|
2393
|
+
* customStyles = `
|
|
2394
|
+
* .my-detail-panel { padding: 16px; }
|
|
2395
|
+
* .my-status-badge { border-radius: 4px; }
|
|
2396
|
+
* `;
|
|
2397
|
+
* ```
|
|
2398
|
+
*
|
|
2399
|
+
* ```html
|
|
2400
|
+
* <tbw-grid [customStyles]="customStyles">...</tbw-grid>
|
|
2401
|
+
* ```
|
|
2402
|
+
*/
|
|
2403
|
+
customStyles = input(...(ngDevMode ? [undefined, { debugName: "customStyles" }] : []));
|
|
2404
|
+
/**
|
|
2405
|
+
* Grid-wide sorting toggle.
|
|
2406
|
+
* When false, disables sorting for all columns regardless of their individual `sortable` setting.
|
|
2407
|
+
* When true (default), columns with `sortable: true` can be sorted.
|
|
2408
|
+
*
|
|
2409
|
+
* This is a core grid config property, not a plugin feature.
|
|
2410
|
+
* For multi-column sorting, also add the `[multiSort]` feature.
|
|
2411
|
+
*
|
|
2412
|
+
* @default true
|
|
2413
|
+
*
|
|
2414
|
+
* @example
|
|
2415
|
+
* ```html
|
|
2416
|
+
* <!-- Disable all sorting -->
|
|
2417
|
+
* <tbw-grid [sortable]="false" />
|
|
2418
|
+
*
|
|
2419
|
+
* <!-- Enable sorting (default) - columns still need sortable: true -->
|
|
2420
|
+
* <tbw-grid [sortable]="true" />
|
|
2421
|
+
*
|
|
2422
|
+
* <!-- Enable multi-column sorting -->
|
|
2423
|
+
* <tbw-grid [sortable]="true" [multiSort]="true" />
|
|
2424
|
+
* ```
|
|
2425
|
+
*/
|
|
2426
|
+
sortable = input(...(ngDevMode ? [undefined, { debugName: "sortable" }] : []));
|
|
2427
|
+
/**
|
|
2428
|
+
* Grid-wide filtering toggle.
|
|
2429
|
+
* When false, disables filtering for all columns regardless of their individual `filterable` setting.
|
|
2430
|
+
* When true (default), columns with `filterable: true` can be filtered.
|
|
2431
|
+
*
|
|
2432
|
+
* Requires the FilteringPlugin to be loaded.
|
|
2433
|
+
*
|
|
2434
|
+
* @default true
|
|
2435
|
+
*
|
|
2436
|
+
* @example
|
|
2437
|
+
* ```html
|
|
2438
|
+
* <!-- Disable all filtering -->
|
|
2439
|
+
* <tbw-grid [filterable]="false" [filtering]="true" />
|
|
2440
|
+
*
|
|
2441
|
+
* <!-- Enable filtering (default) -->
|
|
2442
|
+
* <tbw-grid [filterable]="true" [filtering]="true" />
|
|
2443
|
+
* ```
|
|
2444
|
+
*/
|
|
2445
|
+
filterable = input(...(ngDevMode ? [undefined, { debugName: "filterable" }] : []));
|
|
2446
|
+
/**
|
|
2447
|
+
* Grid-wide selection toggle.
|
|
2448
|
+
* When false, disables selection for all rows/cells.
|
|
2449
|
+
* When true (default), selection is enabled based on plugin mode.
|
|
2450
|
+
*
|
|
2451
|
+
* Requires the SelectionPlugin to be loaded.
|
|
2452
|
+
*
|
|
2453
|
+
* @default true
|
|
2454
|
+
*
|
|
2455
|
+
* @example
|
|
2456
|
+
* ```html
|
|
2457
|
+
* <!-- Disable all selection -->
|
|
2458
|
+
* <tbw-grid [selectable]="false" [selection]="'range'" />
|
|
2459
|
+
*
|
|
2460
|
+
* <!-- Enable selection (default) -->
|
|
2461
|
+
* <tbw-grid [selectable]="true" [selection]="'range'" />
|
|
2462
|
+
* ```
|
|
2463
|
+
*/
|
|
2464
|
+
selectable = input(...(ngDevMode ? [undefined, { debugName: "selectable" }] : []));
|
|
2465
|
+
/**
|
|
2466
|
+
* Show a loading overlay on the grid.
|
|
2467
|
+
* Use this during initial data fetch or refresh operations.
|
|
2468
|
+
*
|
|
2469
|
+
* For row/cell loading states, access the grid element directly:
|
|
2470
|
+
* - `grid.setRowLoading(rowId, true/false)`
|
|
2471
|
+
* - `grid.setCellLoading(rowId, field, true/false)`
|
|
2472
|
+
*
|
|
2473
|
+
* @default false
|
|
2474
|
+
*
|
|
2475
|
+
* @example
|
|
2476
|
+
* ```html
|
|
2477
|
+
* <!-- Show loading during data fetch -->
|
|
2478
|
+
* <tbw-grid [loading]="isLoading" [rows]="rows" />
|
|
2479
|
+
* ```
|
|
2480
|
+
*
|
|
2481
|
+
* ```typescript
|
|
2482
|
+
* isLoading = true;
|
|
2483
|
+
*
|
|
2484
|
+
* ngOnInit() {
|
|
2485
|
+
* this.dataService.fetchData().subscribe(data => {
|
|
2486
|
+
* this.rows = data;
|
|
2487
|
+
* this.isLoading = false;
|
|
2488
|
+
* });
|
|
2489
|
+
* }
|
|
2490
|
+
* ```
|
|
2491
|
+
*/
|
|
2492
|
+
loading = input(...(ngDevMode ? [undefined, { debugName: "loading" }] : []));
|
|
2493
|
+
/**
|
|
2494
|
+
* Angular-specific grid configuration that supports component classes for renderers/editors.
|
|
2495
|
+
*
|
|
2496
|
+
* Use this input when you want to specify Angular component classes directly in column configs.
|
|
2497
|
+
* Components must implement the appropriate interfaces:
|
|
2498
|
+
* - Renderers: `AngularCellRenderer<TRow, TValue>` - requires `value()` and `row()` signal inputs
|
|
2499
|
+
* - Editors: `AngularCellEditor<TRow, TValue>` - adds `commit` and `cancel` outputs
|
|
2500
|
+
*
|
|
2501
|
+
* The directive automatically processes component classes and converts them to grid-compatible
|
|
2502
|
+
* renderer/editor functions before applying to the grid.
|
|
2503
|
+
*
|
|
2504
|
+
* @example
|
|
2505
|
+
* ```typescript
|
|
2506
|
+
* // Component that implements AngularCellEditor
|
|
2507
|
+
* @Component({...})
|
|
2508
|
+
* export class BonusEditorComponent implements AngularCellEditor<Employee, number> {
|
|
2509
|
+
* value = input.required<number>();
|
|
2510
|
+
* row = input.required<Employee>();
|
|
2511
|
+
* commit = output<number>();
|
|
2512
|
+
* cancel = output<void>();
|
|
2513
|
+
* }
|
|
2514
|
+
*
|
|
2515
|
+
* // In your grid config
|
|
2516
|
+
* config: AngularGridConfig<Employee> = {
|
|
2517
|
+
* columns: [
|
|
2518
|
+
* { field: 'name', header: 'Name' },
|
|
2519
|
+
* { field: 'bonus', header: 'Bonus', editable: true, editor: BonusEditorComponent }
|
|
2520
|
+
* ]
|
|
2521
|
+
* };
|
|
2522
|
+
* ```
|
|
2523
|
+
*
|
|
2524
|
+
* ```html
|
|
2525
|
+
* <tbw-grid [angularConfig]="config" [rows]="employees"></tbw-grid>
|
|
2526
|
+
* ```
|
|
2527
|
+
*/
|
|
2528
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2529
|
+
angularConfig = input(...(ngDevMode ? [undefined, { debugName: "angularConfig" }] : []));
|
|
2530
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2531
|
+
// FEATURE INPUTS - Declarative plugin configuration
|
|
2532
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2533
|
+
/**
|
|
2534
|
+
* Enable cell/row/range selection.
|
|
2535
|
+
*
|
|
2536
|
+
* **Requires feature import:**
|
|
2537
|
+
* ```typescript
|
|
2538
|
+
* import '@toolbox-web/grid-angular/features/selection';
|
|
2539
|
+
* ```
|
|
2540
|
+
*
|
|
2541
|
+
* @example
|
|
2542
|
+
* ```html
|
|
2543
|
+
* <!-- Shorthand - just the mode -->
|
|
2544
|
+
* <tbw-grid [selection]="'range'" />
|
|
2545
|
+
*
|
|
2546
|
+
* <!-- Full config object -->
|
|
2547
|
+
* <tbw-grid [selection]="{ mode: 'range', checkbox: true }" />
|
|
2548
|
+
* ```
|
|
2549
|
+
*/
|
|
2550
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2551
|
+
selection = input(...(ngDevMode ? [undefined, { debugName: "selection" }] : []));
|
|
2552
|
+
/**
|
|
2553
|
+
* Enable inline cell editing.
|
|
2554
|
+
*
|
|
2555
|
+
* **Requires feature import:**
|
|
2556
|
+
* ```typescript
|
|
2557
|
+
* import '@toolbox-web/grid-angular/features/editing';
|
|
2558
|
+
* ```
|
|
2559
|
+
*
|
|
2560
|
+
* @example
|
|
2561
|
+
* ```html
|
|
2562
|
+
* <!-- Enable with default trigger (dblclick) -->
|
|
2563
|
+
* <tbw-grid [editing]="true" />
|
|
2564
|
+
*
|
|
2565
|
+
* <!-- Specify trigger -->
|
|
2566
|
+
* <tbw-grid [editing]="'click'" />
|
|
2567
|
+
* <tbw-grid [editing]="'dblclick'" />
|
|
2568
|
+
* <tbw-grid [editing]="'manual'" />
|
|
2569
|
+
* ```
|
|
2570
|
+
*/
|
|
2571
|
+
editing = input(...(ngDevMode ? [undefined, { debugName: "editing" }] : []));
|
|
2572
|
+
/**
|
|
2573
|
+
* Enable clipboard copy/paste. Requires selection to be enabled.
|
|
2574
|
+
*
|
|
2575
|
+
* **Requires feature import:**
|
|
2576
|
+
* ```typescript
|
|
2577
|
+
* import '@toolbox-web/grid-angular/features/clipboard';
|
|
2578
|
+
* ```
|
|
2579
|
+
*
|
|
2580
|
+
* @example
|
|
2581
|
+
* ```html
|
|
2582
|
+
* <tbw-grid [selection]="'range'" [clipboard]="true" />
|
|
2583
|
+
* ```
|
|
2584
|
+
*/
|
|
2585
|
+
clipboard = input(...(ngDevMode ? [undefined, { debugName: "clipboard" }] : []));
|
|
2586
|
+
/**
|
|
2587
|
+
* Enable right-click context menu.
|
|
2588
|
+
*
|
|
2589
|
+
* **Requires feature import:**
|
|
2590
|
+
* ```typescript
|
|
2591
|
+
* import '@toolbox-web/grid-angular/features/context-menu';
|
|
2592
|
+
* ```
|
|
2593
|
+
*
|
|
2594
|
+
* @example
|
|
2595
|
+
* ```html
|
|
2596
|
+
* <tbw-grid [contextMenu]="true" />
|
|
2597
|
+
* ```
|
|
2598
|
+
*/
|
|
2599
|
+
contextMenu = input(...(ngDevMode ? [undefined, { debugName: "contextMenu" }] : []));
|
|
2600
|
+
/**
|
|
2601
|
+
* Enable multi-column sorting.
|
|
2602
|
+
*
|
|
2603
|
+
* Multi-sort allows users to sort by multiple columns simultaneously.
|
|
2604
|
+
* For basic single-column sorting, columns with `sortable: true` work without this plugin.
|
|
2605
|
+
*
|
|
2606
|
+
* **Requires feature import:**
|
|
2607
|
+
* ```typescript
|
|
2608
|
+
* import '@toolbox-web/grid-angular/features/multi-sort';
|
|
2609
|
+
* ```
|
|
2610
|
+
*
|
|
2611
|
+
* @example
|
|
2612
|
+
* ```html
|
|
2613
|
+
* <!-- Enable multi-column sorting -->
|
|
2614
|
+
* <tbw-grid [multiSort]="true" />
|
|
2615
|
+
*
|
|
2616
|
+
* <!-- Limit to single column (uses plugin but restricts to 1 column) -->
|
|
2617
|
+
* <tbw-grid [multiSort]="'single'" />
|
|
2618
|
+
*
|
|
2619
|
+
* <!-- Full config -->
|
|
2620
|
+
* <tbw-grid [multiSort]="{ maxSortColumns: 3 }" />
|
|
2621
|
+
* ```
|
|
2622
|
+
*/
|
|
2623
|
+
multiSort = input(...(ngDevMode ? [undefined, { debugName: "multiSort" }] : []));
|
|
2624
|
+
/**
|
|
2625
|
+
* @deprecated Use `[multiSort]` instead. Will be removed in a future version.
|
|
2626
|
+
*
|
|
2627
|
+
* Enable column sorting. This is an alias for `[multiSort]`.
|
|
2628
|
+
*
|
|
2629
|
+
* **Requires feature import:**
|
|
2630
|
+
* ```typescript
|
|
2631
|
+
* import '@toolbox-web/grid-angular/features/multi-sort';
|
|
2632
|
+
* ```
|
|
2633
|
+
*/
|
|
2634
|
+
sorting = input(...(ngDevMode ? [undefined, { debugName: "sorting" }] : []));
|
|
2635
|
+
/**
|
|
2636
|
+
* Enable column filtering.
|
|
2637
|
+
*
|
|
2638
|
+
* **Requires feature import:**
|
|
2639
|
+
* ```typescript
|
|
2640
|
+
* import '@toolbox-web/grid-angular/features/filtering';
|
|
2641
|
+
* ```
|
|
2642
|
+
*
|
|
2643
|
+
* @example
|
|
2644
|
+
* ```html
|
|
2645
|
+
* <tbw-grid [filtering]="true" />
|
|
2646
|
+
* <tbw-grid [filtering]="{ debounceMs: 200 }" />
|
|
2647
|
+
* ```
|
|
2648
|
+
*/
|
|
2649
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2650
|
+
filtering = input(...(ngDevMode ? [undefined, { debugName: "filtering" }] : []));
|
|
2651
|
+
/**
|
|
2652
|
+
* Enable column drag-to-reorder.
|
|
2653
|
+
*
|
|
2654
|
+
* **Requires feature import:**
|
|
2655
|
+
* ```typescript
|
|
2656
|
+
* import '@toolbox-web/grid-angular/features/reorder';
|
|
2657
|
+
* ```
|
|
2658
|
+
*
|
|
2659
|
+
* @example
|
|
2660
|
+
* ```html
|
|
2661
|
+
* <tbw-grid [reorder]="true" />
|
|
2662
|
+
* ```
|
|
2663
|
+
*/
|
|
2664
|
+
reorder = input(...(ngDevMode ? [undefined, { debugName: "reorder" }] : []));
|
|
2665
|
+
/**
|
|
2666
|
+
* Enable column visibility toggle panel.
|
|
2667
|
+
*
|
|
2668
|
+
* **Requires feature import:**
|
|
2669
|
+
* ```typescript
|
|
2670
|
+
* import '@toolbox-web/grid-angular/features/visibility';
|
|
2671
|
+
* ```
|
|
2672
|
+
*
|
|
2673
|
+
* @example
|
|
2674
|
+
* ```html
|
|
2675
|
+
* <tbw-grid [visibility]="true" />
|
|
2676
|
+
* ```
|
|
2677
|
+
*/
|
|
2678
|
+
visibility = input(...(ngDevMode ? [undefined, { debugName: "visibility" }] : []));
|
|
2679
|
+
/**
|
|
2680
|
+
* Enable pinned/sticky columns.
|
|
2681
|
+
* Columns are pinned via the `sticky` column property.
|
|
2682
|
+
*
|
|
2683
|
+
* **Requires feature import:**
|
|
2684
|
+
* ```typescript
|
|
2685
|
+
* import '@toolbox-web/grid-angular/features/pinned-columns';
|
|
2686
|
+
* ```
|
|
2687
|
+
*
|
|
2688
|
+
* @example
|
|
2689
|
+
* ```html
|
|
2690
|
+
* <tbw-grid [pinnedColumns]="true" [columns]="[
|
|
2691
|
+
* { field: 'id', sticky: 'left' },
|
|
2692
|
+
* { field: 'name' },
|
|
2693
|
+
* { field: 'actions', sticky: 'right' }
|
|
2694
|
+
* ]" />
|
|
2695
|
+
* ```
|
|
2696
|
+
*/
|
|
2697
|
+
pinnedColumns = input(...(ngDevMode ? [undefined, { debugName: "pinnedColumns" }] : []));
|
|
2698
|
+
/**
|
|
2699
|
+
* Enable multi-level column headers (column groups).
|
|
2700
|
+
*
|
|
2701
|
+
* **Requires feature import:**
|
|
2702
|
+
* ```typescript
|
|
2703
|
+
* import '@toolbox-web/grid-angular/features/grouping-columns';
|
|
2704
|
+
* ```
|
|
2705
|
+
*
|
|
2706
|
+
* @example
|
|
2707
|
+
* ```html
|
|
2708
|
+
* <tbw-grid [groupingColumns]="{ columnGroups: [...] }" />
|
|
2709
|
+
* ```
|
|
2710
|
+
*/
|
|
2711
|
+
groupingColumns = input(...(ngDevMode ? [undefined, { debugName: "groupingColumns" }] : []));
|
|
2712
|
+
/**
|
|
2713
|
+
* Enable horizontal column virtualization for wide grids.
|
|
2714
|
+
*
|
|
2715
|
+
* **Requires feature import:**
|
|
2716
|
+
* ```typescript
|
|
2717
|
+
* import '@toolbox-web/grid-angular/features/column-virtualization';
|
|
2718
|
+
* ```
|
|
2719
|
+
*
|
|
2720
|
+
* @example
|
|
2721
|
+
* ```html
|
|
2722
|
+
* <tbw-grid [columnVirtualization]="true" />
|
|
2723
|
+
* ```
|
|
2724
|
+
*/
|
|
2725
|
+
columnVirtualization = input(...(ngDevMode ? [undefined, { debugName: "columnVirtualization" }] : []));
|
|
2726
|
+
/**
|
|
2727
|
+
* Enable row drag-to-reorder.
|
|
2728
|
+
*
|
|
2729
|
+
* **Requires feature import:**
|
|
2730
|
+
* ```typescript
|
|
2731
|
+
* import '@toolbox-web/grid-angular/features/row-reorder';
|
|
2732
|
+
* ```
|
|
2733
|
+
*
|
|
2734
|
+
* @example
|
|
2735
|
+
* ```html
|
|
2736
|
+
* <tbw-grid [rowReorder]="true" />
|
|
2737
|
+
* ```
|
|
2738
|
+
*/
|
|
2739
|
+
rowReorder = input(...(ngDevMode ? [undefined, { debugName: "rowReorder" }] : []));
|
|
2740
|
+
/**
|
|
2741
|
+
* Enable row grouping by field values.
|
|
2742
|
+
*
|
|
2743
|
+
* **Requires feature import:**
|
|
2744
|
+
* ```typescript
|
|
2745
|
+
* import '@toolbox-web/grid-angular/features/grouping-rows';
|
|
2746
|
+
* ```
|
|
2747
|
+
*
|
|
2748
|
+
* @example
|
|
2749
|
+
* ```html
|
|
2750
|
+
* <tbw-grid [groupingRows]="{ groupBy: ['department'] }" />
|
|
2751
|
+
* ```
|
|
2752
|
+
*/
|
|
2753
|
+
groupingRows = input(...(ngDevMode ? [undefined, { debugName: "groupingRows" }] : []));
|
|
2754
|
+
/**
|
|
2755
|
+
* Enable pinned rows (aggregation/status bar).
|
|
2756
|
+
*
|
|
2757
|
+
* **Requires feature import:**
|
|
2758
|
+
* ```typescript
|
|
2759
|
+
* import '@toolbox-web/grid-angular/features/pinned-rows';
|
|
2760
|
+
* ```
|
|
2761
|
+
*
|
|
2762
|
+
* @example
|
|
2763
|
+
* ```html
|
|
2764
|
+
* <tbw-grid [pinnedRows]="{ bottom: [{ type: 'aggregation' }] }" />
|
|
2765
|
+
* ```
|
|
2766
|
+
*/
|
|
2767
|
+
pinnedRows = input(...(ngDevMode ? [undefined, { debugName: "pinnedRows" }] : []));
|
|
2768
|
+
/**
|
|
2769
|
+
* Enable hierarchical tree view.
|
|
2770
|
+
*
|
|
2771
|
+
* **Requires feature import:**
|
|
2772
|
+
* ```typescript
|
|
2773
|
+
* import '@toolbox-web/grid-angular/features/tree';
|
|
2774
|
+
* ```
|
|
2775
|
+
*
|
|
2776
|
+
* @example
|
|
2777
|
+
* ```html
|
|
2778
|
+
* <tbw-grid [tree]="{ childrenField: 'children' }" />
|
|
2779
|
+
* ```
|
|
2780
|
+
*/
|
|
2781
|
+
tree = input(...(ngDevMode ? [undefined, { debugName: "tree" }] : []));
|
|
2782
|
+
/**
|
|
2783
|
+
* Enable master-detail expandable rows.
|
|
2784
|
+
*
|
|
2785
|
+
* **Requires feature import:**
|
|
2786
|
+
* ```typescript
|
|
2787
|
+
* import '@toolbox-web/grid-angular/features/master-detail';
|
|
2788
|
+
* ```
|
|
2789
|
+
*
|
|
2790
|
+
* @example
|
|
2791
|
+
* ```html
|
|
2792
|
+
* <tbw-grid [masterDetail]="{ detailRenderer: detailFn }" />
|
|
2793
|
+
* ```
|
|
2794
|
+
*/
|
|
2795
|
+
masterDetail = input(...(ngDevMode ? [undefined, { debugName: "masterDetail" }] : []));
|
|
2796
|
+
/**
|
|
2797
|
+
* Enable responsive card layout for narrow viewports.
|
|
2798
|
+
*
|
|
2799
|
+
* **Requires feature import:**
|
|
2800
|
+
* ```typescript
|
|
2801
|
+
* import '@toolbox-web/grid-angular/features/responsive';
|
|
2802
|
+
* ```
|
|
2803
|
+
*
|
|
2804
|
+
* @example
|
|
2805
|
+
* ```html
|
|
2806
|
+
* <tbw-grid [responsive]="{ breakpoint: 768 }" />
|
|
2807
|
+
* ```
|
|
2808
|
+
*/
|
|
2809
|
+
responsive = input(...(ngDevMode ? [undefined, { debugName: "responsive" }] : []));
|
|
2810
|
+
/**
|
|
2811
|
+
* Enable undo/redo for cell edits. Requires editing to be enabled.
|
|
2812
|
+
*
|
|
2813
|
+
* **Requires feature import:**
|
|
2814
|
+
* ```typescript
|
|
2815
|
+
* import '@toolbox-web/grid-angular/features/undo-redo';
|
|
2816
|
+
* ```
|
|
2817
|
+
*
|
|
2818
|
+
* @example
|
|
2819
|
+
* ```html
|
|
2820
|
+
* <tbw-grid [editing]="'dblclick'" [undoRedo]="true" />
|
|
2821
|
+
* ```
|
|
2822
|
+
*/
|
|
2823
|
+
undoRedo = input(...(ngDevMode ? [undefined, { debugName: "undoRedo" }] : []));
|
|
2824
|
+
/**
|
|
2825
|
+
* Enable CSV/JSON export functionality.
|
|
2826
|
+
*
|
|
2827
|
+
* **Requires feature import:**
|
|
2828
|
+
* ```typescript
|
|
2829
|
+
* import '@toolbox-web/grid-angular/features/export';
|
|
2830
|
+
* ```
|
|
2831
|
+
*
|
|
2832
|
+
* @example
|
|
2833
|
+
* ```html
|
|
2834
|
+
* <tbw-grid [export]="true" />
|
|
2835
|
+
* <tbw-grid [export]="{ filename: 'data.csv' }" />
|
|
2836
|
+
* ```
|
|
2837
|
+
*/
|
|
2838
|
+
exportFeature = input(...(ngDevMode ? [undefined, { debugName: "exportFeature" }] : []));
|
|
2839
|
+
/**
|
|
2840
|
+
* Enable print functionality.
|
|
2841
|
+
*
|
|
2842
|
+
* **Requires feature import:**
|
|
2843
|
+
* ```typescript
|
|
2844
|
+
* import '@toolbox-web/grid-angular/features/print';
|
|
2845
|
+
* ```
|
|
2846
|
+
*
|
|
2847
|
+
* @example
|
|
2848
|
+
* ```html
|
|
2849
|
+
* <tbw-grid [print]="true" />
|
|
2850
|
+
* ```
|
|
2851
|
+
*/
|
|
2852
|
+
print = input(...(ngDevMode ? [undefined, { debugName: "print" }] : []));
|
|
2853
|
+
/**
|
|
2854
|
+
* Enable pivot table functionality.
|
|
2855
|
+
*
|
|
2856
|
+
* **Requires feature import:**
|
|
2857
|
+
* ```typescript
|
|
2858
|
+
* import '@toolbox-web/grid-angular/features/pivot';
|
|
2859
|
+
* ```
|
|
2860
|
+
*
|
|
2861
|
+
* @example
|
|
2862
|
+
* ```html
|
|
2863
|
+
* <tbw-grid [pivot]="{ rowFields: ['category'], valueField: 'sales' }" />
|
|
2864
|
+
* ```
|
|
2865
|
+
*/
|
|
2866
|
+
pivot = input(...(ngDevMode ? [undefined, { debugName: "pivot" }] : []));
|
|
2867
|
+
/**
|
|
2868
|
+
* Enable server-side data operations.
|
|
2869
|
+
*
|
|
2870
|
+
* **Requires feature import:**
|
|
2871
|
+
* ```typescript
|
|
2872
|
+
* import '@toolbox-web/grid-angular/features/server-side';
|
|
2873
|
+
* ```
|
|
2874
|
+
*
|
|
2875
|
+
* @example
|
|
2876
|
+
* ```html
|
|
2877
|
+
* <tbw-grid [serverSide]="{ dataSource: fetchDataFn }" />
|
|
2878
|
+
* ```
|
|
2879
|
+
*/
|
|
2880
|
+
serverSide = input(...(ngDevMode ? [undefined, { debugName: "serverSide" }] : []));
|
|
2881
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2882
|
+
// EVENT OUTPUTS - All grid events
|
|
2883
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2884
|
+
/**
|
|
2885
|
+
* Emitted when a cell is clicked.
|
|
2886
|
+
*
|
|
2887
|
+
* @example
|
|
2888
|
+
* ```html
|
|
2889
|
+
* <tbw-grid (cellClick)="onCellClick($event)">...</tbw-grid>
|
|
2890
|
+
* ```
|
|
2891
|
+
*/
|
|
2892
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2893
|
+
cellClick = output();
|
|
2894
|
+
/**
|
|
2895
|
+
* Emitted when a row is clicked.
|
|
2896
|
+
*
|
|
2897
|
+
* @example
|
|
2898
|
+
* ```html
|
|
2899
|
+
* <tbw-grid (rowClick)="onRowClick($event)">...</tbw-grid>
|
|
2900
|
+
* ```
|
|
2901
|
+
*/
|
|
2902
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2903
|
+
rowClick = output();
|
|
2904
|
+
/**
|
|
2905
|
+
* Emitted when a cell is activated (Enter key or double-click).
|
|
2906
|
+
*
|
|
2907
|
+
* @example
|
|
2908
|
+
* ```html
|
|
2909
|
+
* <tbw-grid (cellActivate)="onCellActivate($event)">...</tbw-grid>
|
|
2910
|
+
* ```
|
|
2911
|
+
*/
|
|
2912
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2913
|
+
cellActivate = output();
|
|
2914
|
+
/**
|
|
2915
|
+
* Emitted when a cell value changes (before commit).
|
|
2916
|
+
*
|
|
2917
|
+
* @example
|
|
2918
|
+
* ```html
|
|
2919
|
+
* <tbw-grid (cellChange)="onCellChange($event)">...</tbw-grid>
|
|
2920
|
+
* ```
|
|
2921
|
+
*/
|
|
2922
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2923
|
+
cellChange = output();
|
|
2924
|
+
/**
|
|
2925
|
+
* Emitted when a cell value is committed (inline editing).
|
|
2926
|
+
* Provides the row, field, new value, and change tracking information.
|
|
2927
|
+
*
|
|
2928
|
+
* @example
|
|
2929
|
+
* ```html
|
|
2930
|
+
* <tbw-grid (cellCommit)="onCellCommit($event)">...</tbw-grid>
|
|
2931
|
+
* ```
|
|
2932
|
+
*
|
|
2933
|
+
* ```typescript
|
|
2934
|
+
* onCellCommit(event: CellCommitEvent) {
|
|
2935
|
+
* console.log(`Changed ${event.field} to ${event.value} in row ${event.rowIndex}`);
|
|
2936
|
+
* }
|
|
2937
|
+
* ```
|
|
2938
|
+
*/
|
|
2939
|
+
cellCommit = output();
|
|
2940
|
+
/**
|
|
2941
|
+
* Emitted when a row's values are committed (bulk/row editing).
|
|
2942
|
+
* Provides the row data and change tracking information.
|
|
2943
|
+
*
|
|
2944
|
+
* @example
|
|
2945
|
+
* ```html
|
|
2946
|
+
* <tbw-grid (rowCommit)="onRowCommit($event)">...</tbw-grid>
|
|
2947
|
+
* ```
|
|
2948
|
+
*/
|
|
2949
|
+
rowCommit = output();
|
|
2950
|
+
/**
|
|
2951
|
+
* Emitted when the changed rows are reset.
|
|
2952
|
+
*
|
|
2953
|
+
* @example
|
|
2954
|
+
* ```html
|
|
2955
|
+
* <tbw-grid (changedRowsReset)="onChangedRowsReset($event)">...</tbw-grid>
|
|
2956
|
+
* ```
|
|
2957
|
+
*/
|
|
2958
|
+
changedRowsReset = output();
|
|
2959
|
+
/**
|
|
2960
|
+
* Emitted when sort state changes.
|
|
2961
|
+
*
|
|
2962
|
+
* @example
|
|
2963
|
+
* ```html
|
|
2964
|
+
* <tbw-grid (sortChange)="onSortChange($event)">...</tbw-grid>
|
|
2965
|
+
* ```
|
|
2966
|
+
*/
|
|
2967
|
+
sortChange = output();
|
|
2968
|
+
/**
|
|
2969
|
+
* Emitted when filter values change.
|
|
2970
|
+
*
|
|
2971
|
+
* @example
|
|
2972
|
+
* ```html
|
|
2973
|
+
* <tbw-grid (filterChange)="onFilterChange($event)">...</tbw-grid>
|
|
2974
|
+
* ```
|
|
2975
|
+
*/
|
|
2976
|
+
filterChange = output();
|
|
2977
|
+
/**
|
|
2978
|
+
* Emitted when a column is resized.
|
|
2979
|
+
*
|
|
2980
|
+
* @example
|
|
2981
|
+
* ```html
|
|
2982
|
+
* <tbw-grid (columnResize)="onColumnResize($event)">...</tbw-grid>
|
|
2983
|
+
* ```
|
|
2984
|
+
*/
|
|
2985
|
+
columnResize = output();
|
|
2986
|
+
/**
|
|
2987
|
+
* Emitted when a column is moved via drag-and-drop.
|
|
2988
|
+
*
|
|
2989
|
+
* @example
|
|
2990
|
+
* ```html
|
|
2991
|
+
* <tbw-grid (columnMove)="onColumnMove($event)">...</tbw-grid>
|
|
2992
|
+
* ```
|
|
2993
|
+
*/
|
|
2994
|
+
columnMove = output();
|
|
2995
|
+
/**
|
|
2996
|
+
* Emitted when column visibility changes.
|
|
2997
|
+
*
|
|
2998
|
+
* @example
|
|
2999
|
+
* ```html
|
|
3000
|
+
* <tbw-grid (columnVisibility)="onColumnVisibility($event)">...</tbw-grid>
|
|
3001
|
+
* ```
|
|
3002
|
+
*/
|
|
3003
|
+
columnVisibility = output();
|
|
3004
|
+
/**
|
|
3005
|
+
* Emitted when column state changes (resize, reorder, visibility).
|
|
3006
|
+
*
|
|
3007
|
+
* @example
|
|
3008
|
+
* ```html
|
|
3009
|
+
* <tbw-grid (columnStateChange)="onColumnStateChange($event)">...</tbw-grid>
|
|
3010
|
+
* ```
|
|
3011
|
+
*/
|
|
3012
|
+
columnStateChange = output();
|
|
3013
|
+
/**
|
|
3014
|
+
* Emitted when selection changes.
|
|
3015
|
+
*
|
|
3016
|
+
* @example
|
|
3017
|
+
* ```html
|
|
3018
|
+
* <tbw-grid (selectionChange)="onSelectionChange($event)">...</tbw-grid>
|
|
3019
|
+
* ```
|
|
3020
|
+
*/
|
|
3021
|
+
selectionChange = output();
|
|
3022
|
+
/**
|
|
3023
|
+
* Emitted when a row is moved via drag-and-drop.
|
|
3024
|
+
*
|
|
3025
|
+
* @example
|
|
3026
|
+
* ```html
|
|
3027
|
+
* <tbw-grid (rowMove)="onRowMove($event)">...</tbw-grid>
|
|
3028
|
+
* ```
|
|
3029
|
+
*/
|
|
3030
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3031
|
+
rowMove = output();
|
|
3032
|
+
/**
|
|
3033
|
+
* Emitted when a group is expanded or collapsed.
|
|
3034
|
+
*
|
|
3035
|
+
* @example
|
|
3036
|
+
* ```html
|
|
3037
|
+
* <tbw-grid (groupToggle)="onGroupToggle($event)">...</tbw-grid>
|
|
3038
|
+
* ```
|
|
3039
|
+
*/
|
|
3040
|
+
groupToggle = output();
|
|
3041
|
+
/**
|
|
3042
|
+
* Emitted when a tree node is expanded.
|
|
3043
|
+
*
|
|
3044
|
+
* @example
|
|
3045
|
+
* ```html
|
|
3046
|
+
* <tbw-grid (treeExpand)="onTreeExpand($event)">...</tbw-grid>
|
|
3047
|
+
* ```
|
|
3048
|
+
*/
|
|
3049
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3050
|
+
treeExpand = output();
|
|
3051
|
+
/**
|
|
3052
|
+
* Emitted when a detail panel is expanded or collapsed.
|
|
3053
|
+
*
|
|
3054
|
+
* @example
|
|
3055
|
+
* ```html
|
|
3056
|
+
* <tbw-grid (detailExpand)="onDetailExpand($event)">...</tbw-grid>
|
|
3057
|
+
* ```
|
|
3058
|
+
*/
|
|
3059
|
+
detailExpand = output();
|
|
3060
|
+
/**
|
|
3061
|
+
* Emitted when responsive mode changes (table ↔ card).
|
|
3062
|
+
*
|
|
3063
|
+
* @example
|
|
3064
|
+
* ```html
|
|
3065
|
+
* <tbw-grid (responsiveChange)="onResponsiveChange($event)">...</tbw-grid>
|
|
3066
|
+
* ```
|
|
3067
|
+
*/
|
|
3068
|
+
responsiveChange = output();
|
|
3069
|
+
/**
|
|
3070
|
+
* Emitted when cells are copied to clipboard.
|
|
3071
|
+
*
|
|
3072
|
+
* @example
|
|
3073
|
+
* ```html
|
|
3074
|
+
* <tbw-grid (copy)="onCopy($event)">...</tbw-grid>
|
|
3075
|
+
* ```
|
|
3076
|
+
*/
|
|
3077
|
+
copy = output();
|
|
3078
|
+
/**
|
|
3079
|
+
* Emitted when cells are pasted from clipboard.
|
|
3080
|
+
*
|
|
3081
|
+
* @example
|
|
3082
|
+
* ```html
|
|
3083
|
+
* <tbw-grid (paste)="onPaste($event)">...</tbw-grid>
|
|
3084
|
+
* ```
|
|
3085
|
+
*/
|
|
3086
|
+
paste = output();
|
|
3087
|
+
/**
|
|
3088
|
+
* Emitted when undo/redo is performed.
|
|
3089
|
+
*
|
|
3090
|
+
* @example
|
|
3091
|
+
* ```html
|
|
3092
|
+
* <tbw-grid (undoRedoAction)="onUndoRedo($event)">...</tbw-grid>
|
|
3093
|
+
* ```
|
|
3094
|
+
*/
|
|
3095
|
+
undoRedoAction = output();
|
|
3096
|
+
/**
|
|
3097
|
+
* Emitted when export completes.
|
|
3098
|
+
*
|
|
3099
|
+
* @example
|
|
3100
|
+
* ```html
|
|
3101
|
+
* <tbw-grid (exportComplete)="onExportComplete($event)">...</tbw-grid>
|
|
3102
|
+
* ```
|
|
3103
|
+
*/
|
|
3104
|
+
exportComplete = output();
|
|
3105
|
+
/**
|
|
3106
|
+
* Emitted when print starts.
|
|
3107
|
+
*
|
|
3108
|
+
* @example
|
|
3109
|
+
* ```html
|
|
3110
|
+
* <tbw-grid (printStart)="onPrintStart($event)">...</tbw-grid>
|
|
3111
|
+
* ```
|
|
3112
|
+
*/
|
|
3113
|
+
printStart = output();
|
|
3114
|
+
/**
|
|
3115
|
+
* Emitted when print completes.
|
|
3116
|
+
*
|
|
3117
|
+
* @example
|
|
3118
|
+
* ```html
|
|
3119
|
+
* <tbw-grid (printComplete)="onPrintComplete($event)">...</tbw-grid>
|
|
3120
|
+
* ```
|
|
3121
|
+
*/
|
|
3122
|
+
printComplete = output();
|
|
3123
|
+
// Map of output names to event names for automatic wiring
|
|
3124
|
+
eventOutputMap = {
|
|
3125
|
+
cellClick: 'cell-click',
|
|
3126
|
+
rowClick: 'row-click',
|
|
3127
|
+
cellActivate: 'cell-activate',
|
|
3128
|
+
cellChange: 'cell-change',
|
|
3129
|
+
cellCommit: 'cell-commit',
|
|
3130
|
+
rowCommit: 'row-commit',
|
|
3131
|
+
changedRowsReset: 'changed-rows-reset',
|
|
3132
|
+
sortChange: 'sort-change',
|
|
3133
|
+
filterChange: 'filter-change',
|
|
3134
|
+
columnResize: 'column-resize',
|
|
3135
|
+
columnMove: 'column-move',
|
|
3136
|
+
columnVisibility: 'column-visibility',
|
|
3137
|
+
columnStateChange: 'column-state-change',
|
|
3138
|
+
selectionChange: 'selection-change',
|
|
3139
|
+
rowMove: 'row-move',
|
|
3140
|
+
groupToggle: 'group-toggle',
|
|
3141
|
+
treeExpand: 'tree-expand',
|
|
3142
|
+
detailExpand: 'detail-expand',
|
|
3143
|
+
responsiveChange: 'responsive-change',
|
|
3144
|
+
copy: 'copy',
|
|
3145
|
+
paste: 'paste',
|
|
3146
|
+
undoRedoAction: 'undo-redo',
|
|
3147
|
+
exportComplete: 'export-complete',
|
|
3148
|
+
printStart: 'print-start',
|
|
3149
|
+
printComplete: 'print-complete',
|
|
3150
|
+
};
|
|
3151
|
+
// Store event listeners for cleanup
|
|
3152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3153
|
+
eventListeners = new Map();
|
|
3154
|
+
ngOnInit() {
|
|
3155
|
+
// Create and register the adapter
|
|
3156
|
+
this.adapter = new AngularGridAdapter(this.injector, this.appRef, this.viewContainerRef);
|
|
3157
|
+
DataGridElement.registerAdapter(this.adapter);
|
|
3158
|
+
const grid = this.elementRef.nativeElement;
|
|
3159
|
+
// Wire up all event listeners based on eventOutputMap
|
|
3160
|
+
this.setupEventListeners(grid);
|
|
3161
|
+
// Register adapter on the grid element so MasterDetailPlugin can use it
|
|
3162
|
+
// via the __frameworkAdapter hook during attach()
|
|
3163
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3164
|
+
grid.__frameworkAdapter = this.adapter;
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* Sets up event listeners for all outputs using the eventOutputMap.
|
|
3168
|
+
*/
|
|
3169
|
+
setupEventListeners(grid) {
|
|
3170
|
+
// Wire up all event listeners
|
|
3171
|
+
for (const [outputName, eventName] of Object.entries(this.eventOutputMap)) {
|
|
3172
|
+
const listener = (e) => {
|
|
3173
|
+
const detail = e.detail;
|
|
3174
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3175
|
+
this[outputName].emit(detail);
|
|
3176
|
+
};
|
|
3177
|
+
grid.addEventListener(eventName, listener);
|
|
3178
|
+
this.eventListeners.set(eventName, listener);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* Creates plugins from feature inputs.
|
|
3183
|
+
* Uses the feature registry to allow tree-shaking - only imported features are bundled.
|
|
3184
|
+
* Returns the array of created plugins (doesn't modify grid).
|
|
3185
|
+
*/
|
|
3186
|
+
createFeaturePlugins() {
|
|
3187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3188
|
+
const plugins = [];
|
|
3189
|
+
// Helper to add plugin if feature is registered
|
|
3190
|
+
const addPlugin = (name, config) => {
|
|
3191
|
+
if (config === undefined || config === null || config === false)
|
|
3192
|
+
return;
|
|
3193
|
+
const plugin = createPluginFromFeature(name, config);
|
|
3194
|
+
if (plugin)
|
|
3195
|
+
plugins.push(plugin);
|
|
3196
|
+
};
|
|
3197
|
+
// Add plugins for each feature input
|
|
3198
|
+
addPlugin('selection', this.selection());
|
|
3199
|
+
addPlugin('editing', this.editing());
|
|
3200
|
+
addPlugin('clipboard', this.clipboard());
|
|
3201
|
+
addPlugin('contextMenu', this.contextMenu());
|
|
3202
|
+
// multiSort is the primary input; sorting is a deprecated alias
|
|
3203
|
+
addPlugin('multiSort', this.multiSort() ?? this.sorting());
|
|
3204
|
+
addPlugin('filtering', this.filtering());
|
|
3205
|
+
addPlugin('reorder', this.reorder());
|
|
3206
|
+
addPlugin('visibility', this.visibility());
|
|
3207
|
+
addPlugin('pinnedColumns', this.pinnedColumns());
|
|
3208
|
+
addPlugin('groupingColumns', this.groupingColumns());
|
|
3209
|
+
addPlugin('columnVirtualization', this.columnVirtualization());
|
|
3210
|
+
addPlugin('rowReorder', this.rowReorder());
|
|
3211
|
+
addPlugin('groupingRows', this.groupingRows());
|
|
3212
|
+
addPlugin('pinnedRows', this.pinnedRows());
|
|
3213
|
+
addPlugin('tree', this.tree());
|
|
3214
|
+
addPlugin('masterDetail', this.masterDetail());
|
|
3215
|
+
addPlugin('responsive', this.responsive());
|
|
3216
|
+
addPlugin('undoRedo', this.undoRedo());
|
|
3217
|
+
addPlugin('export', this.exportFeature());
|
|
3218
|
+
addPlugin('print', this.print());
|
|
3219
|
+
addPlugin('pivot', this.pivot());
|
|
3220
|
+
addPlugin('serverSide', this.serverSide());
|
|
3221
|
+
return plugins;
|
|
3222
|
+
}
|
|
3223
|
+
ngAfterContentInit() {
|
|
3224
|
+
// After Angular child directives have initialized (GridColumnView, GridColumnEditor, GridDetailView, GridToolPanel),
|
|
3225
|
+
// force the grid to re-parse light DOM columns so adapters can create renderers/editors
|
|
3226
|
+
const grid = this.elementRef.nativeElement;
|
|
3227
|
+
if (grid && typeof grid.refreshColumns === 'function') {
|
|
3228
|
+
// Use setTimeout to ensure Angular effects have run (template registration)
|
|
3229
|
+
setTimeout(() => {
|
|
3230
|
+
grid.refreshColumns();
|
|
3231
|
+
// Configure MasterDetailPlugin after Angular templates are registered
|
|
3232
|
+
this.configureMasterDetail(grid);
|
|
3233
|
+
// Configure ResponsivePlugin card renderer if template is present
|
|
3234
|
+
this.configureResponsiveCard(grid);
|
|
3235
|
+
// Refresh shell header to pick up tool panel templates
|
|
3236
|
+
// This allows Angular templates to be used in tool panels
|
|
3237
|
+
if (typeof grid.refreshShellHeader === 'function') {
|
|
3238
|
+
grid.refreshShellHeader();
|
|
3239
|
+
}
|
|
3240
|
+
// Register custom styles if provided
|
|
3241
|
+
this.registerCustomStyles(grid);
|
|
3242
|
+
}, 0);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Registers custom styles into the grid.
|
|
3247
|
+
* Uses the grid's registerStyles() API for clean encapsulation.
|
|
3248
|
+
*/
|
|
3249
|
+
registerCustomStyles(grid) {
|
|
3250
|
+
const styles = this.customStyles();
|
|
3251
|
+
if (!styles)
|
|
3252
|
+
return;
|
|
3253
|
+
// Wait for grid to be ready before registering styles
|
|
3254
|
+
grid.ready?.().then(() => {
|
|
3255
|
+
grid.registerStyles?.('angular-custom-styles', styles);
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
/**
|
|
3259
|
+
* Configures the MasterDetailPlugin after Angular templates are registered.
|
|
3260
|
+
* - If plugin exists: refresh its detail renderer
|
|
3261
|
+
* - If plugin doesn't exist but <tbw-grid-detail> is present: dynamically import and add the plugin
|
|
3262
|
+
*/
|
|
3263
|
+
async configureMasterDetail(grid) {
|
|
3264
|
+
if (!this.adapter)
|
|
3265
|
+
return;
|
|
3266
|
+
// Check for existing plugin by name to avoid importing the class
|
|
3267
|
+
const existingPlugin = grid.gridConfig?.plugins?.find((p) => p.name === 'masterDetail');
|
|
3268
|
+
if (existingPlugin && typeof existingPlugin.refreshDetailRenderer === 'function') {
|
|
3269
|
+
// Plugin exists - just refresh the renderer to pick up Angular templates
|
|
3270
|
+
existingPlugin.refreshDetailRenderer();
|
|
3271
|
+
return;
|
|
3272
|
+
}
|
|
3273
|
+
// Check if <tbw-grid-detail> is present in light DOM
|
|
3274
|
+
const detailElement = grid.querySelector('tbw-grid-detail');
|
|
3275
|
+
if (!detailElement)
|
|
3276
|
+
return;
|
|
3277
|
+
// Create detail renderer from Angular template
|
|
3278
|
+
const detailRenderer = this.adapter.createDetailRenderer(grid);
|
|
3279
|
+
if (!detailRenderer)
|
|
3280
|
+
return;
|
|
3281
|
+
// Parse configuration from attributes
|
|
3282
|
+
const animationAttr = detailElement.getAttribute('animation');
|
|
3283
|
+
let animation = 'slide';
|
|
3284
|
+
if (animationAttr === 'false') {
|
|
3285
|
+
animation = false;
|
|
3286
|
+
}
|
|
3287
|
+
else if (animationAttr === 'fade') {
|
|
3288
|
+
animation = 'fade';
|
|
3289
|
+
}
|
|
3290
|
+
const showExpandColumn = detailElement.getAttribute('showExpandColumn') !== 'false';
|
|
3291
|
+
// Dynamically import the plugin to avoid bundling it when not used
|
|
3292
|
+
const { MasterDetailPlugin } = await import('@toolbox-web/grid/plugins/master-detail');
|
|
3293
|
+
// Create and add the plugin
|
|
3294
|
+
const plugin = new MasterDetailPlugin({
|
|
3295
|
+
detailRenderer: detailRenderer,
|
|
3296
|
+
showExpandColumn,
|
|
3297
|
+
animation,
|
|
3298
|
+
});
|
|
3299
|
+
const currentConfig = grid.gridConfig || {};
|
|
3300
|
+
const existingPlugins = currentConfig.plugins || [];
|
|
3301
|
+
grid.gridConfig = {
|
|
3302
|
+
...currentConfig,
|
|
3303
|
+
plugins: [...existingPlugins, plugin],
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* Configures the ResponsivePlugin with Angular template-based card renderer.
|
|
3308
|
+
* - If plugin exists: updates its cardRenderer configuration
|
|
3309
|
+
* - If plugin doesn't exist but <tbw-grid-responsive-card> is present: logs a warning
|
|
3310
|
+
*/
|
|
3311
|
+
configureResponsiveCard(grid) {
|
|
3312
|
+
if (!this.adapter)
|
|
3313
|
+
return;
|
|
3314
|
+
// Check if <tbw-grid-responsive-card> is present in light DOM
|
|
3315
|
+
const cardElement = grid.querySelector('tbw-grid-responsive-card');
|
|
3316
|
+
if (!cardElement)
|
|
3317
|
+
return;
|
|
3318
|
+
// Create card renderer from Angular template
|
|
3319
|
+
const cardRenderer = this.adapter.createResponsiveCardRenderer(grid);
|
|
3320
|
+
if (!cardRenderer)
|
|
3321
|
+
return;
|
|
3322
|
+
// Find existing plugin by name to avoid importing the class
|
|
3323
|
+
const existingPlugin = grid.gridConfig?.plugins?.find((p) => p.name === 'responsive');
|
|
3324
|
+
if (existingPlugin && typeof existingPlugin.setCardRenderer === 'function') {
|
|
3325
|
+
// Plugin exists - update its cardRenderer
|
|
3326
|
+
existingPlugin.setCardRenderer(cardRenderer);
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
// Plugin doesn't exist - log a warning
|
|
3330
|
+
console.warn('[tbw-grid-angular] <tbw-grid-responsive-card> found but ResponsivePlugin is not configured.\n' +
|
|
3331
|
+
'Add ResponsivePlugin to your gridConfig.plugins array:\n\n' +
|
|
3332
|
+
' import { ResponsivePlugin } from "@toolbox-web/grid/plugins/responsive";\n' +
|
|
3333
|
+
' gridConfig = {\n' +
|
|
3334
|
+
' plugins: [new ResponsivePlugin({ breakpoint: 600 })]\n' +
|
|
3335
|
+
' };');
|
|
3336
|
+
}
|
|
3337
|
+
ngOnDestroy() {
|
|
3338
|
+
const grid = this.elementRef.nativeElement;
|
|
3339
|
+
// Cleanup all event listeners
|
|
3340
|
+
if (grid) {
|
|
3341
|
+
for (const [eventName, listener] of this.eventListeners) {
|
|
3342
|
+
grid.removeEventListener(eventName, listener);
|
|
3343
|
+
}
|
|
3344
|
+
this.eventListeners.clear();
|
|
3345
|
+
}
|
|
3346
|
+
// Cleanup custom styles
|
|
3347
|
+
if (grid && this.customStyles()) {
|
|
3348
|
+
grid.unregisterStyles?.('angular-custom-styles');
|
|
3349
|
+
}
|
|
3350
|
+
// Cleanup adapter if needed
|
|
3351
|
+
if (this.adapter) {
|
|
3352
|
+
this.adapter.destroy?.();
|
|
3353
|
+
this.adapter = null;
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: Grid, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
3357
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.1", type: Grid, isStandalone: true, selector: "tbw-grid", inputs: { customStyles: { classPropertyName: "customStyles", publicName: "customStyles", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, filterable: { classPropertyName: "filterable", publicName: "filterable", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, angularConfig: { classPropertyName: "angularConfig", publicName: "angularConfig", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, clipboard: { classPropertyName: "clipboard", publicName: "clipboard", isSignal: true, isRequired: false, transformFunction: null }, contextMenu: { classPropertyName: "contextMenu", publicName: "contextMenu", isSignal: true, isRequired: false, transformFunction: null }, multiSort: { classPropertyName: "multiSort", publicName: "multiSort", isSignal: true, isRequired: false, transformFunction: null }, sorting: { classPropertyName: "sorting", publicName: "sorting", isSignal: true, isRequired: false, transformFunction: null }, filtering: { classPropertyName: "filtering", publicName: "filtering", isSignal: true, isRequired: false, transformFunction: null }, reorder: { classPropertyName: "reorder", publicName: "reorder", isSignal: true, isRequired: false, transformFunction: null }, visibility: { classPropertyName: "visibility", publicName: "visibility", isSignal: true, isRequired: false, transformFunction: null }, pinnedColumns: { classPropertyName: "pinnedColumns", publicName: "pinnedColumns", isSignal: true, isRequired: false, transformFunction: null }, groupingColumns: { classPropertyName: "groupingColumns", publicName: "groupingColumns", isSignal: true, isRequired: false, transformFunction: null }, columnVirtualization: { classPropertyName: "columnVirtualization", publicName: "columnVirtualization", isSignal: true, isRequired: false, transformFunction: null }, rowReorder: { classPropertyName: "rowReorder", publicName: "rowReorder", isSignal: true, isRequired: false, transformFunction: null }, groupingRows: { classPropertyName: "groupingRows", publicName: "groupingRows", isSignal: true, isRequired: false, transformFunction: null }, pinnedRows: { classPropertyName: "pinnedRows", publicName: "pinnedRows", isSignal: true, isRequired: false, transformFunction: null }, tree: { classPropertyName: "tree", publicName: "tree", isSignal: true, isRequired: false, transformFunction: null }, masterDetail: { classPropertyName: "masterDetail", publicName: "masterDetail", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, undoRedo: { classPropertyName: "undoRedo", publicName: "undoRedo", isSignal: true, isRequired: false, transformFunction: null }, exportFeature: { classPropertyName: "exportFeature", publicName: "exportFeature", isSignal: true, isRequired: false, transformFunction: null }, print: { classPropertyName: "print", publicName: "print", isSignal: true, isRequired: false, transformFunction: null }, pivot: { classPropertyName: "pivot", publicName: "pivot", isSignal: true, isRequired: false, transformFunction: null }, serverSide: { classPropertyName: "serverSide", publicName: "serverSide", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cellClick: "cellClick", rowClick: "rowClick", cellActivate: "cellActivate", cellChange: "cellChange", cellCommit: "cellCommit", rowCommit: "rowCommit", changedRowsReset: "changedRowsReset", sortChange: "sortChange", filterChange: "filterChange", columnResize: "columnResize", columnMove: "columnMove", columnVisibility: "columnVisibility", columnStateChange: "columnStateChange", selectionChange: "selectionChange", rowMove: "rowMove", groupToggle: "groupToggle", treeExpand: "treeExpand", detailExpand: "detailExpand", responsiveChange: "responsiveChange", copy: "copy", paste: "paste", undoRedoAction: "undoRedoAction", exportComplete: "exportComplete", printStart: "printStart", printComplete: "printComplete" }, ngImport: i0 });
|
|
3358
|
+
}
|
|
3359
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.1", ngImport: i0, type: Grid, decorators: [{
|
|
3360
|
+
type: Directive,
|
|
3361
|
+
args: [{ selector: 'tbw-grid' }]
|
|
3362
|
+
}], ctorParameters: () => [], propDecorators: { customStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "customStyles", required: false }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], filterable: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterable", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], angularConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "angularConfig", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], editing: [{ type: i0.Input, args: [{ isSignal: true, alias: "editing", required: false }] }], clipboard: [{ type: i0.Input, args: [{ isSignal: true, alias: "clipboard", required: false }] }], contextMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "contextMenu", required: false }] }], multiSort: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiSort", required: false }] }], sorting: [{ type: i0.Input, args: [{ isSignal: true, alias: "sorting", required: false }] }], filtering: [{ type: i0.Input, args: [{ isSignal: true, alias: "filtering", required: false }] }], reorder: [{ type: i0.Input, args: [{ isSignal: true, alias: "reorder", required: false }] }], visibility: [{ type: i0.Input, args: [{ isSignal: true, alias: "visibility", required: false }] }], pinnedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "pinnedColumns", required: false }] }], groupingColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupingColumns", required: false }] }], columnVirtualization: [{ type: i0.Input, args: [{ isSignal: true, alias: "columnVirtualization", required: false }] }], rowReorder: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowReorder", required: false }] }], groupingRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupingRows", required: false }] }], pinnedRows: [{ type: i0.Input, args: [{ isSignal: true, alias: "pinnedRows", required: false }] }], tree: [{ type: i0.Input, args: [{ isSignal: true, alias: "tree", required: false }] }], masterDetail: [{ type: i0.Input, args: [{ isSignal: true, alias: "masterDetail", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], undoRedo: [{ type: i0.Input, args: [{ isSignal: true, alias: "undoRedo", required: false }] }], exportFeature: [{ type: i0.Input, args: [{ isSignal: true, alias: "exportFeature", required: false }] }], print: [{ type: i0.Input, args: [{ isSignal: true, alias: "print", required: false }] }], pivot: [{ type: i0.Input, args: [{ isSignal: true, alias: "pivot", required: false }] }], serverSide: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverSide", required: false }] }], cellClick: [{ type: i0.Output, args: ["cellClick"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], cellActivate: [{ type: i0.Output, args: ["cellActivate"] }], cellChange: [{ type: i0.Output, args: ["cellChange"] }], cellCommit: [{ type: i0.Output, args: ["cellCommit"] }], rowCommit: [{ type: i0.Output, args: ["rowCommit"] }], changedRowsReset: [{ type: i0.Output, args: ["changedRowsReset"] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], filterChange: [{ type: i0.Output, args: ["filterChange"] }], columnResize: [{ type: i0.Output, args: ["columnResize"] }], columnMove: [{ type: i0.Output, args: ["columnMove"] }], columnVisibility: [{ type: i0.Output, args: ["columnVisibility"] }], columnStateChange: [{ type: i0.Output, args: ["columnStateChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowMove: [{ type: i0.Output, args: ["rowMove"] }], groupToggle: [{ type: i0.Output, args: ["groupToggle"] }], treeExpand: [{ type: i0.Output, args: ["treeExpand"] }], detailExpand: [{ type: i0.Output, args: ["detailExpand"] }], responsiveChange: [{ type: i0.Output, args: ["responsiveChange"] }], copy: [{ type: i0.Output, args: ["copy"] }], paste: [{ type: i0.Output, args: ["paste"] }], undoRedoAction: [{ type: i0.Output, args: ["undoRedoAction"] }], exportComplete: [{ type: i0.Output, args: ["exportComplete"] }], printStart: [{ type: i0.Output, args: ["printStart"] }], printComplete: [{ type: i0.Output, args: ["printComplete"] }] } });
|
|
3363
|
+
|
|
3364
|
+
/**
|
|
3365
|
+
* @packageDocumentation
|
|
3366
|
+
* @toolbox-web/grid-angular - Angular adapter for @toolbox-web/grid.
|
|
3367
|
+
*
|
|
3368
|
+
* Provides directives for seamless Angular integration with the grid component.
|
|
3369
|
+
*/
|
|
3370
|
+
|
|
3371
|
+
/**
|
|
3372
|
+
* Generated bundle index. Do not edit.
|
|
3373
|
+
*/
|
|
3374
|
+
|
|
3375
|
+
export { AngularGridAdapter, BaseGridEditor, GRID_ICONS, GRID_TYPE_DEFAULTS, Grid, GridColumnEditor, GridColumnView, GridDetailView, GridFormArray, GridIconRegistry, GridResponsiveCard, GridToolPanel, GridTypeRegistry, TbwEditor as TbwCellEditor, TbwRenderer as TbwCellView, TbwEditor, TbwRenderer, clearFeatureRegistry, createPluginFromFeature, getFeatureFactory, getFormArrayContext, getRegisteredFeatures, injectGrid, isComponentClass, isFeatureRegistered, provideGridIcons, provideGridTypeDefaults, registerFeature };
|
|
3376
|
+
//# sourceMappingURL=toolbox-web-grid-angular.mjs.map
|