@angular-bootstrap/ngbootstrap 0.0.9 → 0.0.11

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 CHANGED
@@ -39,6 +39,7 @@ All components are standalone, so you import them directly into your feature com
39
39
  ```ts
40
40
  import { Component } from '@angular/core';
41
41
  import { Datagrid } from '@angular-bootstrap/ngbootstrap/datagrid';
42
+ import { NgbDatagridDefaultEditService } from '@angular-bootstrap/ngbootstrap/datagrid';
42
43
 
43
44
  interface User {
44
45
  id: number;
@@ -54,6 +55,8 @@ interface User {
54
55
  <ngb-datagrid
55
56
  [columns]="columns"
56
57
  [data]="rows"
58
+ [trackBy]="trackById"
59
+ [editService]="editService"
57
60
  [enableSorting]="true"
58
61
  [enableFiltering]="true"
59
62
  [enablePagination]="true"
@@ -63,6 +66,8 @@ interface User {
63
66
  `,
64
67
  })
65
68
  export class UsersComponent {
69
+ trackById = (_: number, row: User) => row.id;
70
+ editService = new NgbDatagridDefaultEditService<User>();
66
71
  columns = [
67
72
  { field: 'id', header: 'ID', sortable: true },
68
73
  { field: 'name', header: 'Name', sortable: true, filterable: true },
@@ -86,6 +91,8 @@ Key datagrid capabilities:
86
91
  - Column/global filtering (`enableFiltering`, `enableGlobalFilter`, `filtersChange`).
87
92
  - Pagination (`enablePagination`, `pageSize`, `pageChange`).
88
93
  - Inline add/edit/delete (`enableAdd`, `enableEdit`, `enableDelete`, `rowAdd`, `rowSave`, `rowDelete`).
94
+ - Stable row identity via `trackBy` (defaults to index).
95
+ - Pluggable editing logic via `editService` (implement `NgbDatagridEditService`).
89
96
  - Export to PDF/Excel via `exportOptions`.
90
97
 
91
98
  Export requires optional peer dependencies. Install only if you use export:
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Input, Directive, EventEmitter, Output, ChangeDetectionStrategy, Component, inject, Injectable, TemplateRef, QueryList, ContentChildren, ContentChild, InjectionToken, signal, Optional, Inject, ElementRef, HostListener, HostBinding, ViewChild, forwardRef, ViewChildren } from '@angular/core';
2
+ import { Input, Directive, EventEmitter, Output, ChangeDetectionStrategy, Component, inject, Injectable, signal, TemplateRef, QueryList, ContentChildren, ContentChild, InjectionToken, Optional, Inject, ElementRef, HostListener, HostBinding, ViewChild, forwardRef, ViewChildren } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
5
  import * as i2 from '@angular/forms';
@@ -198,6 +198,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
198
198
  args: [{ providedIn: 'root' }]
199
199
  }] });
200
200
 
201
+ const clone = (value) => {
202
+ try {
203
+ // Prefer structuredClone when available.
204
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
205
+ return globalThis.structuredClone ? globalThis.structuredClone(value) : JSON.parse(JSON.stringify(value));
206
+ }
207
+ catch {
208
+ return value;
209
+ }
210
+ };
211
+ const shallowEqual = (a, b) => {
212
+ if (a === b)
213
+ return true;
214
+ if (!a || !b)
215
+ return false;
216
+ const aKeys = Object.keys(a);
217
+ const bKeys = Object.keys(b);
218
+ if (aKeys.length !== bKeys.length)
219
+ return false;
220
+ for (const key of aKeys) {
221
+ if (a[key] !== b[key])
222
+ return false;
223
+ }
224
+ return true;
225
+ };
226
+ class NgbDatagridDefaultEditService {
227
+ originals = signal([], ...(ngDevMode ? [{ debugName: "originals" }] : []));
228
+ newRows = signal([], ...(ngDevMode ? [{ debugName: "newRows" }] : []));
229
+ create(data, newRow, _rowIndex, rowId) {
230
+ this.newRows.update((ids) => (ids.some((id) => Object.is(id, rowId)) ? ids : [...ids, rowId]));
231
+ // Baseline for a new row is itself until saved/cancelled.
232
+ this.originals.update((items) => {
233
+ if (items.some((it) => Object.is(it.id, rowId)))
234
+ return items;
235
+ return [...items, { id: rowId, row: clone(newRow) }];
236
+ });
237
+ return [...data, newRow];
238
+ }
239
+ update(data, updatedRow, rowIndex, rowId) {
240
+ // Snapshot original the first time we see this rowId.
241
+ const hasBaseline = this.originals().some((it) => Object.is(it.id, rowId));
242
+ if (!hasBaseline) {
243
+ const current = data[rowIndex];
244
+ if (current != null) {
245
+ this.originals.update((items) => [...items, { id: rowId, row: clone(current) }]);
246
+ }
247
+ }
248
+ return this.replaceRow(data, updatedRow, rowIndex);
249
+ }
250
+ remove(data, rowIndex, rowId) {
251
+ this.newRows.update((ids) => ids.filter((id) => !Object.is(id, rowId)));
252
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
253
+ return this.removeRow(data, rowIndex);
254
+ }
255
+ assignValues(row, values) {
256
+ return { ...row, ...values };
257
+ }
258
+ isNew(rowId) {
259
+ return this.newRows().some((id) => Object.is(id, rowId));
260
+ }
261
+ hasChanges(rowId, currentRow) {
262
+ const baseline = this.originals().find((it) => Object.is(it.id, rowId))?.row;
263
+ if (!baseline)
264
+ return false;
265
+ return !shallowEqual(baseline, currentRow);
266
+ }
267
+ saveChanges(data, _rowIndex, rowId, currentRow) {
268
+ this.newRows.update((ids) => ids.filter((id) => !Object.is(id, rowId)));
269
+ // Once saved, clear baseline tracking (no longer "dirty").
270
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
271
+ // Data is already updated by `create` or `update` at this point.
272
+ return data.slice();
273
+ }
274
+ cancelChanges(data, rowIndex, rowId) {
275
+ if (this.newRows().some((id) => Object.is(id, rowId))) {
276
+ this.newRows.update((ids) => ids.filter((id) => !Object.is(id, rowId)));
277
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
278
+ return this.removeRow(data, rowIndex);
279
+ }
280
+ const baseline = this.originals().find((it) => Object.is(it.id, rowId))?.row;
281
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
282
+ if (!baseline)
283
+ return data.slice();
284
+ return this.replaceRow(data, baseline, rowIndex);
285
+ }
286
+ replaceRow(data, updatedRow, rowIndex) {
287
+ if (rowIndex < 0 || rowIndex >= data.length)
288
+ return data.slice();
289
+ const copy = data.slice();
290
+ copy[rowIndex] = updatedRow;
291
+ return copy;
292
+ }
293
+ removeRow(data, rowIndex) {
294
+ if (rowIndex < 0 || rowIndex >= data.length)
295
+ return data.slice();
296
+ return data.filter((_, i) => i !== rowIndex);
297
+ }
298
+ }
299
+
201
300
  class JsPdfAdapter {
202
301
  async export({ fileName, columns, rows, options }) {
203
302
  const { jsPDF } = await import('jspdf');
@@ -232,7 +331,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
232
331
  args: [{ selector: '[ngbExportButton]' }]
233
332
  }] });
234
333
 
235
- const EMAIL_RX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
334
+ const MAX_EMAIL_LENGTH = 254;
335
+ const isReasonableEmail = (value) => {
336
+ if (value == null)
337
+ return false;
338
+ const str = String(value);
339
+ if (!str || str.length > MAX_EMAIL_LENGTH)
340
+ return false;
341
+ // Basic, linear-time check: one "@", non-empty local part, and a domain with at least one ".".
342
+ const at = str.indexOf('@');
343
+ if (at <= 0)
344
+ return false;
345
+ if (str.indexOf('@', at + 1) !== -1)
346
+ return false;
347
+ if (at === str.length - 1)
348
+ return false;
349
+ if (/\s/.test(str))
350
+ return false;
351
+ const dot = str.lastIndexOf('.');
352
+ if (dot <= at + 1)
353
+ return false;
354
+ if (dot === str.length - 1)
355
+ return false;
356
+ return true;
357
+ };
236
358
  class Datagrid {
237
359
  /** Column definitions to render */
238
360
  columns = [];
@@ -273,6 +395,8 @@ class Datagrid {
273
395
  };
274
396
  theme = 'bootstrap';
275
397
  responsive = false;
398
+ trackBy;
399
+ editService;
276
400
  // Data hooks for export
277
401
  dataProviderAll; // used when pages='all'
278
402
  dataProviderSelection; // used when pages='selection'
@@ -310,31 +434,14 @@ class Datagrid {
310
434
  saveAttemptedEdit = false;
311
435
  addForm = this.fb.group({});
312
436
  saveAttemptedNew = false;
437
+ addDraftRowId = null;
438
+ defaultEditService = new NgbDatagridDefaultEditService();
313
439
  norm(v) {
314
440
  return (v ?? '').toString().toLowerCase().trim();
315
441
  }
316
442
  keyOf(col) {
317
443
  return col.field;
318
444
  }
319
- passesRowFilters(row) {
320
- // per-column
321
- for (const col of this.columns) {
322
- if (!col.filterable)
323
- continue;
324
- const key = col.field;
325
- const q = this.norm(this.filters[key]);
326
- if (!q)
327
- continue;
328
- const cell = this.norm(row[col.field]);
329
- if (!cell.includes(q))
330
- return false;
331
- }
332
- // global
333
- const g = this.norm(this.globalFilter);
334
- if (!g)
335
- return true;
336
- return this.columns.some(c => this.norm(row[c.field]).includes(g));
337
- }
338
445
  getDefaults() {
339
446
  return typeof this.newRowDefaults === 'function'
340
447
  ? this.newRowDefaults() ?? {}
@@ -388,16 +495,6 @@ class Datagrid {
388
495
  }
389
496
  return this.fb.group(group);
390
497
  }
391
- rowIsValidAgainst(targetErrors) {
392
- for (const col of this.columns) {
393
- if (col.editable === false)
394
- continue;
395
- const key = this.keyOf(col);
396
- if (targetErrors[key])
397
- return false;
398
- }
399
- return true;
400
- }
401
498
  strictEmailValidator() {
402
499
  // simple, pragmatic: local@domain.tld (tld >= 2)
403
500
  const rx = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
@@ -405,6 +502,8 @@ class Datagrid {
405
502
  const v = c.value;
406
503
  if (v === null || v === undefined || v === '')
407
504
  return null; // let "required" handle empties
505
+ if (`${v}`.length > MAX_EMAIL_LENGTH)
506
+ return { email: true };
408
507
  return rx.test(String(v)) ? null : { email: true };
409
508
  };
410
509
  }
@@ -438,18 +537,6 @@ class Datagrid {
438
537
  return true;
439
538
  });
440
539
  }
441
- compare(a, b) {
442
- if (a == null && b == null)
443
- return 0;
444
- if (a == null)
445
- return -1;
446
- if (b == null)
447
- return 1;
448
- if (typeof a === 'string' && typeof b === 'string') {
449
- return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
450
- }
451
- return a < b ? -1 : a > b ? 1 : 0;
452
- }
453
540
  get sorted() {
454
541
  if (!this.enableSorting || !this.sort.active || !this.sort.direction)
455
542
  return this.filtered;
@@ -611,6 +698,12 @@ class Datagrid {
611
698
  if (ch['columns'])
612
699
  this.rebuildFilterForm();
613
700
  }
701
+ getRowId(rowIndex, row) {
702
+ return this.trackBy ? this.trackBy(rowIndex, row) : rowIndex;
703
+ }
704
+ getEditService() {
705
+ return this.editService ?? this.defaultEditService;
706
+ }
614
707
  startAdd() {
615
708
  if (!this.enableAdd || this.addingNew)
616
709
  return;
@@ -619,6 +712,11 @@ class Datagrid {
619
712
  this.addingNew = true;
620
713
  this.addForm = this.buildFormFromRow(); // defaults
621
714
  this.saveAttemptedNew = false;
715
+ // Register a draft row with the edit service so implementations can track "new" state.
716
+ const service = this.getEditService();
717
+ const draft = service.assignValues({}, this.addForm.value);
718
+ this.addDraftRowId = Symbol('ngb-datagrid-new-row');
719
+ service.create(this.data ?? [], draft, this.data.length, this.addDraftRowId);
622
720
  }
623
721
  saveAdd() {
624
722
  if (!this.addingNew || !this.addForm)
@@ -628,18 +726,28 @@ class Datagrid {
628
726
  this.addForm.updateValueAndValidity();
629
727
  if (this.addForm.invalid)
630
728
  return;
631
- const newRow = { ...this.addForm.value };
729
+ const service = this.getEditService();
730
+ const newRow = service.assignValues({}, this.addForm.value);
731
+ const rowIndex = this.data.length;
732
+ const rowId = this.addDraftRowId ?? this.getRowId(rowIndex, newRow);
733
+ service.create(this.data ?? [], newRow, rowIndex, rowId);
734
+ service.saveChanges(this.data ?? [], rowIndex, rowId, newRow);
632
735
  this.rowAdd.emit({ newRow });
633
736
  this.addingNew = false;
634
737
  this.addForm = this.fb.group({});
635
738
  ;
636
739
  this.saveAttemptedNew = false;
740
+ this.addDraftRowId = null;
637
741
  }
638
742
  cancelAdd() {
743
+ if (this.addDraftRowId != null) {
744
+ this.getEditService().cancelChanges(this.data ?? [], this.data.length, this.addDraftRowId);
745
+ }
639
746
  this.addingNew = false;
640
747
  this.addForm = this.fb.group({});
641
748
  ;
642
749
  this.saveAttemptedNew = false;
750
+ this.addDraftRowId = null;
643
751
  }
644
752
  startEdit(i) {
645
753
  if (!this.enableEdit)
@@ -650,6 +758,9 @@ class Datagrid {
650
758
  this.editForm = this.buildFormFromRow(this.paged[i]);
651
759
  this.saveAttemptedEdit = false;
652
760
  const di = this.data.indexOf(row);
761
+ const rowId = this.getRowId(di, this.data[di]);
762
+ // Start tracking baseline for the row (service can snapshot original state).
763
+ this.getEditService().update(this.data ?? [], this.data[di], di, rowId);
653
764
  this.rowEdit.emit({ row: this.data[di], index: di });
654
765
  }
655
766
  saveEdit(i) {
@@ -662,7 +773,11 @@ class Datagrid {
662
773
  return;
663
774
  const di = this.data.indexOf(this.paged[i]);
664
775
  const original = this.data[di];
665
- const updated = { ...original, ...this.editForm.value };
776
+ const rowId = this.getRowId(di, original);
777
+ const service = this.getEditService();
778
+ const updated = service.assignValues(original, this.editForm.value);
779
+ service.update(this.data ?? [], updated, di, rowId);
780
+ service.saveChanges(this.data ?? [], di, rowId, updated);
666
781
  this.rowSave.emit({ original, updated, index: di });
667
782
  this.editingIndex = null;
668
783
  this.editForm = this.fb.group({});
@@ -670,6 +785,8 @@ class Datagrid {
670
785
  }
671
786
  cancelEdit(i) {
672
787
  const di = this.data.indexOf(this.paged[i]);
788
+ const rowId = this.getRowId(di, this.data[di]);
789
+ this.getEditService().cancelChanges(this.data ?? [], di, rowId);
673
790
  this.rowCancel.emit({ row: this.data[di], index: di });
674
791
  this.editingIndex = null;
675
792
  this.editForm = this.fb.group({}); // empty group
@@ -677,6 +794,10 @@ class Datagrid {
677
794
  }
678
795
  // For the "Add row" draft
679
796
  onNewDraftChange(col) {
797
+ if (this.draftNew) {
798
+ const key = this.keyOf(col);
799
+ this.draftNew = this.getEditService().assignValues(this.draftNew, { [key]: this.draftNew[key] });
800
+ }
680
801
  this.validateInto(col, this.draftNew, this.errorsNew);
681
802
  }
682
803
  validateInto(col, targetDraft, targetErrors) {
@@ -692,8 +813,7 @@ class Datagrid {
692
813
  err = 'Required';
693
814
  }
694
815
  // type checks
695
- const EMAIL_RX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
696
- if (!err && col.type === 'email' && val && !EMAIL_RX.test(String(val)))
816
+ if (!err && col.type === 'email' && val && !isReasonableEmail(val))
697
817
  err = 'Invalid email';
698
818
  if (!err && col.type === 'number' && val !== '' && val != null && Number.isNaN(Number(val)))
699
819
  err = 'Invalid number';
@@ -710,10 +830,17 @@ class Datagrid {
710
830
  if (!this.enableDelete)
711
831
  return;
712
832
  const di = this.dataIndexFromPaged(i);
713
- this.rowDelete.emit({ row: this.data[di], index: di });
833
+ const row = this.data[di];
834
+ const rowId = this.getRowId(di, row);
835
+ this.getEditService().remove(this.data ?? [], di, rowId);
836
+ this.rowDelete.emit({ row, index: di });
714
837
  }
715
838
  // (optional) better *ngFor performance
716
- trackRow = (_, row) => row;
839
+ trackRow = (index, row) => {
840
+ const di = this.data.indexOf(row);
841
+ const rowIndex = di >= 0 ? di : index;
842
+ return this.trackBy ? this.trackBy(rowIndex, row) : rowIndex;
843
+ };
717
844
  toggleSort(field) {
718
845
  if (!this.enableSorting)
719
846
  return;
@@ -794,7 +921,7 @@ class Datagrid {
794
921
  return r === true || (!!r && r.enabled === true);
795
922
  }
796
923
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: Datagrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
797
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: Datagrid, isStandalone: true, selector: "ngb-datagrid", inputs: { columns: "columns", data: "data", enableSorting: "enableSorting", enableFiltering: "enableFiltering", enableGlobalFilter: "enableGlobalFilter", enablePagination: "enablePagination", enableEdit: "enableEdit", enableDelete: "enableDelete", pageSizeOptions: "pageSizeOptions", enableAdd: "enableAdd", addButtonAriaLabel: "addButtonAriaLabel", addButtonText: "addButtonText", globalFilterAriaLabel: "globalFilterAriaLabel", expandRowAriaLabel: "expandRowAriaLabel", collapseRowAriaLabel: "collapseRowAriaLabel", exportPdfAriaLabel: "exportPdfAriaLabel", exportExcelAriaLabel: "exportExcelAriaLabel", newRowDefaults: "newRowDefaults", strictEmail: "strictEmail", editOnRowClick: "editOnRowClick", singleExpand: "singleExpand", exportOptions: "exportOptions", theme: "theme", responsive: "responsive", dataProviderAll: "dataProviderAll", dataProviderSelection: "dataProviderSelection", pageSize: "pageSize" }, outputs: { rowAdd: "rowAdd", rowEdit: "rowEdit", rowSave: "rowSave", rowCancel: "rowCancel", rowDelete: "rowDelete", sortChange: "sortChange", filtersChange: "filtersChange", pageChange: "pageChange" }, providers: [
924
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: Datagrid, isStandalone: true, selector: "ngb-datagrid", inputs: { columns: "columns", data: "data", enableSorting: "enableSorting", enableFiltering: "enableFiltering", enableGlobalFilter: "enableGlobalFilter", enablePagination: "enablePagination", enableEdit: "enableEdit", enableDelete: "enableDelete", pageSizeOptions: "pageSizeOptions", enableAdd: "enableAdd", addButtonAriaLabel: "addButtonAriaLabel", addButtonText: "addButtonText", globalFilterAriaLabel: "globalFilterAriaLabel", expandRowAriaLabel: "expandRowAriaLabel", collapseRowAriaLabel: "collapseRowAriaLabel", exportPdfAriaLabel: "exportPdfAriaLabel", exportExcelAriaLabel: "exportExcelAriaLabel", newRowDefaults: "newRowDefaults", strictEmail: "strictEmail", editOnRowClick: "editOnRowClick", singleExpand: "singleExpand", exportOptions: "exportOptions", theme: "theme", responsive: "responsive", trackBy: "trackBy", editService: "editService", dataProviderAll: "dataProviderAll", dataProviderSelection: "dataProviderSelection", pageSize: "pageSize" }, outputs: { rowAdd: "rowAdd", rowEdit: "rowEdit", rowSave: "rowSave", rowCancel: "rowCancel", rowDelete: "rowDelete", sortChange: "sortChange", filtersChange: "filtersChange", pageChange: "pageChange" }, providers: [
798
925
  { provide: PdfExportAdapter, useClass: JsPdfAdapter },
799
926
  { provide: ExcelExportAdapter, useClass: XlsxAdapter }
800
927
  ], queries: [{ propertyName: "exportButtonDir", first: true, predicate: ExportButtonDirective, descendants: true }, { propertyName: "rowDetailTpl", first: true, predicate: NgbRowDetailTemplate, descendants: true }, { propertyName: "cellTplQ", predicate: NgbCellTemplate }, { propertyName: "editTplQ", predicate: NgbEditorTemplate }, { propertyName: "filterTplQ", predicate: NgbFilterTemplate }, { propertyName: "globalTplQ", predicate: NgbGlobalFilterTemplate }], usesOnChanges: true, ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div class=\"ngb-grid\" [attr.data-theme]=\"theme\" [class.ngb-responsive]=\"isResponsiveEnabled()\">\n <!-- Export toolbar -->\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"exportOptions?.enabled\">\n <ng-container *ngIf=\"exportButtonTpl; else defaultExportBtns\"\n [ngTemplateOutlet]=\"exportButtonTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: triggerExport }\">\n </ng-container>\n\n <ng-template #defaultExportBtns>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary me-2\"\n *ngIf=\"exportOptions.type === 'pdf' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('pdf')\"\n [attr.aria-label]=\"exportAriaLabel('pdf')\">\n Export to PDF\n </button>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n *ngIf=\"exportOptions.type === 'excel' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('excel')\"\n [attr.aria-label]=\"exportAriaLabel('excel')\">\n Export to Excel\n </button>\n </ng-template>\n </div>\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"enableAdd\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"startAdd()\"\n [disabled]=\"addingNew\"\n [attr.aria-label]=\"addButtonAriaLabel || addButtonText\"\n >\n {{ addButtonText }}\n </button>\n </div>\n <!-- Global filter -->\n <div class=\"mb-2\" *ngIf=\"enableFiltering && enableGlobalFilter\">\n <ng-container\n *ngIf=\"globalTpl; else defaultGlobalFilter\"\n [ngTemplateOutlet]=\"globalTpl?.template\"\n [ngTemplateOutletContext]=\"{ $implicit: globalFilterCtrl }\"\n >\n </ng-container>\n\n <ng-template #defaultGlobalFilter>\n <input\n type=\"search\"\n class=\"form-control form-control-sm\"\n placeholder=\"Search all columns...\"\n [formControl]=\"globalFilterCtrl\"\n [attr.aria-label]=\"globalFilterAriaLabel\"\n />\n </ng-template>\n </div>\n\n <table class=\"table table-bordered\"\n role=\"grid\"\n [attr.aria-readonly]=\"(enableEdit || enableAdd) ? 'false' : 'true'\">\n <thead class=\"thead-light\">\n <tr>\n <th *ngIf=\"rowDetailTpl\" style=\"width:1%\" scope=\"col\" aria-hidden=\"true\"></th>\n <th\n *ngFor=\"let col of columns\"\n [attr.data-title]=\"col.header\"\n [class.sortable]=\"enableSorting && col.sortable\"\n scope=\"col\"\n [attr.aria-sort]=\"enableSorting && col.sortable ? ariaSortFor(col.field) : null\"\n >\n <ng-container *ngIf=\"enableSorting && col.sortable; else plainHeader\">\n <button\n type=\"button\"\n class=\"btn btn-link p-0 text-start w-100\"\n (click)=\"toggleSort(col.field)\"\n [attr.aria-label]=\"sortButtonAriaLabel(col)\"\n >\n <span>{{ col.header }}</span>\n <span\n class=\"sort-indicator\"\n *ngIf=\"enableSorting && sort.active === col.field\"\n aria-hidden=\"true\"\n >\n {{\n sort.direction === 'asc'\n ? '\u25B2'\n : sort.direction === 'desc'\n ? '\u25BC'\n : ''\n }}\n </span>\n </button>\n </ng-container>\n <ng-template #plainHeader>\n <span>{{ col.header }}</span>\n </ng-template>\n </th>\n <th\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-center\"\n scope=\"col\"\n style=\"width: 1%\"\n >\n Actions\n </th>\n </tr>\n\n <!-- Per-column filters -->\n <tr *ngIf=\"anyFilterable\" [formGroup]=\"filterForm\">\n <th *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container *ngIf=\"col.filterable\">\n <ng-container *ngIf=\"filterTpls[col.field] as ft; else defaultFilter\">\n <ng-container\n [ngTemplateOutlet]=\"ft.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: filterForm.get(col.field),\n control: filterForm.get(col.field),\n col: col\n }\"\n >\n </ng-container>\n </ng-container>\n <ng-template #defaultFilter>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"columnFilterAriaLabel(col)\"\n />\n </ng-template>\n </ng-container>\n </th>\n <th *ngIf=\"enableEdit || enableDelete\"></th>\n </tr>\n </thead>\n\n <tbody>\n <!-- ADD NEW ROW -->\n <tr *ngIf=\"addingNew\" [formGroup]=\"addForm\">\n <!-- blank caret cell to keep alignment when detail column is present -->\n <td *ngIf=\"rowDetailTpl\"></td>\n\n <td *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container [ngSwitch]=\"col.type\">\n <!-- boolean -->\n <div\n *ngSwitchCase=\"'boolean'\"\n class=\"form-check m-0\"\n [class.is-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'add-error-' + col.field : null\"\n />\n </div>\n\n <!-- number/email/date/text -->\n <input\n *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [class.is-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid\"\n [attr.type]=\"col.type === 'number' ? 'number' :\n col.type === 'email' ? 'email' :\n col.type === 'date' ? 'date' : 'text'\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'add-error-' + col.field : null\"\n />\n </ng-container>\n\n <div class=\"invalid-feedback d-block\"\n *ngIf=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.errors as e\"\n [attr.id]=\"'add-error-' + col.field\"\n role=\"alert\">\n <ng-container *ngIf=\"e['required']\">Required</ng-container>\n <ng-container *ngIf=\"e['email']\">Invalid email</ng-container>\n <ng-container *ngIf=\"e['number']\">Invalid number</ng-container>\n <ng-container *ngIf=\"e['date']\">Invalid date</ng-container>\n </div>\n </td>\n\n <td *ngIf=\"enableEdit || enableDelete\" class=\"text-nowrap text-center\">\n <button type=\"button\" class=\"btn btn-sm btn-success me-1\" (click)=\"saveAdd()\" [disabled]=\"addForm.invalid\">Save</button>\n <button type=\"button\" class=\"btn btn-sm btn-secondary\" (click)=\"cancelAdd()\">Cancel</button>\n </td>\n </tr>\n\n <!-- DATA ROWS + DETAIL ROWS -->\n <ng-container *ngFor=\"let row of paged; let i = index; trackBy: trackRow\">\n\n <!-- DATA ROW (click-to-edit supported) -->\n <tr (click)=\"onRowClick($event, i)\" [formGroup]=\"editForm\">\n\n <!-- caret/expander cell (left-most) -->\n <td *ngIf=\"rowDetailTpl\" class=\"text-center align-middle\">\n <button\n type=\"button\"\n class=\"expand btn btn-link p-0\"\n (click)=\"$event.stopPropagation(); toggleExpand(i)\"\n [attr.aria-expanded]=\"isExpanded(i)\"\n [attr.aria-controls]=\"'dg-row-detail-' + i\"\n [attr.aria-label]=\"isExpanded(i) ? collapseRowAriaLabel : expandRowAriaLabel\">\n {{ isExpanded(i) ? '-' : '+' }}\n </button>\n </td>\n\n <!-- DATA CELLS -->\n <td *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <!-- EDIT MODE -->\n <ng-container *ngIf=\"editingIndex === i && (col.editable ?? true); else readCell\">\n <!-- editor template override -->\n <ng-container *ngIf=\"editTpls[col.field] as et; else defaultEditor\"\n [ngTemplateOutlet]=\"et.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: editForm.get(col.field),\n control: editForm.get(col.field),\n row: row, col: col, form: editForm, index: i, isNew: false\n }\">\n </ng-container>\n\n <!-- default editors -->\n <ng-template #defaultEditor>\n <ng-container [ngSwitch]=\"col.type\">\n <div *ngSwitchCase=\"'boolean'\" class=\"form-check m-0\">\n <input type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'edit-error-' + col.field : null\" />\n </div>\n <select *ngSwitchCase=\"'select'\"\n class=\"form-select form-select-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\">\n <option *ngFor=\"let o of col.options ?? []\" [ngValue]=\"o.value\">{{ o.label }}</option>\n </select>\n <input *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [attr.type]=\"col.type === 'number' ? 'number' :\n col.type === 'email' ? 'email' :\n col.type === 'date' ? 'date' : 'text'\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'edit-error-' + col.field : null\" />\n </ng-container>\n\n <div class=\"invalid-feedback d-block\"\n *ngIf=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.errors as e\"\n [attr.id]=\"'edit-error-' + col.field\"\n role=\"alert\">\n <span *ngIf=\"e['required']\">Required</span>\n <span *ngIf=\"e['email']\">Invalid email</span>\n <span *ngIf=\"e['number']\">Invalid number</span>\n <span *ngIf=\"e['date']\">Invalid date</span>\n </div>\n </ng-template>\n </ng-container>\n\n <!-- READ MODE (with optional cell template) -->\n <ng-template #readCell>\n <ng-container *ngIf=\"cellTpls[col.field] as ct; else defaultCell\"\n [ngTemplateOutlet]=\"ct.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row[col.field], row: row, col: col, index: i }\">\n </ng-container>\n <ng-template #defaultCell>\n <ng-container [ngSwitch]=\"col.type\">\n <span *ngSwitchCase=\"'boolean'\">{{ row[col.field] ? 'Yes' : 'No' }}</span>\n <span *ngSwitchDefault>{{ row[col.field] }}</span>\n </ng-container>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td *ngIf=\"enableEdit || enableDelete\" class=\"text-nowrap text-center\">\n <ng-container *ngIf=\"editingIndex !== i; else editBtns\">\n <button type=\"button\" *ngIf=\"enableEdit\" class=\"btn btn-sm btn-outline-primary me-1 no-edit-trigger\" (click)=\"startEdit(i)\">Edit</button>\n <button type=\"button\" *ngIf=\"enableDelete\" class=\"btn btn-sm btn-outline-danger no-edit-trigger\" (click)=\"deleteRow(i)\">Delete</button>\n </ng-container>\n <ng-template #editBtns>\n <button type=\"button\" class=\"btn btn-sm btn-success me-1\" (click)=\"saveEdit(i)\" [disabled]=\"editForm.invalid\">Save</button>\n <button type=\"button\" class=\"btn btn-sm btn-secondary\" (click)=\"cancelEdit(i)\">Cancel</button>\n </ng-template>\n </td>\n </tr>\n\n <!-- DETAIL ROW (spans all columns) -->\n <tr *ngIf=\"rowDetailTpl && isExpanded(i)\" [attr.id]=\"'dg-row-detail-' + i\">\n <td [attr.colspan]=\"detailColspan\" role=\"region\">\n <ng-container [ngTemplateOutlet]=\"rowDetailTpl.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, index: i }\">\n </ng-container>\n </td>\n </tr>\n\n </ng-container>\n </tbody>\n\n </table>\n\n <!-- Footer (left = count, center = pagination, right = page size) -->\n <div *ngIf=\"enablePagination\" class=\"d-flex align-items-center justify-content-between mt-2\">\n <!-- left: count -->\n <div class=\"small text-muted\" aria-live=\"polite\">\n {{ startIndex }} - {{ endIndex }} of {{ sorted.length }}\n\n </div>\n\n <!-- center: pager -->\n <div class=\"flex-grow-1 d-flex justify-content-center\">\n <ngb-pagination\n [page]=\"page\"\n [pageSize]=\"pageSize\"\n [collectionSize]=\"sorted.length\"\n [maxSize]=\"5\"\n (pageChange)=\"onPage($event)\">\n </ngb-pagination>\n </div>\n\n <!-- right: rows per page -->\n <div class=\"d-flex align-items-center gap-2\">\n <label class=\"form-label form-label-sm mb-0\" for=\"pageSize\">Rows:</label>\n <select\n id=\"pageSize\"\n class=\"form-select form-select-sm\"\n [(ngModel)]=\"pageSize\"\n (ngModelChange)=\"onPageSize($event)\"\n aria-label=\"Rows per page\">\n <option *ngFor=\"let s of pageSizeOptions\" [value]=\"s\">{{ s }}</option>\n </select>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.table{margin-bottom:.5rem}th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.sort-indicator{margin-left:.35rem;font-size:.75rem}.expand{cursor:pointer;font-size:24px;line-height:18px;display:block}.ngb-grid[data-theme=material]{--ngb-primary: #3f51b5}.ngb-grid[data-theme=material] .btn{border-color:var(--ngb-primary)}.ngb-grid[data-theme=material] table.table th,.ngb-grid[data-theme=material] table.table td{border-bottom:1px solid #e5e7eb}.ngb-grid[data-theme=tailwind]{--ngb-primary: rgb(29 78 216)}.ngb-grid[data-theme=tailwind] .btn{border-color:var(--ngb-primary);border-radius:.75rem}.ngb-grid[data-theme=tailwind] table.table th,.ngb-grid[data-theme=tailwind] table.table td{border-bottom:1px solid #e5e7eb;padding:.5rem}.ngb-responsive .table{width:100%}@media(max-width:768px){.ngb-responsive thead{display:none}.ngb-responsive tbody tr{display:grid;grid-template-columns:1fr;gap:.5rem;border:1px solid #e5e7eb;border-radius:.75rem;padding:.75rem;margin-bottom:.75rem}.ngb-responsive tbody td{display:grid;grid-template-columns:8rem 1fr}.ngb-responsive tbody td:before{content:attr(data-title);font-weight:600;opacity:.75;padding-right:.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: NgbPaginationComponent, selector: "ngb-pagination", inputs: ["page", "pageSize", "collectionSize", "maxSize"], outputs: ["pageChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -853,6 +980,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
853
980
  type: Input
854
981
  }], responsive: [{
855
982
  type: Input
983
+ }], trackBy: [{
984
+ type: Input
985
+ }], editService: [{
986
+ type: Input
856
987
  }], dataProviderAll: [{
857
988
  type: Input
858
989
  }], dataProviderSelection: [{
@@ -3529,5 +3660,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3529
3660
  * Generated bundle index. Do not edit.
3530
3661
  */
3531
3662
 
3532
- export { DATAGRID_TEMPLATE_DIRECTIVES, DND_I18N, DND_LIVE_ANNOUNCE, Datagrid, ExcelExportAdapter, ExportButtonDirective, JsPdfAdapter, NgbCellTemplate, NgbChipsComponent, NgbDndItemDirective, NgbDndListDirective, NgbDndState, NgbEditorTemplate, NgbExportService, NgbFilterTemplate, NgbGlobalFilterTemplate, NgbLiveAnnouncer, NgbPaginationComponent, NgbRowDetailTemplate, NgbSplitterComponent, NgbSplitterPaneComponent, NgbStepLabelDirective, NgbStepperComponent, NgbTreeComponent, NgbTypeaheadComponent, PdfExportAdapter, XlsxAdapter, defaultDndI18n };
3663
+ export { DATAGRID_TEMPLATE_DIRECTIVES, DND_I18N, DND_LIVE_ANNOUNCE, Datagrid, ExcelExportAdapter, ExportButtonDirective, JsPdfAdapter, NgbCellTemplate, NgbChipsComponent, NgbDatagridDefaultEditService, NgbDndItemDirective, NgbDndListDirective, NgbDndState, NgbEditorTemplate, NgbExportService, NgbFilterTemplate, NgbGlobalFilterTemplate, NgbLiveAnnouncer, NgbPaginationComponent, NgbRowDetailTemplate, NgbSplitterComponent, NgbSplitterPaneComponent, NgbStepLabelDirective, NgbStepperComponent, NgbTreeComponent, NgbTypeaheadComponent, PdfExportAdapter, XlsxAdapter, defaultDndI18n };
3533
3664
  //# sourceMappingURL=angular-bootstrap-ngbootstrap.mjs.map