@onemrvapublic/design-system-demos 22.4.0 → 22.4.1-develop.2
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/browser/assets/codes.json +1 -1
- package/browser/assets/demos-manifest.json +1 -1
- package/browser/assets/json/chips-input.json +1 -1
- package/browser/assets/json/table-entrypoint.json +1 -0
- package/browser/assets/json/table.json +1 -1
- package/browser/main.js +7 -6
- package/browser/styles.css +1 -1
- package/package.json +1 -1
- package/browser/assets/json/card-table.json +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
["address","address-input","autocomplete","avatar","badge","bank-input","bce-pipe","birthplace-input","breadcrumb","button","button-loading","button-toggle","card","
|
|
1
|
+
["address","address-input","autocomplete","avatar","badge","bank-input","bce-pipe","birthplace-input","breadcrumb","button","button-loading","button-toggle","card","checkbox","chips","chips-input","choice-chips","collapsible-page-divider","copy-to-clipboard","country-input","datepicker","datepicker-luxon","datepicker-month-year","daterangepicker","dialog","dialog-content","digit-only","drag-and-drop","drawer","empty","enterprise-number-input","error","error-handler","fab","file-panel","file-upload","file-upload-manual","form","grid","horizontal-stepper","icon","icon-button","if-width-is","jsonform","language-switcher","layout","link","mask","menu","message-box","multiselect","niss-pipe","not-found","notification","overlay","paginator","palette","panel","phone-input","pop-over","progress-bar","radio","rxjs-combine-operators","rxjs-complex-case","rxjs-flattening-operators","rxjs-observable","rxjs-subject","select","selectable-box","selectable-box-large","side-menu","skeleton","slide-toggle","spacing","spinner","sticker","summary-stepper","tab","table","table-dialog-content","table-entrypoint","task-list","text-input","textarea","timeline","timepicker","toast","toc","toolbar","tooltip","typography","validators"]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
[{"slug":"address","name":"Address","description":"","thumbnail":"","permalink":"/demos/address"},{"slug":"address-input","name":"Address Input","description":"","thumbnail":"","permalink":"/demos/address-input"},{"slug":"autocomplete","name":"Autocomplete","description":"","thumbnail":"","permalink":"/demos/autocomplete"},{"slug":"avatar","name":"Avatar","description":"","thumbnail":"","permalink":"/demos/avatar"},{"slug":"badge","name":"Badge","description":"","thumbnail":"","permalink":"/demos/badge"},{"slug":"bank-input","name":"Bank Input","description":"","thumbnail":"","permalink":"/demos/bank-input"},{"slug":"bce-pipe","name":"BCE pipe","description":"","thumbnail":"","permalink":"/demos/bce-pipe"},{"slug":"birthplace-input","name":"Birthplace input","description":"","thumbnail":"","permalink":"/demos/birthplace-input"},{"slug":"breadcrumb","name":"Breadcrumb","description":"","thumbnail":"","permalink":"/demos/breadcrumb"},{"slug":"button","name":"Button","description":"","thumbnail":"","permalink":"/demos/button"},{"slug":"button-loading","name":"Button Loading","description":"","thumbnail":"","permalink":"/demos/button-loading"},{"slug":"button-toggle","name":"Button Toggle","description":"","thumbnail":"","permalink":"/demos/button-toggle"},{"slug":"card","name":"Card","description":"Content card with an optional image, label, title, subtitle, description and help tooltip. The card is not clickable as a whole: it can hold buttons, so a card-wide click target would nest interactive elements. Put the action in the cardActions slot instead.","thumbnail":"","permalink":"/demos/card"},{"slug":"card-table","name":"Card Table","description":"","thumbnail":"","permalink":"/demos/card-table"},{"slug":"checkbox","name":"Checkbox","description":"","thumbnail":"","permalink":"/demos/checkbox"},{"slug":"chips","name":"Chips","description":"","thumbnail":"","permalink":"/demos/chips"},{"slug":"chips-input","name":"Chips Input","description":"","thumbnail":"","permalink":"/demos/chips-input"},{"slug":"choice-chips","name":"Choice chips","description":"","thumbnail":"","permalink":"/demos/choice-chips"},{"slug":"collapsible-page-divider","name":"Collapsible Page Divider","description":"","thumbnail":"","permalink":"/demos/collapsible-page-divider"},{"slug":"copy-to-clipboard","name":"Copy to clipboard","description":"","thumbnail":"","permalink":"/demos/copy-to-clipboard"},{"slug":"country-input","name":"Country Input","description":"","thumbnail":"","permalink":"/demos/country-input"},{"slug":"datepicker","name":"Date Picker","description":"","thumbnail":"","permalink":"/demos/datepicker"},{"slug":"datepicker-luxon","name":"Date Picker Luxon","description":"","thumbnail":"","permalink":"/demos/datepicker-luxon"},{"slug":"datepicker-month-year","name":"Date Picker Month Year ","description":"","thumbnail":"","permalink":"/demos/datepicker-month-year"},{"slug":"daterangepicker","name":"Date Range Picker","description":"","thumbnail":"","permalink":"/demos/daterangepicker"},{"slug":"dialog","name":"Dialog","description":"","thumbnail":"","permalink":"/demos/dialog"},{"slug":"dialog-content","name":"Dialog Content","description":"","thumbnail":"","permalink":"/demos/dialog-content"},{"slug":"digit-only","name":"Digit Only directive","description":"","thumbnail":"","permalink":"/demos/digit-only"},{"slug":"drag-and-drop","name":"Drag and drop","description":"","thumbnail":"","permalink":"/demos/drag-and-drop"},{"slug":"drawer","name":"Drawer","description":"","thumbnail":"","permalink":"/demos/drawer"},{"slug":"empty","name":"Empty","description":"","thumbnail":"","permalink":"/demos/empty"},{"slug":"enterprise-number-input","name":"Enterprise number (VIES)","description":"","thumbnail":"","permalink":"/demos/enterprise-number-input"},{"slug":"error","name":"Page Error","description":"","thumbnail":"","permalink":"/demos/error"},{"slug":"error-handler","name":"Error Handler","description":"","thumbnail":"","permalink":"/demos/error-handler"},{"slug":"fab","name":"FAB","description":"","thumbnail":"","permalink":"/demos/fab"},{"slug":"file-panel","name":"File Panel","description":"","thumbnail":"","permalink":"/demos/file-panel"},{"slug":"file-upload","name":"File Upload","description":"","thumbnail":"","permalink":"/demos/file-upload"},{"slug":"file-upload-manual","name":"File Upload Manual","description":"","thumbnail":"","permalink":"/demos/file-upload-manual"},{"slug":"form","name":"Form","description":"","thumbnail":"","permalink":"/demos/form"},{"slug":"grid","name":"Grid","description":"","thumbnail":"","permalink":"/demos/grid"},{"slug":"horizontal-stepper","name":"Stepper (horizontal)","description":"","thumbnail":"","permalink":"/demos/horizontal-stepper"},{"slug":"icon","name":"Icon","description":"","thumbnail":"","permalink":"/demos/icon"},{"slug":"icon-button","name":"Icon Button","description":"","thumbnail":"","permalink":"/demos/icon-button"},{"slug":"if-width-is","name":"If width is","description":"","thumbnail":"","permalink":"/demos/if-width-is"},{"slug":"jsonform","name":"JSON Forms","description":"","thumbnail":"","permalink":"/demos/jsonform"},{"slug":"language-switcher","name":"Language switcher","description":"","thumbnail":"","permalink":"/demos/language-switcher"},{"slug":"layout","name":"Layout and drawer","description":"","thumbnail":"","permalink":"/demos/layout"},{"slug":"link","name":"Link","description":"","thumbnail":"","permalink":"/demos/link"},{"slug":"mask","name":"Mask","description":"","thumbnail":"","permalink":"/demos/mask"},{"slug":"menu","name":"Menu","description":"","thumbnail":"","permalink":"/demos/menu"},{"slug":"message-box","name":"Message box","description":"","thumbnail":"","permalink":"/demos/message-box"},{"slug":"multiselect","name":"Multi Select","description":"","thumbnail":"","permalink":"/demos/multiselect"},{"slug":"niss-pipe","name":"NISS pipe","description":"","thumbnail":"","permalink":"/demos/niss-pipe"},{"slug":"not-found","name":"Page not found","description":"","thumbnail":"","permalink":"/demos/not-found"},{"slug":"notification","name":"Notification","description":"","thumbnail":"","permalink":"/demos/notification"},{"slug":"overlay","name":"Overlay","description":"","thumbnail":"","permalink":"/demos/overlay"},{"slug":"paginator","name":"Paginator","description":"","thumbnail":"","permalink":"/demos/paginator"},{"slug":"palette","name":"Palette","description":"","thumbnail":"","permalink":"/demos/palette"},{"slug":"panel","name":"Panel","description":"","thumbnail":"","permalink":"/demos/panel"},{"slug":"phone-input","name":"Phone Input","description":"","thumbnail":"","permalink":"/demos/phone-input"},{"slug":"pop-over","name":"PopOver","description":"","thumbnail":"","permalink":"/demos/pop-over"},{"slug":"progress-bar","name":"ProgressBar","description":"","thumbnail":"","permalink":"/demos/progress-bar"},{"slug":"radio","name":"Radio","description":"","thumbnail":"","permalink":"/demos/radio"},{"slug":"rxjs-combine-operators","name":"Rxjs Combine Operators","description":"","thumbnail":"","permalink":"/demos/rxjs-combine-operators"},{"slug":"rxjs-complex-case","name":"Rxjs Complex case","description":"","thumbnail":"","permalink":"/demos/rxjs-complex-case"},{"slug":"rxjs-flattening-operators","name":"Rxjs Flattening Operators","description":"","thumbnail":"","permalink":"/demos/rxjs-flattening-operators"},{"slug":"rxjs-observable","name":"Rxjs Observable","description":"","thumbnail":"","permalink":"/demos/rxjs-observable"},{"slug":"rxjs-subject","name":"Rxjs Subject","description":"","thumbnail":"","permalink":"/demos/rxjs-subject"},{"slug":"select","name":"Select","description":"","thumbnail":"","permalink":"/demos/select"},{"slug":"selectable-box","name":"Selectable box","description":"","thumbnail":"","permalink":"/demos/selectable-box"},{"slug":"selectable-box-large","name":"Selectable box Large","description":"","thumbnail":"","permalink":"/demos/selectable-box-large"},{"slug":"side-menu","name":"Side menu","description":"","thumbnail":"","permalink":"/demos/side-menu"},{"slug":"skeleton","name":"Skeleton","description":"","thumbnail":"","permalink":"/demos/skeleton"},{"slug":"slide-toggle","name":"Slide toggle","description":"","thumbnail":"","permalink":"/demos/slide-toggle"},{"slug":"spacing","name":"Spacing","description":"","thumbnail":"","permalink":"/demos/spacing"},{"slug":"spinner","name":"Spinner","description":"","thumbnail":"","permalink":"/demos/spinner"},{"slug":"sticker","name":"Sticker","description":"","thumbnail":"","permalink":"/demos/sticker"},{"slug":"summary-stepper","name":"Stepper (with Summary)","description":"","thumbnail":"","permalink":"/demos/summary-stepper"},{"slug":"tab","name":"Tab","description":"","thumbnail":"","permalink":"/demos/tab"},{"slug":"table","name":"Table","description":"","thumbnail":"","permalink":"/demos/table"},{"slug":"table-dialog-content","name":"Table Dialog Content","description":"","thumbnail":"","permalink":"/demos/table-dialog-content"},{"slug":"task-list","name":"Task list","description":"","thumbnail":"","permalink":"/demos/task-list"},{"slug":"text-input","name":"Text Input","description":"","thumbnail":"","permalink":"/demos/text-input"},{"slug":"textarea","name":"Textarea","description":"","thumbnail":"","permalink":"/demos/textarea"},{"slug":"timeline","name":"Timeline","description":"Plot dated periods on a horizontal, year-based timeline. Overlapping periods stack into rows and take their color from the legend, with month/day zoom, selection, tooltips, filters, and an optional full-size mode.","thumbnail":"assets/thumbnails/timeline-thumbnail.png","permalink":"/demos/timeline"},{"slug":"timepicker","name":"Timepicker","description":"","thumbnail":"","permalink":"/demos/timepicker"},{"slug":"toast","name":"Toast","description":"","thumbnail":"","permalink":"/demos/toast"},{"slug":"toc","name":"Table of content","description":"","thumbnail":"","permalink":"/demos/toc"},{"slug":"toolbar","name":"Toolbar","description":"","thumbnail":"","permalink":"/demos/toolbar"},{"slug":"tooltip","name":"Tooltip","description":"","thumbnail":"","permalink":"/demos/tooltip"},{"slug":"typography","name":"Typography","description":"","thumbnail":"","permalink":"/demos/typography"},{"slug":"validators","name":"Validators","description":"","thumbnail":"","permalink":"/demos/validators"}]
|
|
1
|
+
[{"slug":"address","name":"Address","description":"","thumbnail":"","permalink":"/demos/address"},{"slug":"address-input","name":"Address Input","description":"","thumbnail":"","permalink":"/demos/address-input"},{"slug":"autocomplete","name":"Autocomplete","description":"","thumbnail":"","permalink":"/demos/autocomplete"},{"slug":"avatar","name":"Avatar","description":"","thumbnail":"","permalink":"/demos/avatar"},{"slug":"badge","name":"Badge","description":"","thumbnail":"","permalink":"/demos/badge"},{"slug":"bank-input","name":"Bank Input","description":"","thumbnail":"","permalink":"/demos/bank-input"},{"slug":"bce-pipe","name":"BCE pipe","description":"","thumbnail":"","permalink":"/demos/bce-pipe"},{"slug":"birthplace-input","name":"Birthplace input","description":"","thumbnail":"","permalink":"/demos/birthplace-input"},{"slug":"breadcrumb","name":"Breadcrumb","description":"","thumbnail":"","permalink":"/demos/breadcrumb"},{"slug":"button","name":"Button","description":"","thumbnail":"","permalink":"/demos/button"},{"slug":"button-loading","name":"Button Loading","description":"","thumbnail":"","permalink":"/demos/button-loading"},{"slug":"button-toggle","name":"Button Toggle","description":"","thumbnail":"","permalink":"/demos/button-toggle"},{"slug":"card","name":"Card","description":"Content card with an optional image, label, title, subtitle, description and help tooltip. The card is not clickable as a whole: it can hold buttons, so a card-wide click target would nest interactive elements. Put the action in the cardActions slot instead.","thumbnail":"","permalink":"/demos/card"},{"slug":"checkbox","name":"Checkbox","description":"","thumbnail":"","permalink":"/demos/checkbox"},{"slug":"chips","name":"Chips","description":"","thumbnail":"","permalink":"/demos/chips"},{"slug":"chips-input","name":"Chips Input","description":"","thumbnail":"","permalink":"/demos/chips-input"},{"slug":"choice-chips","name":"Choice chips","description":"","thumbnail":"","permalink":"/demos/choice-chips"},{"slug":"collapsible-page-divider","name":"Collapsible Page Divider","description":"","thumbnail":"","permalink":"/demos/collapsible-page-divider"},{"slug":"copy-to-clipboard","name":"Copy to clipboard","description":"","thumbnail":"","permalink":"/demos/copy-to-clipboard"},{"slug":"country-input","name":"Country Input","description":"","thumbnail":"","permalink":"/demos/country-input"},{"slug":"datepicker","name":"Date Picker","description":"","thumbnail":"","permalink":"/demos/datepicker"},{"slug":"datepicker-luxon","name":"Date Picker Luxon","description":"","thumbnail":"","permalink":"/demos/datepicker-luxon"},{"slug":"datepicker-month-year","name":"Date Picker Month Year ","description":"","thumbnail":"","permalink":"/demos/datepicker-month-year"},{"slug":"daterangepicker","name":"Date Range Picker","description":"","thumbnail":"","permalink":"/demos/daterangepicker"},{"slug":"dialog","name":"Dialog","description":"","thumbnail":"","permalink":"/demos/dialog"},{"slug":"dialog-content","name":"Dialog Content","description":"","thumbnail":"","permalink":"/demos/dialog-content"},{"slug":"digit-only","name":"Digit Only directive","description":"","thumbnail":"","permalink":"/demos/digit-only"},{"slug":"drag-and-drop","name":"Drag and drop","description":"","thumbnail":"","permalink":"/demos/drag-and-drop"},{"slug":"drawer","name":"Drawer","description":"","thumbnail":"","permalink":"/demos/drawer"},{"slug":"empty","name":"Empty","description":"","thumbnail":"","permalink":"/demos/empty"},{"slug":"enterprise-number-input","name":"Enterprise number (VIES)","description":"","thumbnail":"","permalink":"/demos/enterprise-number-input"},{"slug":"error","name":"Page Error","description":"","thumbnail":"","permalink":"/demos/error"},{"slug":"error-handler","name":"Error Handler","description":"","thumbnail":"","permalink":"/demos/error-handler"},{"slug":"fab","name":"FAB","description":"","thumbnail":"","permalink":"/demos/fab"},{"slug":"file-panel","name":"File Panel","description":"","thumbnail":"","permalink":"/demos/file-panel"},{"slug":"file-upload","name":"File Upload","description":"","thumbnail":"","permalink":"/demos/file-upload"},{"slug":"file-upload-manual","name":"File Upload Manual","description":"","thumbnail":"","permalink":"/demos/file-upload-manual"},{"slug":"form","name":"Form","description":"","thumbnail":"","permalink":"/demos/form"},{"slug":"grid","name":"Grid","description":"","thumbnail":"","permalink":"/demos/grid"},{"slug":"horizontal-stepper","name":"Stepper (horizontal)","description":"","thumbnail":"","permalink":"/demos/horizontal-stepper"},{"slug":"icon","name":"Icon","description":"","thumbnail":"","permalink":"/demos/icon"},{"slug":"icon-button","name":"Icon Button","description":"","thumbnail":"","permalink":"/demos/icon-button"},{"slug":"if-width-is","name":"If width is","description":"","thumbnail":"","permalink":"/demos/if-width-is"},{"slug":"jsonform","name":"JSON Forms","description":"","thumbnail":"","permalink":"/demos/jsonform"},{"slug":"language-switcher","name":"Language switcher","description":"","thumbnail":"","permalink":"/demos/language-switcher"},{"slug":"layout","name":"Layout and drawer","description":"","thumbnail":"","permalink":"/demos/layout"},{"slug":"link","name":"Link","description":"","thumbnail":"","permalink":"/demos/link"},{"slug":"mask","name":"Mask","description":"","thumbnail":"","permalink":"/demos/mask"},{"slug":"menu","name":"Menu","description":"","thumbnail":"","permalink":"/demos/menu"},{"slug":"message-box","name":"Message box","description":"","thumbnail":"","permalink":"/demos/message-box"},{"slug":"multiselect","name":"Multi Select","description":"","thumbnail":"","permalink":"/demos/multiselect"},{"slug":"niss-pipe","name":"NISS pipe","description":"","thumbnail":"","permalink":"/demos/niss-pipe"},{"slug":"not-found","name":"Page not found","description":"","thumbnail":"","permalink":"/demos/not-found"},{"slug":"notification","name":"Notification","description":"","thumbnail":"","permalink":"/demos/notification"},{"slug":"overlay","name":"Overlay","description":"","thumbnail":"","permalink":"/demos/overlay"},{"slug":"paginator","name":"Paginator","description":"","thumbnail":"","permalink":"/demos/paginator"},{"slug":"palette","name":"Palette","description":"","thumbnail":"","permalink":"/demos/palette"},{"slug":"panel","name":"Panel","description":"","thumbnail":"","permalink":"/demos/panel"},{"slug":"phone-input","name":"Phone Input","description":"","thumbnail":"","permalink":"/demos/phone-input"},{"slug":"pop-over","name":"PopOver","description":"","thumbnail":"","permalink":"/demos/pop-over"},{"slug":"progress-bar","name":"ProgressBar","description":"","thumbnail":"","permalink":"/demos/progress-bar"},{"slug":"radio","name":"Radio","description":"","thumbnail":"","permalink":"/demos/radio"},{"slug":"rxjs-combine-operators","name":"Rxjs Combine Operators","description":"","thumbnail":"","permalink":"/demos/rxjs-combine-operators"},{"slug":"rxjs-complex-case","name":"Rxjs Complex case","description":"","thumbnail":"","permalink":"/demos/rxjs-complex-case"},{"slug":"rxjs-flattening-operators","name":"Rxjs Flattening Operators","description":"","thumbnail":"","permalink":"/demos/rxjs-flattening-operators"},{"slug":"rxjs-observable","name":"Rxjs Observable","description":"","thumbnail":"","permalink":"/demos/rxjs-observable"},{"slug":"rxjs-subject","name":"Rxjs Subject","description":"","thumbnail":"","permalink":"/demos/rxjs-subject"},{"slug":"select","name":"Select","description":"","thumbnail":"","permalink":"/demos/select"},{"slug":"selectable-box","name":"Selectable box","description":"","thumbnail":"","permalink":"/demos/selectable-box"},{"slug":"selectable-box-large","name":"Selectable box Large","description":"","thumbnail":"","permalink":"/demos/selectable-box-large"},{"slug":"side-menu","name":"Side menu","description":"","thumbnail":"","permalink":"/demos/side-menu"},{"slug":"skeleton","name":"Skeleton","description":"","thumbnail":"","permalink":"/demos/skeleton"},{"slug":"slide-toggle","name":"Slide toggle","description":"","thumbnail":"","permalink":"/demos/slide-toggle"},{"slug":"spacing","name":"Spacing","description":"","thumbnail":"","permalink":"/demos/spacing"},{"slug":"spinner","name":"Spinner","description":"","thumbnail":"","permalink":"/demos/spinner"},{"slug":"sticker","name":"Sticker","description":"","thumbnail":"","permalink":"/demos/sticker"},{"slug":"summary-stepper","name":"Stepper (with Summary)","description":"","thumbnail":"","permalink":"/demos/summary-stepper"},{"slug":"tab","name":"Tab","description":"","thumbnail":"","permalink":"/demos/tab"},{"slug":"table","name":"Table","description":"","thumbnail":"","permalink":"/demos/table"},{"slug":"table-dialog-content","name":"Table Dialog Content","description":"","thumbnail":"","permalink":"/demos/table-dialog-content"},{"slug":"table-entrypoint","name":"Table Entrypoint","description":"","thumbnail":"","permalink":"/demos/table-entrypoint"},{"slug":"task-list","name":"Task list","description":"","thumbnail":"","permalink":"/demos/task-list"},{"slug":"text-input","name":"Text Input","description":"","thumbnail":"","permalink":"/demos/text-input"},{"slug":"textarea","name":"Textarea","description":"","thumbnail":"","permalink":"/demos/textarea"},{"slug":"timeline","name":"Timeline","description":"Plot dated periods on a horizontal, year-based timeline. Overlapping periods stack into rows and take their color from the legend, with month/day zoom, selection, tooltips, filters, and an optional full-size mode.","thumbnail":"assets/thumbnails/timeline-thumbnail.png","permalink":"/demos/timeline"},{"slug":"timepicker","name":"Timepicker","description":"","thumbnail":"","permalink":"/demos/timepicker"},{"slug":"toast","name":"Toast","description":"","thumbnail":"","permalink":"/demos/toast"},{"slug":"toc","name":"Table of content","description":"","thumbnail":"","permalink":"/demos/toc"},{"slug":"toolbar","name":"Toolbar","description":"","thumbnail":"","permalink":"/demos/toolbar"},{"slug":"tooltip","name":"Tooltip","description":"","thumbnail":"","permalink":"/demos/tooltip"},{"slug":"typography","name":"Typography","description":"","thumbnail":"","permalink":"/demos/typography"},{"slug":"validators","name":"Validators","description":"","thumbnail":"","permalink":"/demos/validators"}]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"demo-chips-input.component.html":"<mat-form-field class=\"chips-input-field\" [readonly]=\"readonly()\">\n <mat-label>Favorite Fruits</mat-label>\n <mat-chip-grid #chipGrid aria-label=\"Fruit selection\">\n @for (fruit of fruits(); track fruit) {\n <mat-chip-row\n [color]=\"color()\"\n [disabled]=\"disabled()\"\n [removable]=\"!disabled()\"\n (removed)=\"remove(fruit)\"\n aria-label=\"Fruit: {{ fruit }}\"\n >\n {{ fruit }}\n @if (!disabled() && !readonly()) {\n <button matChipRemove [attr.aria-label]=\"'remove ' + fruit\">\n <mat-icon>
|
|
1
|
+
{"demo-chips-input.component.html":"<mat-form-field class=\"chips-input-field\" [readonly]=\"readonly()\">\n <mat-label>Favorite Fruits</mat-label>\n <mat-chip-grid #chipGrid aria-label=\"Fruit selection\">\n @for (fruit of fruits(); track fruit) {\n <mat-chip-row\n [color]=\"color()\"\n [disabled]=\"disabled()\"\n [removable]=\"!disabled()\"\n (removed)=\"remove(fruit)\"\n aria-label=\"Fruit: {{ fruit }}\"\n >\n {{ fruit }}\n @if (!disabled() && !readonly()) {\n <button matChipRemove [attr.aria-label]=\"'remove ' + fruit\">\n <mat-icon>close</mat-icon>\n </button>\n }\n </mat-chip-row>\n }\n </mat-chip-grid>\n\n <input\n #fruitInput\n placeholder=\"Add fruit...\"\n [(ngModel)]=\"currentFruit\"\n [matChipInputFor]=\"chipGrid\"\n [matAutocomplete]=\"auto\"\n [matChipInputSeparatorKeyCodes]=\"separatorKeysCodes\"\n (matChipInputTokenEnd)=\"add($event)\"\n [disabled]=\"disabled()\"\n />\n\n <mat-autocomplete #auto=\"matAutocomplete\" (optionSelected)=\"selected($event)\">\n @for (fruit of filteredFruits(); track fruit) {\n <mat-option [value]=\"fruit\">{{ fruit }}</mat-option>\n }\n </mat-autocomplete>\n</mat-form-field>\n","demo-chips-input.component.ts":"import {\n Component,\n computed,\n inject,\n input,\n model,\n signal,\n ViewEncapsulation,\n} from '@angular/core';\nimport { MatChipInputEvent, MatChipsModule } from '@angular/material/chips';\nimport { DragDropModule } from '@angular/cdk/drag-drop';\n\nimport { FormsModule } from '@angular/forms';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { DemoComponentBase } from '../../_demo/demo-component-base';\nimport { OnemrvaMatColor } from '@onemrvapublic/design-system/utils';\nimport { OnemRvaReadonlyDirective } from '@onemrvapublic/design-system/shared';\nimport {\n MatAutocomplete,\n MatAutocompleteSelectedEvent,\n MatAutocompleteTrigger,\n MatOption,\n} from '@angular/material/autocomplete';\nimport { LiveAnnouncer } from '@angular/cdk/a11y';\nimport { COMMA, ENTER } from '@angular/cdk/keycodes';\n\nexport interface Fruit {\n name: string;\n}\n@Component({\n selector: 'app-demo-chips-input',\n standalone: true,\n imports: [\n FormsModule,\n MatChipsModule,\n DragDropModule,\n MatIconModule,\n MatFormFieldModule,\n OnemRvaReadonlyDirective,\n MatAutocomplete,\n MatOption,\n MatAutocompleteTrigger,\n ],\n templateUrl: './demo-chips-input.component.html',\n\n encapsulation: ViewEncapsulation.None,\n})\nexport class DemoChipsInputComponent extends DemoComponentBase {\n color = input<OnemrvaMatColor>('');\n disabled = input(false);\n readonly = input(false);\n\n readonly separatorKeysCodes: number[] = [ENTER, COMMA];\n readonly filteredFruits = computed(() => {\n const currentFruit = this.currentFruit().toLowerCase();\n return currentFruit\n ? this.allFruits.filter(fruit =>\n fruit.toLowerCase().includes(currentFruit),\n )\n : this.allFruits.slice();\n });\n readonly currentFruit = model('');\n readonly announcer = inject(LiveAnnouncer);\n readonly fruits = signal(['Lemon']);\n readonly allFruits: string[] = [\n 'Apple',\n 'Lemon',\n 'Lime',\n 'Orange',\n 'Strawberry',\n ];\n\n add(event: MatChipInputEvent): void {\n const value = (event.value || '').trim();\n\n // Add our fruit\n if (value) {\n this.fruits.update(fruits => [...fruits, value]);\n }\n\n // Clear the input value\n this.currentFruit.set('');\n }\n\n remove(fruit: string): void {\n this.fruits.update(fruits => {\n const index = fruits.indexOf(fruit);\n if (index < 0) {\n return fruits;\n }\n\n fruits.splice(index, 1);\n this.announcer.announce(`Removed ${fruit}`);\n return [...fruits];\n });\n }\n\n selected(event: MatAutocompleteSelectedEvent): void {\n this.fruits.update(fruits => [...fruits, event.option.viewValue]);\n this.currentFruit.set('');\n event.option.deselect();\n }\n}\n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"demo-table-entrypoint-datasource.ts":"import { DataSource } from '@angular/cdk/collections';\nimport { MatSort } from '@angular/material/sort';\nimport { map } from 'rxjs/operators';\nimport { BehaviorSubject, merge, Observable } from 'rxjs';\nimport { OnemrvaMatColor } from '@onemrvapublic/design-system/utils';\nimport { OnemrvaMatPaginatorComponent } from '@onemrvapublic/design-system/mat-paginator';\n\n// TODO: Replace this with your own data model type\nexport interface ListItem {\n id: number;\n name: string;\n description: string;\n status: 'success' | 'error' | 'warn';\n color: OnemrvaMatColor;\n}\n\n// TODO: replace this with real data from your application\nconst EXAMPLE_DATA: ListItem[] = [\n {\n id: 1,\n name: 'Hydrogen',\n description:\n 'Hydrogen is the chemical element with the symbol H and atomic number 1',\n status: 'success',\n color: 'primary',\n },\n {\n id: 2,\n name: 'Helium',\n description:\n \"Helium (from Greek: ἥλιος, romanized: helios, lit. 'sun') is a chemical element with the symbol He and atomic number 2\",\n status: 'success',\n color: '',\n },\n {\n id: 3,\n name: 'Lithium',\n description:\n \"Lithium (from Greek: λίθος, romanized: lithos, lit. 'stone') is a chemical element with the symbol Li and atomic number 3\",\n status: 'error',\n color: 'error',\n },\n {\n id: 4,\n name: 'Beryllium',\n description:\n 'Beryllium is a chemical element with the symbol Be and atomic number 4',\n status: 'success',\n color: 'success',\n },\n {\n id: 5,\n name: 'Boron',\n description:\n 'Boron is a chemical element with the symbol B and atomic number 5',\n status: 'success',\n color: '',\n },\n {\n id: 6,\n name: 'Carbon',\n description:\n 'Carbon (from Latin: carbo \"coal\") is a chemical element with the symbol C and atomic number 6',\n status: 'warn',\n color: 'warn',\n },\n {\n id: 7,\n name: 'Nitrogen',\n description:\n 'Nitrogen is the chemical element with the symbol N and atomic number 7',\n status: 'success',\n color: '',\n },\n {\n id: 8,\n name: 'Oxygen',\n description:\n 'Oxygen is the chemical element with the symbol O and atomic number 8',\n status: 'success',\n color: '',\n },\n {\n id: 9,\n name: 'Fluorine',\n description:\n 'Fluorine is a chemical element with the symbol F and atomic number 9',\n status: 'success',\n color: '',\n },\n {\n id: 10,\n name: 'Neon',\n description:\n 'Neon is a chemical element with the symbol Ne and atomic number 10',\n status: 'success',\n color: '',\n },\n {\n id: 11,\n name: 'Sodium',\n description:\n 'Sodium is a chemical element with the symbol Na (from Latin natrium) and atomic number 11',\n status: 'success',\n color: '',\n },\n { id: 12, name: 'Magnesium', description: '', status: 'success', color: '' },\n { id: 13, name: 'Aluminum', description: '', status: 'success', color: '' },\n { id: 14, name: 'Silicon', description: '', status: 'error', color: '' },\n { id: 15, name: 'Phosphorus', description: '', status: 'success', color: '' },\n { id: 16, name: 'Sulfur', description: '', status: 'success', color: '' },\n { id: 17, name: 'Chlorine', description: '', status: 'warn', color: '' },\n { id: 18, name: 'Argon', description: '', status: 'success', color: '' },\n { id: 19, name: 'Potassium', description: '', status: 'success', color: '' },\n { id: 20, name: 'Calcium', description: '', status: 'success', color: '' },\n { id: 21, name: 'Scandium', description: '', status: 'success', color: '' },\n { id: 22, name: 'Titanium', description: '', status: 'success', color: '' },\n { id: 23, name: 'Vanadium', description: '', status: 'success', color: '' },\n { id: 24, name: 'Chromium', description: '', status: 'success', color: '' },\n];\n\n/**\n * Data source for the List view. This class should\n * encapsulate all logic for fetching and manipulating the displayed data\n * (including sorting, pagination, and filtering).\n */\nexport class ListDataSource extends DataSource<ListItem> {\n data: ListItem[] = EXAMPLE_DATA;\n paginator!: OnemrvaMatPaginatorComponent;\n sort: MatSort | undefined;\n data$: BehaviorSubject<ListItem[]> = new BehaviorSubject<ListItem[]>([]);\n\n constructor() {\n super();\n this.data$.next(this.data);\n }\n\n /**\n * Connect this data source to the table. The table will only update when\n * the returned stream emits new items.\n * @returns A stream of the items to be rendered.\n */\n connect(): Observable<ListItem[]> {\n if (this.sort) {\n // Combine everything that affects the rendered data into one update\n // stream for the data-table to consume.\n return merge(this.data$, this.sort.sortChange).pipe(\n map(() => {\n return this.getPagedData(this.getSortedData([...this.data]));\n }),\n );\n } else {\n throw Error('Please set the sort on the data source before connecting.');\n }\n }\n\n /**\n * Called when the table is being destroyed. Use this function, to clean up\n * any open connections or free any held resources that were set up during connect.\n */\n disconnect(): void {\n console.log('disconnect');\n }\n\n /**\n * Paginate the data (client-side). If you're using server-side pagination,\n * this would be replaced by requesting the appropriate data from the server.\n */\n private getPagedData(data: ListItem[]): ListItem[] {\n if (this.paginator) {\n const startIndex = this.paginator.pageIndex * this.paginator.pageSize;\n return data.splice(startIndex, this.paginator.pageSize);\n } else {\n return data;\n }\n }\n\n /**\n * Sort the data (client-side). If you're using server-side sorting,\n * this would be replaced by requesting the appropriate data from the server.\n */\n private getSortedData(data: ListItem[]): ListItem[] {\n if (!this.sort || !this.sort.active || this.sort.direction === '') {\n return data;\n }\n\n return data.sort((a, b) => {\n const isAsc = this.sort?.direction === 'asc';\n switch (this.sort?.active) {\n case 'status':\n return compare(a.status, b.status, isAsc);\n case 'description':\n return compare(a.description, b.description, isAsc);\n case 'name':\n return compare(a.name, b.name, isAsc);\n case 'id':\n return compare(+a.id, +b.id, isAsc);\n default:\n return 0;\n }\n });\n }\n\n /**\n * @param row\n */\n public remove(row: ListItem) {\n const idx = this.data.indexOf(row);\n if (idx >= 0) {\n this.data.splice(idx, 1);\n this.paginator.length = this.data.length;\n }\n this.data$.next(this.data);\n }\n\n /**\n *\n */\n public touch() {\n this.data$.next(this.data);\n }\n}\n\n/** Simple sort comparator for example ID/Name columns (for client-side sorting). */\nfunction compare(\n a: string | number,\n b: string | number,\n isAsc: boolean,\n): number {\n return (a < b ? -1 : 1) * (isAsc ? 1 : -1);\n}\n","demo-table-entrypoint.component.html":"<table\n mat-table\n class=\"full-width-table mat-card-table\"\n matSort\n aria-label=\"Elements\"\n>\n <!-- Id Column -->\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header=\"id\">Id</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.id }}</td>\n </ng-container>\n\n <!-- Name Column -->\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header=\"name\">Name</th>\n <td mat-cell *matCellDef=\"let row\">\n <strong\n ><copy-to-clipboard [tooltip]=\"'copied.to.clipboard' | translate\">{{\n row.name\n }}</copy-to-clipboard></strong\n >\n </td>\n </ng-container>\n\n <!-- Description Column -->\n <ng-container matColumnDef=\"description\">\n <th\n mat-header-cell\n *matHeaderCellDef\n mat-sort-header=\"description\"\n class=\"d-none-xsmall d-table-cell-medium\"\n >\n Description\n </th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"d-none-xsmall d-table-cell-medium\"\n >\n <span></span>\n {{ row.description }}\n </td></ng-container\n >\n\n <!-- Status Column -->\n <ng-container matColumnDef=\"status\">\n <th\n mat-header-cell\n *matHeaderCellDef\n mat-sort-header=\"status\"\n class=\"d-none-xsmall d-table-cell-medium\"\n >\n Status\n </th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"d-none-xsmall d-table-cell-medium\"\n >\n @if (row.status === 'success') {\n <onemrva-mat-sticker\n aria-label=\"Success\"\n class=\"sticker\"\n color=\"success\"\n >\n <mat-icon class=\"mr-s\">thumb_up</mat-icon>Success\n </onemrva-mat-sticker>\n }\n @if (row.status === 'error') {\n <onemrva-mat-sticker aria-label=\"Error\" class=\"sticker\" color=\"error\">\n <mat-icon class=\"mr-s\">error</mat-icon>Error\n </onemrva-mat-sticker>\n }\n @if (row.status === 'warn') {\n <onemrva-mat-sticker aria-label=\"Warning\" class=\"sticker\" color=\"warn\">\n <mat-icon class=\"mr-s\">warning</mat-icon>Warning\n </onemrva-mat-sticker>\n }\n </td>\n </ng-container>\n\n <!-- More Column -->\n <ng-container matColumnDef=\"more\">\n <th mat-header-cell *matHeaderCellDef>More</th>\n <td mat-cell *matCellDef=\"let row\" class=\"text-right\">\n <button\n mat-icon-button\n [matMenuTriggerFor]=\"menu\"\n (click)=\"$event.stopPropagation()\"\n aria-label=\"More\"\n >\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item (click)=\"edit(row)\" aria-label=\"Edit\">\n <mat-icon>edit</mat-icon>Edit\n </button>\n <button mat-menu-item (click)=\"delete(row)\" aria-label=\"Delete\">\n <mat-icon>delete</mat-icon>Delete\n </button>\n <button mat-menu-item (click)=\"share(row)\" aria-label=\"Share\">\n <mat-icon>share</mat-icon>Share\n </button>\n </mat-menu>\n <mat-divider [vertical]=\"true\" />\n <button mat-icon-button aria-label=\"Continue\">\n <mat-icon>chevron_right</mat-icon>\n </button>\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"displayedColumns\"></tr>\n <tr\n mat-row\n (matRowClickable)=\"open(row)\"\n *matRowDef=\"let row; columns: displayedColumns\"\n ></tr>\n</table>\n<onemrva-mat-paginator\n [pageSizeDefaultOption]=\"10\"\n [pageIndex]=\"0\"\n [length]=\"dataSource.data.length\"\n (onRequestPage)=\"changePage($event)\"\n previousKey=\"Previous\"\n nextKey=\"Next\"\n #paginator\n/>\n","demo-table-entrypoint.component.ts":"import {\n AfterViewInit,\n Component,\n inject,\n ViewEncapsulation,\n viewChild,\n} from '@angular/core';\nimport { MatSort, MatSortModule } from '@angular/material/sort';\nimport { MatTable, MatTableModule } from '@angular/material/table';\nimport { ListDataSource, ListItem } from './demo-table-entrypoint-datasource';\nimport { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';\n\nimport { MatCardModule } from '@angular/material/card';\nimport { MatChipsModule } from '@angular/material/chips';\nimport { MatIconModule } from '@angular/material/icon';\nimport { MatMenuModule } from '@angular/material/menu';\nimport { MatButtonModule } from '@angular/material/button';\nimport { MatDividerModule } from '@angular/material/divider';\nimport { MatTooltipModule } from '@angular/material/tooltip';\nimport { OnemrvaMatPaginatorComponent } from '@onemrvapublic/design-system/mat-paginator';\nimport { OnemrvaSharedModule } from '@onemrvapublic/design-system/shared';\nimport { OnemrvaCopyToClipboardComponent } from '@onemrvapublic/design-system/mat-copy-to-clipboard';\nimport { OnemrvaMatStickerModule } from '@onemrvapublic/design-system/mat-sticker';\nimport { DemoComponentBase } from '../../_demo/demo-component-base';\nimport { TranslatePipe } from '@ngx-translate/core';\n\n@Component({\n selector: 'app-demo-card-table',\n templateUrl: './demo-table-entrypoint.component.html',\n\n standalone: true,\n imports: [\n MatCardModule,\n MatChipsModule,\n MatTableModule,\n MatSortModule,\n MatIconModule,\n MatMenuModule,\n MatButtonModule,\n OnemrvaSharedModule,\n MatDividerModule,\n MatTooltipModule,\n ClipboardModule,\n OnemrvaCopyToClipboardComponent,\n OnemrvaMatStickerModule,\n OnemrvaMatPaginatorComponent,\n TranslatePipe,\n ],\n encapsulation: ViewEncapsulation.None,\n})\nexport class DemoTableEntrypointComponent\n extends DemoComponentBase\n implements AfterViewInit\n{\n readonly paginator = viewChild.required(OnemrvaMatPaginatorComponent);\n readonly sort = viewChild.required(MatSort);\n readonly table = viewChild.required(MatTable);\n dataSource: ListDataSource;\n\n /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */\n displayedColumns = ['id', 'name', 'description', 'status', 'more'];\n bgcolor = '#123456';\n\n clipboardService = inject(Clipboard);\n constructor() {\n super();\n this.dataSource = new ListDataSource();\n }\n\n ngAfterViewInit(): void {\n this.dataSource.sort = this.sort();\n this.dataSource.paginator = this.paginator();\n this.table().dataSource = this.dataSource;\n }\n\n edit(row: ListItem) {\n alert(`Edit row n°${row.id}: ${row.name}`);\n }\n\n delete(row: ListItem) {\n this.dataSource.remove(row);\n }\n\n share(row: ListItem) {\n alert(`Share row n°${row.id}: ${row.name}`);\n }\n\n changePage(event: any) {\n this.dataSource.paginator.pageIndex = event;\n this.dataSource.touch();\n }\n\n open(row: any) {\n alert(`Clicked : ${row.id}`);\n }\n}\n"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"demo-table-datasource.ts":"import { DataSource } from '@angular/cdk/collections';\nimport { MatSort } from '@angular/material/sort';\nimport { map, tap } from 'rxjs/operators';\nimport {\n BehaviorSubject,\n merge,\n Observable,\n startWith,\n Subject,\n switchMap,\n} from 'rxjs';\nimport { OnemrvaMatPaginatorComponent } from '@onemrvapublic/design-system/mat-paginator';\nimport { FormControl } from '@angular/forms';\nimport { ElementsService, ListItem } from '../../_services/elements.service';\n\n/**\n * Data source for the List view. This class should\n * encapsulate all logic for fetching and manipulating the displayed data\n * (including sorting, pagination, and filtering).\n */\nexport class ListDataSource extends DataSource<ListItem> {\n paginator!: OnemrvaMatPaginatorComponent;\n search!: FormControl;\n sort: MatSort | undefined;\n data$: BehaviorSubject<ListItem[]> = new BehaviorSubject<ListItem[]>([]);\n prevSearch = '';\n refresh$ = new Subject();\n\n constructor(private elementsService: ElementsService) {\n super();\n }\n\n /**\n * Connect this data source to the table. The table will only update when\n * the returned stream emits new items.\n * @returns A stream of the items to be rendered.\n */\n connect(): Observable<ListItem[]> {\n if (this.sort) {\n // Combine everything that affects the rendered data into one update\n // stream for the data-table to consume.\n return merge(\n this.sort.sortChange, // we listen to sort change\n this.search.valueChanges, // we listen to filter change\n this.paginator.pageSize$, // we listen to page size change\n this.paginator.pageIndex$, // we listen to page index change\n this.refresh$, // used to force a refresh\n ).pipe(\n startWith({}),\n tap(() => {\n // if search changed, we reset the pageIndex\n if (\n this.search.getRawValue().trim() !== '' &&\n this.search.getRawValue() !== this.prevSearch\n ) {\n this.paginator.resetPageIndex();\n this.prevSearch = this.search.getRawValue();\n }\n }),\n switchMap(() => {\n const active: keyof ListItem =\n (this.sort?.active as keyof ListItem) || 'id';\n const direction = this.sort?.direction || 'asc';\n // We return the api call observable called with the correct parameters\n return this.elementsService\n .getElementsList(\n active,\n direction,\n this.paginator.pageIndex,\n this.paginator.pageSize,\n this.search.getRawValue(),\n )\n .pipe(\n // We map the response to the Interface we need/want\n map(response => {\n this.paginator.length = response.length;\n this.paginator.pageIndex = response.page;\n this.paginator.pageSize = response.pageSize;\n return response.list;\n }),\n );\n }),\n );\n } else {\n throw Error('Please set the sort on the data source before connecting.');\n }\n }\n\n /**\n * Called when the table is being destroyed. Use this function, to clean up\n * any open connections or free any held resources that were set up during connect.\n */\n disconnect(): void {\n console.log('disconnect');\n }\n\n /**\n * If user click on select all items (outside current page),\n * this service functions returns the IDs of all items in datasource\n */\n all() {\n return this.elementsService.getAllElementsIds(this.search.getRawValue());\n }\n\n /**\n * If user click to delete an item, we tell the api and then refresh the list.\n * @param id\n */\n delete(id: number) {\n this.elementsService.deleteElement(id);\n this.refresh$.next('');\n }\n}\n","demo-table.component.html":"<h1>Table</h1>\n<mat-form-field class=\"search\">\n <mat-label>Search</mat-label>\n <input matInput [formControl]=\"search\" />\n @if (search.getRawValue()) {\n <button matSuffix mat-icon-button aria-label=\"Clear\" (click)=\"clear()\">\n <mat-icon>close</mat-icon>\n </button>\n }\n</mat-form-field>\n<p><a tabindex=\"0\" (click)=\"showHideColumns()\">Hide/Show column</a></p>\n<table\n mat-table\n class=\"full-width-table multi-table-row-header\"\n matSort\n aria-label=\"Elements\"\n>\n <tr mat-header-row *matHeaderRowDef=\"allColumns\"></tr>\n <tr mat-header-row class=\"compact\" *matHeaderRowDef=\"['delay', 'days']\"></tr>\n\n <!-- Notice Row -->\n <tr\n mat-header-row\n class=\"onemrva-notice-row\"\n [class]=\"showNotice() ? '' : 'hidden'\"\n *matHeaderRowDef=\"showNotice() ? ['notice'] : []\"\n ></tr>\n @if (showNotice()) {\n <ng-container matColumnDef=\"notice\">\n <th *matHeaderCellDef colspan=\"7\">\n You have selected {{ checkboxes.length }} items. \n @if (checkboxes.length < paginator.length) {\n <a tabindex=\"0\" (click)=\"selectAll()\"\n >Select {{ paginator.length }} items</a\n > \n }\n @if (checkboxes.length >= 0) {\n <a tabindex=\"0\" (click)=\"deSelectAll()\"\n >Deselect {{ checkboxes.length }} items</a\n >\n }\n </th>\n </ng-container>\n }\n <!-- Notice Row -->\n\n <tr mat-row *matRowDef=\"let _; columns: allSubColumns\"></tr>\n\n <tr *matNoDataRow class=\"noresult\">\n <td colspan=\"7\">\n <onemrva-mat-empty-row (clear)=\"clear()\" />\n </td>\n </tr>\n\n <!-- checkbox Column -->\n <ng-container matColumnDef=\"check\">\n <th mat-header-cell *matHeaderCellDef [attr.rowspan]=\"2\">\n <div>\n <mat-checkbox\n aria-label=\"Check All\"\n value=\"toggleAll\"\n [checked]=\"checked\"\n [indeterminate]=\"undetermined\"\n (change)=\"toggleAllVisible($event)\"\n />\n </div>\n </th>\n <td mat-cell *matCellDef=\"let row\">\n <mat-checkbox\n #checkboxes\n aria-label=\"Check row {{ row.id }}\"\n [value]=\"row.id\"\n [checked]=\"isChecked(row.id)\"\n (change)=\"toggleOne($event, row.id)\"\n />\n </td>\n </ng-container>\n\n <!-- Id Column -->\n @if (columnIsVisible('id')) {\n <ng-container matColumnDef=\"id\" sticky>\n <th\n mat-header-cell\n class=\"compact\"\n *matHeaderCellDef\n mat-sort-header\n [attr.rowspan]=\"2\"\n >\n Id\n </th>\n <td mat-cell class=\"compact\" *matCellDef=\"let row\">\n {{ row.id }}\n </td>\n </ng-container>\n }\n\n <!-- Name Column -->\n @if (columnIsVisible('name')) {\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header [attr.rowspan]=\"2\">\n Name\n </th>\n <td id=\"{{ row.name }}\" mat-cell *matCellDef=\"let row\">\n <a href=\"/#table\">{{ row.name }}</a>\n </td>\n </ng-container>\n }\n\n <!-- Description Column -->\n @if (columnIsVisible('description')) {\n <ng-container matColumnDef=\"description\">\n <th\n mat-header-cell\n *matHeaderCellDef\n mat-sort-header=\"description\"\n [attr.rowspan]=\"2\"\n class=\"d-table-cell-large description-cell\"\n >\n Description\n </th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"d-table-cell-large description-cell\"\n >\n <span></span>\n {{ row.description }}\n </td></ng-container\n >\n }\n @if (columnIsVisible('suspension')) {\n <!-- Suspension Column -->\n <ng-container matColumnDef=\"suspension\">\n <th\n mat-header-cell\n *matHeaderCellDef\n [attr.colspan]=\"2\"\n class=\"align-center compact table__border-left table__border-right\"\n >\n Suspension\n </th>\n <!-- This column doesn't generate <td> items, so no need to add a definition for them -->\n </ng-container>\n }\n\n @if (subColumnIsVisible('delay')) {\n <!-- Delay Column -->\n <ng-container matColumnDef=\"delay\">\n <th\n mat-header-cell\n *matHeaderCellDef\n class=\"compact align-center table__border-left\"\n >\n Delay\n </th>\n <td mat-cell *matCellDef=\"let _\" class=\"align-center table__border-left\">\n 1\n </td>\n </ng-container>\n }\n\n @if (subColumnIsVisible('days')) {\n <!-- Days Column -->\n <ng-container matColumnDef=\"days\">\n <th\n mat-header-cell\n *matHeaderCellDef\n class=\"compact align-center table__border-right\"\n >\n Days\n </th>\n <td mat-cell *matCellDef=\"let _\" class=\"align-center table__border-right\">\n 2\n </td>\n </ng-container>\n }\n\n @if (columnIsVisible('more')) {\n <!-- Menu Column -->\n <ng-container matColumnDef=\"more\">\n <th mat-header-cell class=\"compact\" *matHeaderCellDef [attr.rowspan]=\"2\">\n Menu\n </th>\n <td mat-cell class=\"compact align-right\" *matCellDef=\"let row\">\n <button mat-icon-button [matMenuTriggerFor]=\"menu\" aria-label=\"More\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item (click)=\"edit(row)\" aria-label=\"Edit\">\n <mat-icon>edit</mat-icon>\n Edit\n </button>\n <button mat-menu-item (click)=\"delete(row)\" aria-label=\"Delete\">\n <mat-icon>delete</mat-icon>\n Delete\n </button>\n <button mat-menu-item (click)=\"share(row)\" aria-label=\"Share\">\n <mat-icon>share</mat-icon>\n Share\n </button>\n </mat-menu>\n </td>\n </ng-container>\n }\n</table>\n<div class=\"my-m\">\n <onemrva-mat-paginator\n [pageSizeSelector]=\"true\"\n [pageSizeOptions]=\"[5, 10, 20, 50]\"\n [pageSizeDefaultOption]=\"5\"\n [pageSize]=\"5\"\n previousKey=\"Previous\"\n nextKey=\"Next\"\n (page)=\"changePage($event)\"\n #paginator\n />\n</div>\n\n<h2>Expandable row</h2>\n<p>\n A row can expand to reveal its own sub-rows. Several rows can be open at the\n same time, so two payments can be compared side by side. A row without\n sub-rows has no toggle to press and no detail row of its own.\n</p>\n<p>\n <button\n mat-button\n type=\"button\"\n aria-label=\"Collapse all expanded rows\"\n [disabled]=\"expandedCount() === 0\"\n (click)=\"collapseAll()\"\n >\n Collapse all ({{ expandedCount() }})\n </button>\n</p>\n\n<!--\n The table is kept a real `display: table` (see the .scss) and scrolls from\n this wrapper on narrow screens instead of from the table element itself.\n-->\n<div class=\"demo-expandable-table-scroll\">\n <table\n mat-table\n multiTemplateDataRows\n class=\"full-width-table demo-expandable-table\"\n aria-label=\"Benefit payments per beneficiary\"\n [dataSource]=\"benefitRows\"\n >\n <tr mat-header-row *matHeaderRowDef=\"benefitColumns\"></tr>\n\n <!--\n Data row. The stripe comes from `dataIndex` and not from the row's position\n in the DOM, because the detail rows shift that position — see the .scss.\n -->\n <tr\n mat-row\n *matRowDef=\"let row; columns: benefitColumns; let i = dataIndex\"\n class=\"demo-expandable-row\"\n [class.demo-expandable-row--striped]=\"i % 2 === 0\"\n [class.demo-expandable-row--open]=\"isExpanded(row.id)\"\n ></tr>\n\n <!--\n Detail row. Rendered only for rows that actually have sub-rows — `when:`\n keeps an empty detail row out of the DOM entirely instead of rendering a\n blank one. For rows that do have sub-rows it stays rendered while closed,\n so its wrapper has something to run the open/close transition on; the .scss\n collapses it to zero height.\n -->\n <tr\n mat-row\n *matRowDef=\"let row; columns: detailColumns; when: hasDetailRow\"\n class=\"demo-expandable-detail-row\"\n [attr.id]=\"detailRowId(row.id)\"\n ></tr>\n\n <!-- Expand toggle Column -->\n <ng-container matColumnDef=\"expand\">\n <th mat-header-cell *matHeaderCellDef class=\"demo-expandable-toggle-cell\">\n Details\n </th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-expandable-toggle-cell\">\n <!-- A row without sub-rows has nothing to reveal, so it gets no toggle. -->\n @if (hasDetails(row)) {\n <onemrva-expandable-table-icon\n [isExpanded]=\"isExpanded(row.id)\"\n [detailRowId]=\"detailRowId(row.id)\"\n (expandedChange)=\"toggleExpanded(row.id)\"\n />\n }\n </td>\n </ng-container>\n\n <!-- Id Column -->\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef class=\"compact\">Id</th>\n <td mat-cell *matCellDef=\"let row\" class=\"compact\">{{ row.id }}</td>\n </ng-container>\n\n <!-- Name Column -->\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef>Beneficiary</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.name }}</td>\n </ng-container>\n\n <!-- Office Column -->\n <ng-container matColumnDef=\"office\">\n <th mat-header-cell *matHeaderCellDef>Office</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.office }}</td>\n </ng-container>\n\n <!-- Period Column -->\n <ng-container matColumnDef=\"period\">\n <th mat-header-cell *matHeaderCellDef>Period</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.period }}</td>\n </ng-container>\n\n <!-- Total Column -->\n <ng-container matColumnDef=\"total\">\n <th mat-header-cell *matHeaderCellDef class=\"align-right\">Total</th>\n <td mat-cell *matCellDef=\"let row\" class=\"align-right\">\n {{ row.total }}\n </td>\n </ng-container>\n\n <!--\n Detail Column. A plain grid of divs rather than `onemrva-mat-panel` (its\n chrome is wrong inside a row) or a nested `mat-table`.\n -->\n <ng-container matColumnDef=\"detail\">\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"demo-expandable-detail-cell\"\n [class.demo-expandable-detail-cell--open]=\"isExpanded(row.id)\"\n [attr.colspan]=\"benefitColumns.length\"\n >\n <div class=\"demo-expandable-detail\">\n <div class=\"demo-expandable-detail__inner\">\n <div\n class=\"demo-expandable-detail__grid\"\n role=\"table\"\n [attr.aria-hidden]=\"isExpanded(row.id) ? null : 'true'\"\n [attr.aria-label]=\"'Payment details of ' + row.name\"\n >\n <div class=\"demo-expandable-detail__row\" role=\"row\">\n <div class=\"demo-expandable-detail__header\" role=\"columnheader\">\n Code\n </div>\n <div class=\"demo-expandable-detail__header\" role=\"columnheader\">\n Benefit\n </div>\n <div\n class=\"demo-expandable-detail__header demo-expandable-detail__numeric\"\n role=\"columnheader\"\n >\n Days\n </div>\n <div\n class=\"demo-expandable-detail__header demo-expandable-detail__numeric\"\n role=\"columnheader\"\n >\n Amount\n </div>\n </div>\n @for (detail of detailsFor(row.id); track detail.code) {\n <div class=\"demo-expandable-detail__row\" role=\"row\">\n <div role=\"cell\">{{ detail.code }}</div>\n <div role=\"cell\">{{ detail.label }}</div>\n <div class=\"demo-expandable-detail__numeric\" role=\"cell\">\n {{ detail.days }}\n </div>\n <div class=\"demo-expandable-detail__numeric\" role=\"cell\">\n {{ detail.amount }}\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n </td>\n </ng-container>\n </table>\n</div>\n\n<h2>Expandable row with real rows</h2>\n<p>\n The same toggle, but the sub-rows are real table rows instead of a grid inside\n one wide detail cell, so they sit in the table's own columns. A parent and its\n sub-rows share one stripe and one bottom border, so the group reads as a\n block, and the stripe alternates per parent whether or not any row is open.\n</p>\n<p>\n <button\n mat-button\n type=\"button\"\n aria-label=\"Collapse all expanded rows\"\n [disabled]=\"treeExpandedCount() === 0\"\n (click)=\"collapseAllTree()\"\n >\n Collapse all ({{ treeExpandedCount() }})\n </button>\n</p>\n\n<div class=\"demo-expandable-table-scroll\">\n <table\n mat-table\n class=\"full-width-table demo-tree-table\"\n aria-label=\"Benefit payments per beneficiary, with sub-rows\"\n [dataSource]=\"treeRows()\"\n [trackBy]=\"treeTrackBy\"\n >\n <tr mat-header-row *matHeaderRowDef=\"treeColumns\"></tr>\n\n <!--\n Parent row. The stripe comes from `groupIndex` rather than from the row's\n position in the DOM, because the sub-rows shift that position — see the\n .scss. `lastInGroup` is false while the row is open, which hands the\n group's bottom border to its last sub-row.\n\n No `aria-expanded` on the row itself: on a `role=\"row\"` that attribute is\n only defined inside a `treegrid`, and this is a plain table, so it is\n invalid here (axe `aria-conditional-attr`). Making the table a real\n treegrid would mean owning full grid keyboard navigation, which is not\n what this example is about. The expanded state is already on the toggle\n button inside the row — `onemrva-expandable-table-icon` sets\n `aria-expanded` and `aria-controls` on its own button — which is the\n ordinary disclosure pattern and what the detail-row example above does too.\n -->\n <tr\n mat-row\n *matRowDef=\"let entry; columns: treeColumns\"\n class=\"demo-tree-row\"\n [class.demo-tree-row--striped]=\"entry.groupIndex % 2 === 0\"\n [class.demo-tree-row--grouped]=\"!entry.lastInGroup\"\n ></tr>\n\n <!-- Sub-row. Rendered only while its parent is open; `treeRows()` decides. -->\n <tr\n mat-row\n *matRowDef=\"let entry; columns: treeDetailColumns; when: isTreeChildRow\"\n class=\"demo-tree-row demo-tree-row--child\"\n [class.demo-tree-row--striped]=\"entry.groupIndex % 2 === 0\"\n [class.demo-tree-row--grouped]=\"!entry.lastInGroup\"\n [attr.id]=\"treeChildRowId(entry.parent.id, entry.detail.code)\"\n ></tr>\n\n <!-- Parent columns -->\n <ng-container matColumnDef=\"tree-id\">\n <th mat-header-cell *matHeaderCellDef class=\"compact\">Id</th>\n <td mat-cell *matCellDef=\"let entry\" class=\"compact\">\n {{ entry.row.id }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-name\">\n <th mat-header-cell *matHeaderCellDef>Beneficiary</th>\n <td mat-cell *matCellDef=\"let entry\">{{ entry.row.name }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-office\">\n <th mat-header-cell *matHeaderCellDef>Office</th>\n <td mat-cell *matCellDef=\"let entry\">{{ entry.row.office }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-period\">\n <th mat-header-cell *matHeaderCellDef>Period</th>\n <td mat-cell *matCellDef=\"let entry\">{{ entry.row.period }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-total\">\n <th mat-header-cell *matHeaderCellDef class=\"align-right\">Total</th>\n <td mat-cell *matCellDef=\"let entry\" class=\"align-right\">\n {{ entry.row.total }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-expand\">\n <th mat-header-cell *matHeaderCellDef class=\"demo-tree-toggle-cell\">\n Details\n </th>\n <td mat-cell *matCellDef=\"let entry\" class=\"demo-tree-toggle-cell\">\n <!-- A row without sub-rows has nothing to reveal, so it gets no toggle. -->\n @if (hasDetails(entry.row)) {\n <onemrva-expandable-table-icon\n [isExpanded]=\"isTreeExpanded(entry.row.id)\"\n [detailRowId]=\"treeChildRowIds(entry.row.id)\"\n (expandedChange)=\"toggleTreeExpanded(entry.row.id)\"\n />\n }\n </td>\n </ng-container>\n\n <!--\n Sub-row columns. One per parent column so the cells line up; the code\n takes the Id column, the days take the Period column (they are a\n subdivision of that period) and the Office and Details columns stay empty.\n -->\n <ng-container matColumnDef=\"tree-detail-code\">\n <td mat-cell *matCellDef=\"let entry\" class=\"compact demo-tree-child-cell\">\n {{ entry.detail.code }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-label\">\n <td mat-cell *matCellDef=\"let entry\">{{ entry.detail.label }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-office\">\n <td mat-cell *matCellDef=\"let _\"></td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-days\">\n <td mat-cell *matCellDef=\"let entry\">{{ entry.detail.days }} days</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-amount\">\n <td mat-cell *matCellDef=\"let entry\" class=\"align-right\">\n {{ entry.detail.amount }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-expand\">\n <td mat-cell *matCellDef=\"let _\" class=\"demo-tree-toggle-cell\"></td>\n </ng-container>\n </table>\n</div>\n\n<h2>Inline row editing</h2>\n<p>\n The pen switches one row into edit mode. Only some columns become editable —\n <em>Id</em> is the row's identity and <em>Period</em> comes from the payment\n run, so both keep rendering their value while the rest of the row is a form.\n</p>\n<p>\n Several rows can be open at once. Each one owns its own form, so validity,\n dirtiness and cancel are row-scoped and cancelling one row leaves the others\n untouched. Saving writes to the local signal only — a real screen would call a\n service and handle the failure path — and a dirty row is discarded without\n confirmation, which a real screen would also ask about first.\n</p>\n\n<!--\n Bulk bar. Mirrors the notice row of the first table, but sits outside the\n table: it acts on rows across the whole page, not on one of them.\n-->\n@if (editingCount() > 0) {\n <p class=\"demo-inline-edit-bar\">\n {{ editingCount() }} row(s) in edit mode. \n <button\n mat-button\n type=\"button\"\n aria-label=\"Save every row in edit mode\"\n (click)=\"saveAll()\"\n >\n Save all\n </button>\n <button\n mat-button\n type=\"button\"\n aria-label=\"Discard every row in edit mode\"\n (click)=\"cancelAll()\"\n >\n Discard all\n </button>\n </p>\n}\n\n<div class=\"demo-inline-edit-table-scroll\">\n <table\n mat-table\n class=\"full-width-table demo-inline-edit-table\"\n [dataSource]=\"inlineRows()\"\n aria-label=\"Benefits, editable in place\"\n >\n <tr mat-header-row *matHeaderRowDef=\"inlineColumns\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: inlineColumns\"\n class=\"demo-inline-edit-row\"\n [class.demo-inline-edit-row--editing]=\"isEditing(row.id)\"\n (keydown.enter)=\"isEditing(row.id) && saveEdit(row.id)\"\n (keydown.escape)=\"cancelEdit(row.id)\"\n ></tr>\n\n <!-- Id Column — read-only in both modes. -->\n <ng-container matColumnDef=\"inline-id\">\n <th mat-header-cell *matHeaderCellDef class=\"compact\">Id</th>\n <td mat-cell *matCellDef=\"let row\" class=\"compact\">{{ row.id }}</td>\n </ng-container>\n\n <!-- Name Column — editable text. -->\n <ng-container matColumnDef=\"inline-name\">\n <th mat-header-cell *matHeaderCellDef>Beneficiary</th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-cell\">\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n matInput\n [id]=\"firstControlId(row.id)\"\n [formControl]=\"formFor(row.id).controls.name\"\n [attr.aria-label]=\"'Beneficiary of row ' + row.id\"\n />\n @if (formFor(row.id).controls.name.hasError('required')) {\n <mat-error>Beneficiary is required</mat-error>\n }\n @if (formFor(row.id).controls.name.hasError('maxlength')) {\n <mat-error>60 characters at most</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.name }}\n }\n </td>\n </ng-container>\n\n <!-- Office Column — editable select over a fixed list. -->\n <ng-container matColumnDef=\"inline-office\">\n <th mat-header-cell *matHeaderCellDef>Office</th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-cell\">\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <mat-select\n [formControl]=\"formFor(row.id).controls.office\"\n [attr.aria-label]=\"'Office of row ' + row.id\"\n >\n @for (office of offices; track office) {\n <mat-option [value]=\"office\">{{ office }}</mat-option>\n }\n </mat-select>\n @if (formFor(row.id).controls.office.hasError('required')) {\n <mat-error>Office is required</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.office }}\n }\n </td>\n </ng-container>\n\n <!-- Period Column — read-only for a business reason, not a technical one. -->\n <ng-container matColumnDef=\"inline-period\">\n <th mat-header-cell *matHeaderCellDef>Period</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.period }}</td>\n </ng-container>\n\n <!--\n Payment date Column — editable through the house datepicker.\n\n Same wiring as the datepicker demo: `onemrvaDateFormat` for the DD/MM/YYYY\n mask, the toggle as a suffix, and `[calendarHeaderComponent]`. The overlay\n the calendar opens in is a CDK overlay outside this row, so its own Escape\n (close the calendar) never reaches the row's Escape (cancel the edit).\n -->\n <ng-container matColumnDef=\"inline-paid-on\">\n <th mat-header-cell *matHeaderCellDef>Paid on</th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-cell\">\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n onemrvaDateFormat\n matInput\n [matDatepicker]=\"paidOnPicker\"\n [max]=\"today\"\n [formControl]=\"formFor(row.id).controls.paidOn\"\n [attr.aria-label]=\"'Payment date of row ' + row.id\"\n />\n <mat-datepicker-toggle matSuffix [for]=\"paidOnPicker\" />\n <mat-datepicker\n #paidOnPicker\n [calendarHeaderComponent]=\"OnemrvaMatDatepickerHeaderComponent\"\n />\n @if (formFor(row.id).controls.paidOn.hasError('required')) {\n <mat-error>Payment date is required</mat-error>\n }\n @if (\n formFor(row.id).controls.paidOn.hasError('matDatepickerParse')\n ) {\n <mat-error>Use DD/MM/YYYY</mat-error>\n }\n @if (formFor(row.id).controls.paidOn.hasError('matDatepickerMax')) {\n <mat-error>Cannot be in the future</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.paidOn | date: 'dd/MM/yyyy' }}\n }\n </td>\n </ng-container>\n\n <!-- Total Column — editable number, right-aligned in both modes. -->\n <ng-container matColumnDef=\"inline-total\">\n <th mat-header-cell *matHeaderCellDef class=\"align-right\">Total</th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"align-right demo-inline-edit-cell\"\n >\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n matInput\n type=\"number\"\n step=\"0.01\"\n [formControl]=\"formFor(row.id).controls.total\"\n [attr.aria-label]=\"'Total of row ' + row.id\"\n />\n @if (formFor(row.id).controls.total.hasError('required')) {\n <mat-error>Total is required</mat-error>\n }\n @if (formFor(row.id).controls.total.hasError('min')) {\n <mat-error>Cannot be negative</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.total | currency: 'EUR' }}\n }\n </td>\n </ng-container>\n\n <!-- Actions Column — the mode switch itself. -->\n <ng-container matColumnDef=\"inline-actions\">\n <th mat-header-cell *matHeaderCellDef class=\"demo-inline-edit-actions\">\n Actions\n </th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-actions\">\n @if (isEditing(row.id)) {\n <button\n mat-icon-button\n type=\"button\"\n [attr.aria-label]=\"'Save changes to ' + row.name\"\n (click)=\"saveEdit(row.id)\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [attr.aria-label]=\"'Discard changes to ' + row.name\"\n (click)=\"cancelEdit(row.id)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n } @else {\n <!--\n A locked row keeps a disabled button rather than no button at all,\n so the tooltip stays reachable and the cell keeps its width.\n -->\n <button\n mat-icon-button\n type=\"button\"\n [id]=\"editButtonId(row.id)\"\n [disabled]=\"row.locked\"\n [matTooltip]=\"\n row.locked ? 'Already paid out — no longer editable' : 'Edit row'\n \"\n [attr.aria-label]=\"'Edit ' + row.name\"\n (click)=\"startEdit(row)\"\n >\n <mat-icon>edit</mat-icon>\n </button>\n }\n </td>\n </ng-container>\n </table>\n</div>\n","demo-table.component.scss":"table {\n // Medium media query of material\n // the initial is display: table, but it doesn't show responsive on larger screens\n // Block is required for the scroll overflow!\n display: table;\n overflow: auto;\n width: 100%;\n\n .description-cell {\n min-width: 500px;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Expandable row example (second table)\n//\n// This component sets `ViewEncapsulation.None`, so every selector here leaks\n// globally: all of them are scoped under `.demo-expandable-table`.\n// ---------------------------------------------------------------------------\n// --- Width ------------------------------------------------------------------\n//\n// The `table` rule at the top of this file makes every table its own scroll\n// container with `display: block; overflow: auto`. A block-level <table> wraps\n// its rows in an *anonymous table box*, and that box's width is `auto`, i.e.\n// shrink-to-fit: `width: 100%` then sizes the outer block, not the anonymous\n// table inside it, so a table whose content is narrower than its container\n// stops filling that container. The first table hides this because its\n// `min-width: 500px` description cell already makes it wider than the viewport;\n// this one is narrow enough to visibly shrink.\n//\n// So: scroll from a wrapper, and let the table stay a table.\n.demo-expandable-table-scroll {\n width: 100%;\n overflow: auto;\n}\n\n.demo-expandable-table {\n &.mat-mdc-table {\n display: table;\n overflow: visible;\n }\n\n // --- Striping ------------------------------------------------------------\n //\n // `theme/overrides/_table.scss:22` stripes rows positionally, with\n // `tbody tr:nth-child(odd)`. `multiTemplateDataRows` inserts a detail <tr>\n // after every data <tr>, so a data row's DOM position is no longer its\n // position in the data: every data row lands on an odd child (all striped,\n // no alternation) and the detail rows take over the even ones.\n //\n // Approach taken: neutralise the positional rule for this table only, and\n // re-apply the stripe from a class bound to `dataIndex` in the template.\n // The theme override is shared by the whole library, so it is left alone.\n // `:nth-child(4n + 1)` would only work while every data row is followed by a\n // detail row, which is already not the case — rows without sub-rows get no\n // detail row. The index-based class cannot drift from the data.\n //\n // Both selectors below outrank the theme override on specificity alone, so\n // they do not depend on stylesheet order.\n &.mat-mdc-table:not(.mat-card-table) tbody tr:nth-child(odd) {\n background-color: transparent;\n }\n\n &.mat-mdc-table:not(.mat-card-table) tbody tr.demo-expandable-row--striped {\n background-color: var(--mat-sys-primary-container);\n\n & + tr {\n background-color: var(--mat-sys-primary-container);\n }\n }\n\n // --- Rows ----------------------------------------------------------------\n .demo-expandable-toggle-cell {\n width: 4rem;\n // A row without sub-rows carries no toggle button. On a cell `height` is a\n // minimum, so reserving the button's own size here keeps that row exactly\n // as tall as the rows that do have one.\n height: var(--mdc-icon-button-state-layer-size, 40px);\n }\n\n // An open data row and its detail row read as a single block.\n &.mat-mdc-table tbody tr.demo-expandable-row--open td.mat-mdc-cell {\n border-bottom: none;\n }\n\n // A closed detail row has to take up no vertical space at all, and two\n // things stand in the way of that.\n //\n // First, Material puts `height: var(--mat-table-row-item-container-height,\n // 52px)` on `.mat-mdc-row`. On a <tr> that height is a *minimum*, not a\n // target, so a closed detail row still reserved a full, empty row under its\n // data row. Size the row from its cell instead.\n &.mat-mdc-table tbody tr.demo-expandable-detail-row {\n height: auto;\n }\n\n // Second, the theme pads and underlines every cell.\n &.mat-mdc-table tbody td.demo-expandable-detail-cell.mat-mdc-cell {\n padding: 0;\n border-bottom: none;\n }\n\n &.mat-mdc-table tbody td.demo-expandable-detail-cell--open.mat-mdc-cell {\n border-bottom: 1px solid var(--mat-sys-outline-variant);\n }\n\n // --- Open / close transition ---------------------------------------------\n //\n // A `grid-template-rows: 0fr -> 1fr` transition on the wrapper, so the\n // content keeps its natural height without it being measured in TypeScript.\n // No `@angular/animations`: there are no animation triggers in this project.\n .demo-expandable-detail {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 220ms ease-in-out;\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n\n .demo-expandable-detail-cell--open .demo-expandable-detail {\n grid-template-rows: 1fr;\n }\n\n .demo-expandable-detail__inner {\n min-height: 0;\n overflow: hidden;\n }\n\n // --- Sub-rows ------------------------------------------------------------\n .demo-expandable-detail__grid {\n padding: var(--spacer) var(--spacer-and-half) var(--spacer-and-half);\n }\n\n .demo-expandable-detail__row {\n display: grid;\n grid-template-columns: 6rem minmax(12rem, 1fr) 5rem 8rem;\n gap: var(--spacer);\n padding: var(--half-spacer) 0;\n\n &:not(:last-child) {\n border-bottom: 1px solid var(--mat-sys-outline-variant);\n }\n }\n\n .demo-expandable-detail__header {\n font-weight: var(--mat-sys-title-small-weight, 500);\n color: var(--mat-sys-on-surface-variant);\n }\n\n .demo-expandable-detail__numeric {\n text-align: right;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Expandable rows as real rows (third table)\n//\n// Same caveat as above: `ViewEncapsulation.None`, so everything is scoped under\n// `.demo-tree-table`.\n// ---------------------------------------------------------------------------\n.demo-tree-table {\n &.mat-mdc-table {\n display: table;\n overflow: visible;\n }\n\n // --- Striping ------------------------------------------------------------\n //\n // Same problem as the second table, one step worse: the number of rows before\n // a given row now changes as rows are opened, so the theme's positional\n // `tbody tr:nth-child(odd)` (theme/overrides/_table.scss:22) cannot describe\n // this table at all. Neutralise it here and stripe from `groupIndex`, which\n // is the parent's index in the data and so is unaffected by what is open.\n //\n // A parent and its sub-rows carry the same `groupIndex`, hence the same\n // stripe: the group reads as one band, and the alternation still runs one\n // colour per parent whether or not anything is expanded.\n &.mat-mdc-table:not(.mat-card-table) tbody tr:nth-child(odd) {\n background-color: transparent;\n }\n\n &.mat-mdc-table:not(.mat-card-table) tbody tr.demo-tree-row--striped {\n background-color: var(--mat-sys-primary-container);\n }\n\n // --- Group borders -------------------------------------------------------\n //\n // The theme underlines every cell, which would rule off each sub-row and hide\n // where one group ends. Only the last row of a group keeps its border; the\n // rows above it drop theirs.\n &.mat-mdc-table tbody tr.demo-tree-row--grouped td.mat-mdc-cell {\n border-bottom: none;\n }\n\n // --- Cells ---------------------------------------------------------------\n .demo-tree-toggle-cell {\n width: 4rem;\n // A row without sub-rows carries no toggle button. On a cell `height` is a\n // minimum, so reserving the button's own size here keeps that row exactly\n // as tall as the rows that do have one.\n height: var(--mdc-icon-button-state-layer-size, 40px);\n }\n\n // Sub-rows are indented rather than coloured differently, because the group\n // banding has already spent the background on showing what belongs together.\n .demo-tree-child-cell {\n padding-left: var(--spacer-and-half);\n color: var(--mat-sys-on-surface-variant);\n }\n\n &.mat-mdc-table tbody tr.demo-tree-row--child td.mat-mdc-cell {\n // Sub-rows carry less content than their parent, so they need less room.\n height: auto;\n padding-top: var(--half-spacer);\n padding-bottom: var(--half-spacer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Inline row editing (fourth table)\n//\n// Same discipline as the two examples above: this component sets\n// `ViewEncapsulation.None`, so every selector here is scoped under\n// `.demo-inline-edit-table` (or its wrapper) and none of it leaks.\n//\n// The whole point of the styling here is that *nothing moves* when a row\n// switches mode. A `mat-form-field` is taller and wider than the text it\n// replaces, so without the reservations below the table reflows on every pen\n// click and the row the user aimed at jumps out from under the cursor.\n// ---------------------------------------------------------------------------\n\n// Scroll from a wrapper and let the table stay a table — see the note above\n// `.demo-expandable-table-scroll` for why the `table` rule at the top of this\n// file makes that necessary.\n.demo-inline-edit-table-scroll {\n width: 100%;\n overflow: auto;\n}\n\n.demo-inline-edit-bar {\n display: flex;\n align-items: center;\n gap: var(--half-spacer);\n}\n\n.demo-inline-edit-table {\n &.mat-mdc-table {\n display: table;\n overflow: visible;\n }\n\n // --- No layout jump ------------------------------------------------------\n //\n // Every row is given the height an *editing* row needs, so opening one does\n // not push the rows below it down. Editable cells get a min-width sized for\n // their control for the same reason, in the other axis.\n &.mat-mdc-table tbody tr.demo-inline-edit-row td.mat-mdc-cell {\n height: 4.5rem;\n }\n\n .demo-inline-edit-cell {\n min-width: 12rem;\n }\n\n // A form field in a table cell has to fill the cell and lose the margin it\n // would carry in a form layout. `subscriptSizing=\"dynamic\"` in the template\n // is the other half of this: the default reserves ~20px of subscript space\n // under every field, which would make each editing row taller again.\n .demo-inline-edit-cell .mat-mdc-form-field {\n width: 100%;\n margin: 0;\n }\n\n // The actions cell is sized for two icon buttons whether it currently shows\n // one or two, so the column does not resize with the mode.\n .demo-inline-edit-actions {\n width: 7rem;\n min-width: 7rem;\n white-space: nowrap;\n }\n\n // --- Mode cue ------------------------------------------------------------\n //\n // A left accent bar, not a background colour: the cue has to survive both\n // stripes and must not be carried by colour alone (WCAG 1.4.1).\n &.mat-mdc-table tbody tr.demo-inline-edit-row--editing td.mat-mdc-cell {\n //background: var(--mat-sys-surface-container-low);\n\n &:first-child {\n box-shadow: inset 3px 0 0 0 var(--mat-sys-primary);\n }\n }\n}\n","demo-table.component.ts":"import {\n afterNextRender,\n AfterViewInit,\n Component,\n computed,\n inject,\n Injector,\n signal,\n ViewEncapsulation,\n viewChild,\n viewChildren,\n} from '@angular/core';\nimport { CurrencyPipe, DatePipe } from '@angular/common';\nimport { LiveAnnouncer } from '@angular/cdk/a11y';\nimport { MatSelectModule } from '@angular/material/select';\nimport { MatError } from '@angular/material/form-field';\nimport { MatTooltip } from '@angular/material/tooltip';\nimport { MatDatepickerModule } from '@angular/material/datepicker';\nimport {\n OnemrvaDateFormatDirective,\n onemrvaDateNativeProvider,\n} from '@onemrvapublic/design-system/shared';\nimport { OnemrvaMatDatepickerHeaderComponent } from '@onemrvapublic/design-system/mat-datepicker-header';\nimport { MatSort, MatSortModule } from '@angular/material/sort';\nimport { MatTable, MatTableModule } from '@angular/material/table';\nimport { ListDataSource } from './demo-table-datasource';\nimport { PageEvent } from '@angular/material/paginator';\nimport {\n FormControl,\n FormGroup,\n FormsModule,\n ReactiveFormsModule,\n Validators,\n} from '@angular/forms';\nimport { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox';\n\nimport { MatChipsModule } from '@angular/material/chips';\nimport { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';\nimport { MatButton, MatIconButton } from '@angular/material/button';\nimport {\n OnemrvaMatPaginatorComponent,\n OnemrvaMatPaginatorModule,\n} from '@onemrvapublic/design-system/mat-paginator';\nimport {\n MatFormField,\n MatLabel,\n MatSuffix,\n} from '@angular/material/form-field';\nimport { MatInput } from '@angular/material/input';\nimport { OnemrvaMatEmptyRowComponent } from '@onemrvapublic/design-system/mat-empty-row';\nimport { ElementsService, ListItem } from '../../_services/elements.service';\nimport { OnemrvaMatMessageBoxModule } from '@onemrvapublic/design-system/mat-message-box';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { take } from 'rxjs';\nimport { MatDialog, MatDialogModule } from '@angular/material/dialog';\nimport { DemoTableDialogContentComponent } from '../table-dialog-content';\nimport { MatIcon } from '@angular/material/icon';\nimport { DemoComponentBase } from '../../_demo/demo-component-base';\nimport { OnemrvaExpandableTableIconComponent } from '@onemrvapublic/design-system/mat-table';\n\n/**\n * A row of the \"expandable row\" example.\n *\n * `ListItem` (from `ElementsService`) carries no nested data, so the second\n * example uses its own local shape instead of extending the service.\n */\ninterface BenefitRow {\n id: number;\n name: string;\n office: string;\n period: string;\n total: string;\n}\n\n/** One sub-row revealed when a {@link BenefitRow} is expanded. */\ninterface BenefitDetailRow {\n code: string;\n label: string;\n days: number;\n amount: string;\n}\n\n/**\n * One rendered row of the \"expandable rows as real rows\" example.\n *\n * Unlike the detail-row example, the sub-rows there are ordinary `<tr mat-row>`\n * elements in the same table, so parents and children have to live in one flat\n * array. A discriminated union keeps the two shapes apart while they share it.\n */\ntype BenefitTreeRow =\n | {\n readonly kind: 'parent';\n /** Stable key for the table's `trackBy`. */\n readonly key: string;\n /** Index of the parent in the data — the stripe is derived from it. */\n readonly groupIndex: number;\n /** Last row of its group, i.e. the row that carries the group's border. */\n readonly lastInGroup: boolean;\n readonly row: BenefitRow;\n }\n | {\n readonly kind: 'child';\n readonly key: string;\n readonly groupIndex: number;\n readonly lastInGroup: boolean;\n readonly parent: BenefitRow;\n readonly detail: BenefitDetailRow;\n };\n\n/**\n * Static data for the expandable-row example.\n *\n * Deliberately not a `ListDataSource`: that class throws when `sort` is unset\n * before `connect()` and expects a paginator, so reusing it would mean a second\n * sort and paginator instance for an example that has neither.\n */\nconst BENEFIT_ROWS: readonly BenefitRow[] = [\n {\n id: 1,\n name: 'Janssens, Marie',\n office: 'Brussel',\n period: 'January 2026',\n total: '€ 1.284,60',\n },\n {\n id: 2,\n name: 'Peeters, Tom',\n office: 'Antwerpen',\n period: 'January 2026',\n total: '€ 962,00',\n },\n {\n id: 3,\n name: 'Dubois, Claire',\n office: 'Liège',\n period: 'January 2026',\n total: '€ 1.431,25',\n },\n {\n id: 4,\n name: 'Van Damme, Lars',\n office: 'Gent',\n period: 'January 2026',\n total: '€ 744,15',\n },\n // No sub-rows: this row has no detail row at all and its toggle is disabled.\n {\n id: 5,\n name: 'Mertens, Hind',\n office: 'Brugge',\n period: 'January 2026',\n total: '€ 0,00',\n },\n];\n\n/** Sub-rows per row id. */\nconst BENEFIT_DETAILS: Readonly<Record<number, readonly BenefitDetailRow[]>> = {\n 1: [\n {\n code: 'A01',\n label: 'Full unemployment benefit',\n days: 18,\n amount: '€ 902,40',\n },\n {\n code: 'A07',\n label: 'Seniority supplement',\n days: 18,\n amount: '€ 241,20',\n },\n { code: 'C12', label: 'Commuting allowance', days: 12, amount: '€ 141,00' },\n ],\n 2: [\n {\n code: 'A01',\n label: 'Full unemployment benefit',\n days: 14,\n amount: '€ 728,00',\n },\n {\n code: 'B03',\n label: 'Temporary unemployment — economic',\n days: 4,\n amount: '€ 234,00',\n },\n ],\n 3: [\n {\n code: 'A01',\n label: 'Full unemployment benefit',\n days: 20,\n amount: '€ 1.002,00',\n },\n {\n code: 'A07',\n label: 'Seniority supplement',\n days: 20,\n amount: '€ 268,00',\n },\n { code: 'C12', label: 'Commuting allowance', days: 15, amount: '€ 161,25' },\n ],\n 4: [\n {\n code: 'B03',\n label: 'Temporary unemployment — economic',\n days: 11,\n amount: '€ 643,50',\n },\n { code: 'C12', label: 'Commuting allowance', days: 9, amount: '€ 100,65' },\n ],\n};\n\n// -----------------------------------------------------------------------------\n// Inline row editing (fourth example)\n// -----------------------------------------------------------------------------\n\n/**\n * A row of the inline-editing example.\n *\n * `total` is a number here, not the pre-formatted string the expandable\n * examples use: it has to round-trip through a form control, so view mode\n * formats it with the currency pipe instead.\n */\ninterface EditableBenefitRow {\n /** Identity. Never editable. */\n readonly id: number;\n name: string;\n office: string;\n /** Derived from the payment run, so read-only for a business reason. */\n readonly period: string;\n /** Editable through a datepicker. A real `Date`, not a formatted string. */\n paidOn: Date;\n total: number;\n /** A row that cannot be edited at all — its pen is disabled. */\n readonly locked?: boolean;\n}\n\n/** The form behind one row in edit mode; only the editable columns appear. */\ntype InlineEditForm = FormGroup<{\n name: FormControl<string>;\n office: FormControl<string>;\n paidOn: FormControl<Date>;\n total: FormControl<number>;\n}>;\n\n/** Options of the `office` select. */\nconst EDIT_OFFICES: readonly string[] = [\n 'Brussel',\n 'Antwerpen',\n 'Liège',\n 'Gent',\n 'Brugge',\n 'Namur',\n];\n\nconst EDITABLE_ROWS: readonly EditableBenefitRow[] = [\n {\n id: 1,\n name: 'Janssens, Marie',\n office: 'Brussel',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 3),\n total: 1284.6,\n },\n {\n id: 2,\n name: 'Peeters, Tom',\n office: 'Antwerpen',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 5),\n total: 962,\n },\n {\n id: 3,\n name: 'Dubois, Claire',\n office: 'Liège',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 5),\n total: 1431.25,\n },\n {\n id: 4,\n name: 'Van Damme, Lars',\n office: 'Gent',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 10),\n total: 744.15,\n },\n // Already paid out, so it is shown but cannot be corrected here.\n {\n id: 5,\n name: 'Mertens, Hind',\n office: 'Brugge',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 2),\n total: 0,\n locked: true,\n },\n];\n\n@Component({\n selector: 'app-demo-table',\n templateUrl: './demo-table.component.html',\n styleUrls: ['./demo-table.component.scss'],\n\n standalone: true,\n imports: [\n ReactiveFormsModule,\n FormsModule,\n MatCheckbox,\n MatChipsModule,\n MatFormField,\n MatIconButton,\n MatInput,\n MatLabel,\n MatMenu,\n MatMenuTrigger,\n MatSortModule,\n MatSuffix,\n MatTableModule,\n MatDialogModule,\n MatIcon,\n OnemrvaMatPaginatorModule,\n OnemrvaMatEmptyRowComponent,\n OnemrvaMatMessageBoxModule,\n OnemrvaMatPaginatorComponent,\n MatMenuItem,\n MatButton,\n OnemrvaExpandableTableIconComponent,\n MatSelectModule,\n MatError,\n MatTooltip,\n MatDatepickerModule,\n OnemrvaDateFormatDirective,\n CurrencyPipe,\n DatePipe,\n ],\n // Native `Date` adapter + the DD/MM/YYYY formats the house datepicker uses.\n // Scoped to this component, so the other examples on the page are untouched.\n providers: [onemrvaDateNativeProvider()],\n encapsulation: ViewEncapsulation.None,\n})\nexport class DemoTableComponent\n extends DemoComponentBase\n implements AfterViewInit\n{\n readonly paginator = viewChild.required(OnemrvaMatPaginatorComponent);\n\n readonly sort = viewChild.required(MatSort);\n\n readonly table = viewChild.required(MatTable);\n\n readonly matCheckboxes = viewChildren<MatCheckbox>('checkboxes');\n\n dataSource: ListDataSource;\n\n // Checked checkbox values' list\n public checkboxes: number[] = [];\n\n // Check all checkbox statuses\n public undetermined = false;\n public checked = false;\n\n // all columns first level\n allColumns = ['check', 'id', 'name', 'description', 'suspension', 'more'];\n // all columns second level\n allSubColumns = [\n 'check',\n 'id',\n 'name',\n 'description',\n 'delay',\n 'days',\n 'more',\n ];\n // removable columns\n columns = ['id', 'name', 'description'];\n\n // Filter form control\n search: FormControl = new FormControl('');\n\n // ---------------------------------------------------------------------------\n // Expandable row example (second table)\n //\n // New state is signal-based per `.junie/guidelines.md`; the plain mutable\n // fields above belong to the first table and are left as they are.\n // ---------------------------------------------------------------------------\n\n /** Rows of the expandable-row example. */\n readonly benefitRows = BENEFIT_ROWS;\n\n /** Columns of the expandable-row example; its length is the detail colspan. */\n readonly benefitColumns = [\n 'id',\n 'name',\n 'office',\n 'period',\n 'total',\n 'expand',\n ];\n\n /** The single column of the detail row, which spans the whole table. */\n readonly detailColumns = ['detail'];\n\n /**\n * Ids of every open row.\n *\n * A set rather than a single id: several rows may be open at the same time,\n * so two rows can be compared without collapsing one of them.\n */\n private readonly expandedIds = signal<ReadonlySet<number>>(new Set<number>());\n\n /** How many rows are open; drives the \"Collapse all\" control. */\n readonly expandedCount = computed(() => this.expandedIds().size);\n\n /**\n * Row predicate for the detail row definition.\n *\n * A detail row exists only for rows that have sub-rows — an empty one would\n * otherwise be rendered (and, without the height reset in the .scss, be\n * visible) under every row. For rows that do have sub-rows the detail row\n * stays in the DOM while closed, so that its wrapper has something to run the\n * open/close CSS transition on.\n * @param _index unused row index required by the `when` signature\n * @param row\n */\n readonly hasDetailRow = (_index: number, row: BenefitRow): boolean =>\n this.hasDetails(row);\n\n /**\n * Does this row have sub-rows to reveal?\n * @param row\n */\n hasDetails(row: BenefitRow): boolean {\n return this.detailsFor(row.id).length > 0;\n }\n\n /**\n * Is the row with this id expanded?\n * @param id\n */\n isExpanded(id: number): boolean {\n return this.expandedIds().has(id);\n }\n\n /**\n * Open a collapsed row, or collapse an open one.\n * @param id\n */\n toggleExpanded(id: number): void {\n this.expandedIds.update(ids => {\n const next = new Set(ids);\n if (!next.delete(id)) {\n next.add(id);\n }\n return next;\n });\n }\n\n /**\n * Collapse every open row.\n */\n collapseAll(): void {\n this.expandedIds.set(new Set<number>());\n }\n\n /**\n * Sub-rows of the row with this id.\n * @param id\n */\n detailsFor(id: number): readonly BenefitDetailRow[] {\n return BENEFIT_DETAILS[id] ?? [];\n }\n\n /**\n * DOM id of a detail row, so the toggle button can point `aria-controls` at it.\n * @param id\n */\n detailRowId(id: number): string {\n return `demo-table-benefit-detail-${id}`;\n }\n\n // ---------------------------------------------------------------------------\n // Expandable rows as real rows (third table)\n //\n // Same data and the same toggle as the example above, but a sub-row is a real\n // <tr mat-row> in the same table rather than a grid inside one wide detail\n // cell. So the sub-rows sit in the table's own column layout, and the table\n // has one flat row array that grows and shrinks as rows are opened.\n // ---------------------------------------------------------------------------\n\n /** Columns of a parent row. */\n readonly treeColumns = [\n 'tree-id',\n 'tree-name',\n 'tree-office',\n 'tree-period',\n 'tree-total',\n 'tree-expand',\n ];\n\n /**\n * Columns of a sub-row.\n *\n * A separate set rather than the parent's, so neither cell template has to\n * branch on the row type. The cells still line up: column widths come from\n * the table layout, which spans every row whatever defined them.\n */\n readonly treeDetailColumns = [\n 'tree-detail-code',\n 'tree-detail-label',\n 'tree-detail-office',\n 'tree-detail-days',\n 'tree-detail-amount',\n 'tree-detail-expand',\n ];\n\n /** Ids of every open row of the third table. */\n private readonly treeExpandedIds = signal<ReadonlySet<number>>(\n new Set<number>(),\n );\n\n /** How many rows are open; drives this example's \"Collapse all\" control. */\n readonly treeExpandedCount = computed(() => this.treeExpandedIds().size);\n\n /**\n * The rows to render: every parent, each followed by its sub-rows while it is\n * open.\n *\n * `groupIndex` is the parent's index in the data, so the stripe alternates per\n * parent and a whole group keeps one colour — see the .scss. `lastInGroup`\n * marks the row that closes a group, which is the only row of it that draws a\n * bottom border.\n */\n readonly treeRows = computed<readonly BenefitTreeRow[]>(() => {\n const open = this.treeExpandedIds();\n\n return this.benefitRows.flatMap((row, groupIndex) => {\n const details = open.has(row.id) ? this.detailsFor(row.id) : [];\n\n return [\n {\n kind: 'parent',\n key: `parent-${row.id}`,\n groupIndex,\n lastInGroup: details.length === 0,\n row,\n } satisfies BenefitTreeRow,\n ...details.map(\n (detail, index) =>\n ({\n kind: 'child',\n key: `child-${row.id}-${detail.code}`,\n groupIndex,\n lastInGroup: index === details.length - 1,\n parent: row,\n detail,\n }) satisfies BenefitTreeRow,\n ),\n ];\n });\n });\n\n /**\n * Row predicate selecting the sub-row template; parents fall through to the\n * default row definition.\n * @param _index unused row index required by the `when` signature\n * @param entry\n */\n readonly isTreeChildRow = (_index: number, entry: BenefitTreeRow): boolean =>\n entry.kind === 'child';\n\n /**\n * Identity of a rendered row, so opening one row does not re-create the others.\n * @param _index unused row index required by the `trackBy` signature\n * @param entry\n */\n readonly treeTrackBy = (_index: number, entry: BenefitTreeRow): string =>\n entry.key;\n\n /**\n * Is the row with this id expanded in the third table?\n * @param id\n */\n isTreeExpanded(id: number): boolean {\n return this.treeExpandedIds().has(id);\n }\n\n /**\n * Open a collapsed row of the third table, or collapse an open one.\n * @param id\n */\n toggleTreeExpanded(id: number): void {\n this.treeExpandedIds.update(ids => {\n const next = new Set(ids);\n if (!next.delete(id)) {\n next.add(id);\n }\n return next;\n });\n }\n\n /**\n * Collapse every open row of the third table.\n */\n collapseAllTree(): void {\n this.treeExpandedIds.set(new Set<number>());\n }\n\n /**\n * DOM id of one sub-row.\n * @param id id of the parent row\n * @param code code of the sub-row\n */\n treeChildRowId(id: number, code: string): string {\n return `demo-table-tree-${id}-${code}`;\n }\n\n /**\n * The ids of every sub-row of a parent, for the toggle's `aria-controls`.\n *\n * `aria-controls` takes a list, which is what a row controlling several rows\n * needs. The ids are only in the DOM while the row is open; naming an absent\n * id is how a collapsed control is normally described.\n * @param id\n */\n treeChildRowIds(id: number): string {\n return this.detailsFor(id)\n .map(detail => this.treeChildRowId(id, detail.code))\n .join(' ');\n }\n\n // ---------------------------------------------------------------------------\n // Inline row editing (fourth table)\n //\n // A pen switches one row into edit mode; only `name`, `office` and `total`\n // become controls, `id` and `period` keep rendering their value. Several rows\n // can be open at once, each with its own form, so validity and cancel are\n // row-scoped and nothing is shared between open rows.\n // ---------------------------------------------------------------------------\n\n /** Rows of the inline-editing example; a save writes back here. */\n private readonly editRows =\n signal<readonly EditableBenefitRow[]>(EDITABLE_ROWS);\n\n /** Read-only view of the rows for the template. */\n readonly inlineRows = this.editRows.asReadonly();\n\n /** Options of the `office` select. */\n readonly offices = EDIT_OFFICES;\n\n /** Columns of the inline-editing example. */\n readonly inlineColumns = [\n 'inline-id',\n 'inline-name',\n 'inline-office',\n 'inline-period',\n 'inline-paid-on',\n 'inline-total',\n 'inline-actions',\n ];\n\n /**\n * The house datepicker header, referenced from the template.\n *\n * Same wiring as the datepicker demo: `[calendarHeaderComponent]` takes the\n * component type, which a template can only reach through a field.\n */\n protected readonly OnemrvaMatDatepickerHeaderComponent =\n OnemrvaMatDatepickerHeaderComponent;\n\n /**\n * Upper bound of the payment-date picker.\n *\n * A payment cannot be dated in the future, so the calendar disables those\n * days outright rather than only rejecting them on save.\n */\n readonly today = new Date();\n\n /** Ids of every row currently in edit mode. */\n private readonly editingIds = signal<ReadonlySet<number>>(new Set<number>());\n\n /**\n * The form of every open row.\n *\n * A plain `Map` and not a signal: a `FormGroup` is a mutable object with its\n * own change streams, so putting it in signal state would only invite it to be\n * re-created on every read. `editingIds` is what the template reacts to.\n */\n private readonly editForms = new Map<number, InlineEditForm>();\n\n /** How many rows are open; drives the bulk bar. */\n readonly editingCount = computed(() => this.editingIds().size);\n\n private readonly announcer = inject(LiveAnnouncer);\n private readonly injector = inject(Injector);\n\n /**\n * Is this row in edit mode?\n * @param id\n */\n isEditing(id: number): boolean {\n return this.editingIds().has(id);\n }\n\n /**\n * The form of an open row.\n *\n * Only ever called from inside an `@if (isEditing(...))`, so the entry exists.\n * @param id\n */\n formFor(id: number): InlineEditForm {\n const form = this.editForms.get(id);\n if (!form) {\n throw new Error(`No inline-edit form for row ${id}.`);\n }\n return form;\n }\n\n /**\n * Put a row into edit mode and focus its first editable control.\n *\n * A locked row is a no-op: its pen is rendered disabled, and this guards the\n * programmatic path as well.\n * @param row\n */\n startEdit(row: EditableBenefitRow): void {\n if (row.locked || this.isEditing(row.id)) {\n return;\n }\n\n this.editForms.set(row.id, this.buildForm(row));\n this.editingIds.update(ids => new Set(ids).add(row.id));\n\n void this.announcer.announce(`Editing ${row.name}`);\n this.focusAfterRender(this.firstControlId(row.id));\n }\n\n /**\n * Discard a row's changes and leave edit mode.\n *\n * The demo discards without confirming even when the form is dirty; a real\n * screen would ask first.\n * @param id\n */\n cancelEdit(id: number): void {\n if (!this.isEditing(id)) {\n return;\n }\n\n this.editForms.delete(id);\n this.editingIds.update(ids => {\n const next = new Set(ids);\n next.delete(id);\n return next;\n });\n\n void this.announcer.announce('Changes discarded');\n this.focusAfterRender(this.editButtonId(id));\n }\n\n /**\n * Commit a row if its form is valid, otherwise leave it open with its errors\n * shown.\n * @param id\n * @returns whether the row was committed\n */\n saveEdit(id: number): boolean {\n const form = this.editForms.get(id);\n if (!form) {\n return false;\n }\n\n // Validation runs here and not on every keystroke — see `.junie/guidelines.md`.\n form.markAllAsTouched();\n if (form.invalid) {\n void this.announcer.announce('Row has errors and was not saved');\n return false;\n }\n\n const value = form.getRawValue();\n this.editRows.update(rows =>\n rows.map(row => (row.id === id ? { ...row, ...value } : row)),\n );\n\n this.editForms.delete(id);\n this.editingIds.update(ids => {\n const next = new Set(ids);\n next.delete(id);\n return next;\n });\n\n void this.announcer.announce('Row saved');\n this.focusAfterRender(this.editButtonId(id));\n return true;\n }\n\n /**\n * Commit every valid open row.\n *\n * Rows that fail validation stay open with their errors shown rather than\n * being silently dropped.\n */\n saveAll(): void {\n const ids = [...this.editingIds()];\n const failed = ids.filter(id => !this.saveEdit(id));\n\n if (failed.length > 0) {\n void this.announcer.announce(\n `${ids.length - failed.length} rows saved, ${failed.length} still have errors`,\n );\n this.focusAfterRender(this.firstControlId(failed[0] as number));\n }\n }\n\n /**\n * Discard every open row.\n */\n cancelAll(): void {\n this.editForms.clear();\n this.editingIds.set(new Set<number>());\n void this.announcer.announce('All changes discarded');\n }\n\n /** DOM id of a row's pen button, so focus can return to it. */\n editButtonId(id: number): string {\n return `demo-table-inline-edit-${id}`;\n }\n\n /** DOM id of the first editable control of a row. */\n firstControlId(id: number): string {\n return `demo-table-inline-name-${id}`;\n }\n\n /**\n * Build the form of one row.\n *\n * `nonNullable` throughout, so `getRawValue()` matches `EditableBenefitRow`\n * without a null-check per field.\n * @param row\n */\n private buildForm(row: EditableBenefitRow): InlineEditForm {\n return new FormGroup({\n name: new FormControl(row.name, {\n nonNullable: true,\n validators: [Validators.required, Validators.maxLength(60)],\n }),\n office: new FormControl(row.office, {\n nonNullable: true,\n validators: [Validators.required],\n }),\n // A fresh `Date`: the picker writes a new instance on pick, but typing an\n // invalid date leaves the control's value alone, and sharing the object\n // with the row would let a cancelled edit still show up in view mode.\n paidOn: new FormControl(new Date(row.paidOn), {\n nonNullable: true,\n validators: [Validators.required],\n }),\n total: new FormControl(row.total, {\n nonNullable: true,\n validators: [Validators.required, Validators.min(0)],\n }),\n });\n }\n\n /**\n * Move focus to an element once the template has caught up.\n *\n * Reading the DOM directly is the exception the guidelines allow for focus\n * management: the element does not exist until the row has re-rendered, so\n * there is nothing for a `viewChild` to resolve to at call time either.\n * @param elementId\n */\n private focusAfterRender(elementId: string): void {\n afterNextRender(() => document.getElementById(elementId)?.focus(), {\n injector: this.injector,\n });\n }\n\n dialog = inject(MatDialog);\n elementsService = inject(ElementsService);\n\n constructor() {\n super();\n // Create a new DataSource\n this.dataSource = new ListDataSource(this.elementsService);\n\n // When user start filtering, we need to update the master checkbox\n this.search.valueChanges.pipe(takeUntilDestroyed()).subscribe(search => {\n console.log(search);\n setTimeout(() => {\n this.updateToggleAllStatus();\n }, 100);\n });\n }\n\n /**\n *\n */\n showHideColumns() {\n const panelClass: string[] = ['large'];\n const dialogRef = this.dialog.open(DemoTableDialogContentComponent, {\n data: {\n allColumns: this.allColumns,\n allSubColumns: this.allSubColumns,\n columns: this.columns,\n },\n panelClass,\n });\n dialogRef\n .afterClosed()\n .pipe(take(1))\n .subscribe((values: boolean[]) => {\n if (values !== undefined) {\n this.allColumns = ['check'];\n this.allSubColumns = ['check'];\n values.forEach((value, index) => {\n if (value) {\n this.allColumns.push(this.columns[index]);\n this.allSubColumns.push(this.columns[index]);\n }\n });\n this.allColumns.push('suspension');\n this.allColumns.push('more');\n this.allSubColumns.push('delay');\n this.allSubColumns.push('days');\n this.allSubColumns.push('more');\n }\n });\n }\n\n /**\n * We set the datasource and paginator\n */\n ngAfterViewInit(): void {\n this.dataSource.sort = this.sort();\n this.dataSource.paginator = this.paginator();\n this.dataSource.search = this.search;\n this.table().dataSource = this.dataSource;\n }\n\n /**\n * returns the column visible state\n * @param column\n */\n columnIsVisible(column: string) {\n return this.allColumns.includes(column);\n }\n /**\n * returns the column visible state\n * @param column\n */\n subColumnIsVisible(column: string) {\n return this.allSubColumns.includes(column);\n }\n\n /**\n * Edit row action\n * @param row\n */\n edit(row: ListItem) {\n alert(`Edit row n°${row.id}: ${row.name}`);\n }\n\n /**\n * Delete row action\n * @param row\n */\n delete(row: ListItem) {\n this.dataSource.delete(row.id);\n }\n\n /**\n * Share row action\n * @param row\n */\n share(row: ListItem) {\n alert(`Share row n°${row.id}: ${row.name}`);\n }\n\n /**\n * clear search\n */\n clear() {\n this.search.setValue('');\n }\n\n /**\n * paginator changes page\n * @param event\n */\n changePage(event: PageEvent) {\n console.log(event);\n // timeout necessary to take the changes in account\n setTimeout(() => {\n this.updateToggleAllStatus();\n }, 100);\n }\n\n /**\n * Is the notice visible\n */\n showNotice() {\n return this.checkboxes.length > 0;\n }\n\n /**\n * is a checkbox checked\n * @param id\n */\n isChecked(id: number) {\n return this.checkboxes.includes(id);\n }\n\n /**\n * Toggle One checkbox\n * @param $event\n * @param id\n */\n toggleOne($event: MatCheckboxChange, id: number) {\n const checked = $event.checked;\n this.toggleCheckBox(id, checked);\n this.updateToggleAllStatus();\n }\n\n /**\n * toggle the checkbox with checks\n * @param id\n * @param checked\n * @private\n */\n private toggleCheckBox(id: number, checked: boolean) {\n if (checked) {\n if (!this.checkboxes.includes(id)) {\n this.checkboxes.push(id);\n }\n } else {\n this.checkboxes.splice(this.checkboxes.indexOf(id), 1);\n }\n }\n\n /**\n * Toggle all visible checkboxes\n * @param $event\n */\n toggleAllVisible($event: MatCheckboxChange) {\n const checked = $event.checked;\n this.checked = checked;\n\n this.matCheckboxes().forEach(matCheckbox => {\n const id = parseInt(matCheckbox.value);\n this.toggleCheckBox(id, checked);\n });\n this.updateToggleAllStatus();\n }\n\n /**\n * Update the status of the toggle all checkbox to reflect on what is checked (checked, unchecked, undetermined)\n */\n updateToggleAllStatus() {\n this.undetermined = false;\n this.checked = false;\n let cnt = 0;\n\n this.matCheckboxes().forEach(matCheckbox => {\n const id = parseInt(matCheckbox.value);\n if (this.checkboxes.includes(id)) {\n cnt++;\n }\n });\n if (cnt > 0) {\n if (cnt === this.matCheckboxes().length) {\n this.checked = true;\n } else {\n this.undetermined = true;\n }\n }\n }\n\n /**\n * Select all checkbox in the full datasource's list\n */\n selectAll() {\n this.dataSource\n .all()\n .pipe(take(1))\n .subscribe(ids => {\n this.checkboxes = ids;\n this.updateToggleAllStatus();\n });\n }\n\n /**\n * Deselect all checkboxes\n */\n deSelectAll() {\n this.checkboxes = [];\n this.updateToggleAllStatus();\n }\n}\n"}
|
|
1
|
+
{"demo-table-datasource.ts":"import { DataSource } from '@angular/cdk/collections';\nimport { MatSort } from '@angular/material/sort';\nimport { map, tap } from 'rxjs/operators';\nimport {\n BehaviorSubject,\n merge,\n Observable,\n startWith,\n Subject,\n switchMap,\n} from 'rxjs';\nimport { OnemrvaMatPaginatorComponent } from '@onemrvapublic/design-system/mat-paginator';\nimport { FormControl } from '@angular/forms';\nimport { ElementsService, ListItem } from '../../_services/elements.service';\n\n/**\n * Data source for the List view. This class should\n * encapsulate all logic for fetching and manipulating the displayed data\n * (including sorting, pagination, and filtering).\n */\nexport class ListDataSource extends DataSource<ListItem> {\n paginator!: OnemrvaMatPaginatorComponent;\n search!: FormControl;\n sort: MatSort | undefined;\n data$: BehaviorSubject<ListItem[]> = new BehaviorSubject<ListItem[]>([]);\n prevSearch = '';\n refresh$ = new Subject();\n\n constructor(private elementsService: ElementsService) {\n super();\n }\n\n /**\n * Connect this data source to the table. The table will only update when\n * the returned stream emits new items.\n * @returns A stream of the items to be rendered.\n */\n connect(): Observable<ListItem[]> {\n if (this.sort) {\n // Combine everything that affects the rendered data into one update\n // stream for the data-table to consume.\n return merge(\n this.sort.sortChange, // we listen to sort change\n this.search.valueChanges, // we listen to filter change\n this.paginator.pageSize$, // we listen to page size change\n this.paginator.pageIndex$, // we listen to page index change\n this.refresh$, // used to force a refresh\n ).pipe(\n startWith({}),\n tap(() => {\n // if search changed, we reset the pageIndex\n if (\n this.search.getRawValue().trim() !== '' &&\n this.search.getRawValue() !== this.prevSearch\n ) {\n this.paginator.resetPageIndex();\n this.prevSearch = this.search.getRawValue();\n }\n }),\n switchMap(() => {\n const active: keyof ListItem =\n (this.sort?.active as keyof ListItem) || 'id';\n const direction = this.sort?.direction || 'asc';\n // We return the api call observable called with the correct parameters\n return this.elementsService\n .getElementsList(\n active,\n direction,\n this.paginator.pageIndex,\n this.paginator.pageSize,\n this.search.getRawValue(),\n )\n .pipe(\n // We map the response to the Interface we need/want\n map(response => {\n this.paginator.length = response.length;\n this.paginator.pageIndex = response.page;\n this.paginator.pageSize = response.pageSize;\n return response.list;\n }),\n );\n }),\n );\n } else {\n throw Error('Please set the sort on the data source before connecting.');\n }\n }\n\n /**\n * Called when the table is being destroyed. Use this function, to clean up\n * any open connections or free any held resources that were set up during connect.\n */\n disconnect(): void {\n console.log('disconnect');\n }\n\n /**\n * If user click on select all items (outside current page),\n * this service functions returns the IDs of all items in datasource\n */\n all() {\n return this.elementsService.getAllElementsIds(this.search.getRawValue());\n }\n\n /**\n * If user click to delete an item, we tell the api and then refresh the list.\n * @param id\n */\n delete(id: number) {\n this.elementsService.deleteElement(id);\n this.refresh$.next('');\n }\n}\n","demo-table.component.html":"<h1>Table</h1>\n<mat-form-field class=\"search\">\n <mat-label>Search</mat-label>\n <input matInput [formControl]=\"search\" />\n @if (search.getRawValue()) {\n <button matSuffix mat-icon-button aria-label=\"Clear\" (click)=\"clear()\">\n <mat-icon>close</mat-icon>\n </button>\n }\n</mat-form-field>\n<p><a tabindex=\"0\" (click)=\"showHideColumns()\">Hide/Show column</a></p>\n<table\n mat-table\n class=\"full-width-table multi-table-row-header\"\n matSort\n aria-label=\"Elements\"\n>\n <tr mat-header-row *matHeaderRowDef=\"allColumns\"></tr>\n <tr mat-header-row class=\"compact\" *matHeaderRowDef=\"['delay', 'days']\"></tr>\n\n <!-- Notice Row -->\n <tr\n mat-header-row\n class=\"onemrva-notice-row\"\n [class]=\"showNotice() ? '' : 'hidden'\"\n *matHeaderRowDef=\"showNotice() ? ['notice'] : []\"\n ></tr>\n @if (showNotice()) {\n <ng-container matColumnDef=\"notice\">\n <th *matHeaderCellDef colspan=\"7\">\n You have selected {{ checkboxes.length }} items. \n @if (checkboxes.length < paginator.length) {\n <a tabindex=\"0\" (click)=\"selectAll()\"\n >Select {{ paginator.length }} items</a\n > \n }\n @if (checkboxes.length >= 0) {\n <a tabindex=\"0\" (click)=\"deSelectAll()\"\n >Deselect {{ checkboxes.length }} items</a\n >\n }\n </th>\n </ng-container>\n }\n <!-- Notice Row -->\n\n <tr mat-row *matRowDef=\"let _; columns: allSubColumns\"></tr>\n\n <tr *matNoDataRow class=\"noresult\">\n <td colspan=\"7\">\n <onemrva-mat-empty-row (clear)=\"clear()\" />\n </td>\n </tr>\n\n <!-- checkbox Column -->\n <ng-container matColumnDef=\"check\">\n <th mat-header-cell *matHeaderCellDef [attr.rowspan]=\"2\">\n <div>\n <mat-checkbox\n aria-label=\"Check All\"\n value=\"toggleAll\"\n [checked]=\"checked\"\n [indeterminate]=\"undetermined\"\n (change)=\"toggleAllVisible($event)\"\n />\n </div>\n </th>\n <td mat-cell *matCellDef=\"let row\">\n <mat-checkbox\n #checkboxes\n aria-label=\"Check row {{ row.id }}\"\n [value]=\"row.id\"\n [checked]=\"isChecked(row.id)\"\n (change)=\"toggleOne($event, row.id)\"\n />\n </td>\n </ng-container>\n\n <!-- Id Column -->\n @if (columnIsVisible('id')) {\n <ng-container matColumnDef=\"id\" sticky>\n <th\n mat-header-cell\n class=\"compact\"\n *matHeaderCellDef\n mat-sort-header\n [attr.rowspan]=\"2\"\n >\n Id\n </th>\n <td mat-cell class=\"compact\" *matCellDef=\"let row\">\n {{ row.id }}\n </td>\n </ng-container>\n }\n\n <!-- Name Column -->\n @if (columnIsVisible('name')) {\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef mat-sort-header [attr.rowspan]=\"2\">\n Name\n </th>\n <td id=\"{{ row.name }}\" mat-cell *matCellDef=\"let row\">\n <a href=\"/#table\">{{ row.name }}</a>\n </td>\n </ng-container>\n }\n\n <!-- Description Column -->\n @if (columnIsVisible('description')) {\n <ng-container matColumnDef=\"description\">\n <th\n mat-header-cell\n *matHeaderCellDef\n mat-sort-header=\"description\"\n [attr.rowspan]=\"2\"\n class=\"d-table-cell-large description-cell\"\n >\n Description\n </th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"d-table-cell-large description-cell\"\n >\n <span></span>\n {{ row.description }}\n </td></ng-container\n >\n }\n @if (columnIsVisible('suspension')) {\n <!-- Suspension Column -->\n <ng-container matColumnDef=\"suspension\">\n <th\n mat-header-cell\n *matHeaderCellDef\n [attr.colspan]=\"2\"\n class=\"align-center compact table__border-left table__border-right\"\n >\n Suspension\n </th>\n <!-- This column doesn't generate <td> items, so no need to add a definition for them -->\n </ng-container>\n }\n\n @if (subColumnIsVisible('delay')) {\n <!-- Delay Column -->\n <ng-container matColumnDef=\"delay\">\n <th\n mat-header-cell\n *matHeaderCellDef\n class=\"compact align-center table__border-left\"\n >\n Delay\n </th>\n <td mat-cell *matCellDef=\"let _\" class=\"align-center table__border-left\">\n 1\n </td>\n </ng-container>\n }\n\n @if (subColumnIsVisible('days')) {\n <!-- Days Column -->\n <ng-container matColumnDef=\"days\">\n <th\n mat-header-cell\n *matHeaderCellDef\n class=\"compact align-center table__border-right\"\n >\n Days\n </th>\n <td mat-cell *matCellDef=\"let _\" class=\"align-center table__border-right\">\n 2\n </td>\n </ng-container>\n }\n\n @if (columnIsVisible('more')) {\n <!-- Menu Column -->\n <ng-container matColumnDef=\"more\">\n <th mat-header-cell class=\"compact\" *matHeaderCellDef [attr.rowspan]=\"2\">\n Menu\n </th>\n <td mat-cell class=\"compact align-right\" *matCellDef=\"let row\">\n <button mat-icon-button [matMenuTriggerFor]=\"menu\" aria-label=\"More\">\n <mat-icon>more_vert</mat-icon>\n </button>\n <mat-menu #menu=\"matMenu\">\n <button mat-menu-item (click)=\"edit(row)\" aria-label=\"Edit\">\n <mat-icon>edit</mat-icon>\n Edit\n </button>\n <button mat-menu-item (click)=\"delete(row)\" aria-label=\"Delete\">\n <mat-icon>delete</mat-icon>\n Delete\n </button>\n <button mat-menu-item (click)=\"share(row)\" aria-label=\"Share\">\n <mat-icon>share</mat-icon>\n Share\n </button>\n </mat-menu>\n </td>\n </ng-container>\n }\n</table>\n<div class=\"my-m\">\n <onemrva-mat-paginator\n [pageSizeSelector]=\"true\"\n [pageSizeOptions]=\"[5, 10, 20, 50]\"\n [pageSizeDefaultOption]=\"5\"\n [pageSize]=\"5\"\n previousKey=\"Previous\"\n nextKey=\"Next\"\n (page)=\"changePage($event)\"\n #paginator\n />\n</div>\n\n<h2>Expandable row</h2>\n<p>\n A row can expand to reveal its own sub-rows. Several rows can be open at the\n same time, so two payments can be compared side by side. A row without\n sub-rows has no toggle to press and no detail row of its own.\n</p>\n<p>\n <button\n mat-button\n type=\"button\"\n aria-label=\"Collapse all expanded rows\"\n [disabled]=\"expandedCount() === 0\"\n (click)=\"collapseAll()\"\n >\n Collapse all ({{ expandedCount() }})\n </button>\n</p>\n\n<!--\n The table is kept a real `display: table` (see the .scss) and scrolls from\n this wrapper on narrow screens instead of from the table element itself.\n-->\n<div class=\"demo-expandable-table-scroll\">\n <table\n mat-table\n multiTemplateDataRows\n class=\"full-width-table demo-expandable-table\"\n aria-label=\"Benefit payments per beneficiary\"\n [dataSource]=\"benefitRows\"\n >\n <tr mat-header-row *matHeaderRowDef=\"benefitColumns\"></tr>\n\n <!--\n Data row. The stripe comes from `dataIndex` and not from the row's position\n in the DOM, because the detail rows shift that position — see the .scss.\n -->\n <tr\n mat-row\n *matRowDef=\"let row; columns: benefitColumns; let i = dataIndex\"\n class=\"demo-expandable-row\"\n [class.demo-expandable-row--striped]=\"i % 2 === 0\"\n [class.demo-expandable-row--open]=\"isExpanded(row.id)\"\n [class.demo-expandable-row--error-row]=\"hasRowError(row.id)\"\n ></tr>\n\n <!--\n Detail row. Rendered only for rows that actually have sub-rows — `when:`\n keeps an empty detail row out of the DOM entirely instead of rendering a\n blank one. For rows that do have sub-rows it stays rendered while closed,\n so its wrapper has something to run the open/close transition on; the .scss\n collapses it to zero height.\n -->\n <tr\n mat-row\n *matRowDef=\"let row; columns: detailColumns; when: hasDetailRow\"\n class=\"demo-expandable-detail-row\"\n [attr.id]=\"detailRowId(row.id)\"\n ></tr>\n\n <!-- Expand toggle Column -->\n <ng-container matColumnDef=\"expand\">\n <th mat-header-cell *matHeaderCellDef class=\"demo-expandable-toggle-cell\">\n Details\n </th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-expandable-toggle-cell\">\n <!-- A row without sub-rows has nothing to reveal, so it gets no toggle. -->\n @if (hasDetails(row)) {\n <onemrva-expandable-table-icon\n [isExpanded]=\"isExpanded(row.id)\"\n [detailRowId]=\"detailRowId(row.id)\"\n (expandedChange)=\"toggleExpanded(row.id)\"\n />\n }\n </td>\n </ng-container>\n\n <!-- Id Column -->\n <ng-container matColumnDef=\"id\">\n <th mat-header-cell *matHeaderCellDef class=\"compact\">Id</th>\n <td mat-cell *matCellDef=\"let row\" class=\"compact\">\n <span class=\"text-only-cell\">{{ row.id }}</span>\n </td>\n </ng-container>\n\n <!-- Name Column -->\n <ng-container matColumnDef=\"name\">\n <th mat-header-cell *matHeaderCellDef>Beneficiary</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.name }}</td>\n </ng-container>\n\n <!-- Office Column -->\n <ng-container matColumnDef=\"office\">\n <th mat-header-cell *matHeaderCellDef>Office</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.office }}</td>\n </ng-container>\n\n <!-- Period Column -->\n <ng-container matColumnDef=\"period\">\n <th mat-header-cell *matHeaderCellDef>Period</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.period }}</td>\n </ng-container>\n\n <!-- Total Column -->\n <ng-container matColumnDef=\"total\">\n <th mat-header-cell *matHeaderCellDef class=\"align-right\">Total</th>\n <td mat-cell *matCellDef=\"let row\" class=\"align-right\">\n {{ row.total }}\n </td>\n </ng-container>\n\n <!--\n Detail Column. A plain grid of divs rather than `onemrva-mat-panel` (its\n chrome is wrong inside a row) or a nested `mat-table`.\n -->\n <ng-container matColumnDef=\"detail\">\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"demo-expandable-detail-cell\"\n [class.demo-expandable-detail-cell--open]=\"isExpanded(row.id)\"\n [attr.colspan]=\"benefitColumns.length\"\n >\n <div class=\"demo-expandable-detail\">\n <div class=\"demo-expandable-detail__inner\">\n <div\n class=\"demo-expandable-detail__grid\"\n role=\"table\"\n [attr.aria-hidden]=\"isExpanded(row.id) ? null : 'true'\"\n [attr.aria-label]=\"'Payment details of ' + row.name\"\n >\n <div class=\"demo-expandable-detail__row\" role=\"row\">\n <div class=\"demo-expandable-detail__header\" role=\"columnheader\">\n Code\n </div>\n <div class=\"demo-expandable-detail__header\" role=\"columnheader\">\n Benefit\n </div>\n <div\n class=\"demo-expandable-detail__header demo-expandable-detail__numeric\"\n role=\"columnheader\"\n >\n Days\n </div>\n <div\n class=\"demo-expandable-detail__header demo-expandable-detail__numeric\"\n role=\"columnheader\"\n >\n Amount\n </div>\n </div>\n @for (detail of detailsFor(row.id); track detail.code) {\n <div class=\"demo-expandable-detail__row\" role=\"row\">\n <div role=\"cell\">{{ detail.code }}</div>\n <div role=\"cell\">{{ detail.label }}</div>\n <div class=\"demo-expandable-detail__numeric\" role=\"cell\">\n {{ detail.days }}\n </div>\n <div class=\"demo-expandable-detail__numeric\" role=\"cell\">\n {{ detail.amount }}\n </div>\n </div>\n }\n </div>\n </div>\n </div>\n </td>\n </ng-container>\n </table>\n</div>\n\n<h2>Expandable row with real rows</h2>\n<p>\n The same toggle, but the sub-rows are real table rows instead of a grid inside\n one wide detail cell, so they sit in the table's own columns. A parent and its\n sub-rows share one stripe and one bottom border, so the group reads as a\n block, and the stripe alternates per parent whether or not any row is open.\n</p>\n<p>\n <button\n mat-button\n type=\"button\"\n aria-label=\"Collapse all expanded rows\"\n [disabled]=\"treeExpandedCount() === 0\"\n (click)=\"collapseAllTree()\"\n >\n Collapse all ({{ treeExpandedCount() }})\n </button>\n</p>\n\n<div class=\"demo-expandable-table-scroll\">\n <table\n mat-table\n class=\"full-width-table demo-tree-table\"\n aria-label=\"Benefit payments per beneficiary, with sub-rows\"\n [dataSource]=\"treeRows()\"\n [trackBy]=\"treeTrackBy\"\n >\n <tr mat-header-row *matHeaderRowDef=\"treeColumns\"></tr>\n\n <!--\n Parent row. The stripe comes from `groupIndex` rather than from the row's\n position in the DOM, because the sub-rows shift that position — see the\n .scss. `lastInGroup` is false while the row is open, which hands the\n group's bottom border to its last sub-row.\n\n No `aria-expanded` on the row itself: on a `role=\"row\"` that attribute is\n only defined inside a `treegrid`, and this is a plain table, so it is\n invalid here (axe `aria-conditional-attr`). Making the table a real\n treegrid would mean owning full grid keyboard navigation, which is not\n what this example is about. The expanded state is already on the toggle\n button inside the row — `onemrva-expandable-table-icon` sets\n `aria-expanded` and `aria-controls` on its own button — which is the\n ordinary disclosure pattern and what the detail-row example above does too.\n -->\n <tr\n mat-row\n *matRowDef=\"let entry; columns: treeColumns\"\n class=\"demo-tree-row\"\n [class.demo-tree-row--striped]=\"entry.groupIndex % 2 === 0\"\n [class.demo-tree-row--grouped]=\"!entry.lastInGroup\"\n ></tr>\n\n <!-- Sub-row. Rendered only while its parent is open; `treeRows()` decides. -->\n <tr\n mat-row\n *matRowDef=\"let entry; columns: treeDetailColumns; when: isTreeChildRow\"\n class=\"demo-tree-row demo-tree-row--child\"\n [class.demo-tree-row--striped]=\"entry.groupIndex % 2 === 0\"\n [class.demo-tree-row--grouped]=\"!entry.lastInGroup\"\n [attr.id]=\"treeChildRowId(entry.parent.id, entry.detail.code)\"\n ></tr>\n\n <!-- Parent columns -->\n <ng-container matColumnDef=\"tree-id\">\n <th mat-header-cell *matHeaderCellDef class=\"compact\">Id</th>\n <td mat-cell *matCellDef=\"let entry\" class=\"compact\">\n {{ entry.row.id }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-name\">\n <th mat-header-cell *matHeaderCellDef>Beneficiary</th>\n <td mat-cell *matCellDef=\"let entry\">{{ entry.row.name }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-office\">\n <th mat-header-cell *matHeaderCellDef>Office</th>\n <td mat-cell *matCellDef=\"let entry\">{{ entry.row.office }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-period\">\n <th mat-header-cell *matHeaderCellDef>Period</th>\n <td mat-cell *matCellDef=\"let entry\">{{ entry.row.period }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-total\">\n <th mat-header-cell *matHeaderCellDef class=\"align-right\">Total</th>\n <td mat-cell *matCellDef=\"let entry\" class=\"align-right\">\n {{ entry.row.total }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-expand\">\n <th mat-header-cell *matHeaderCellDef class=\"demo-tree-toggle-cell\">\n Details\n </th>\n <td mat-cell *matCellDef=\"let entry\" class=\"demo-tree-toggle-cell\">\n <!-- A row without sub-rows has nothing to reveal, so it gets no toggle. -->\n @if (hasDetails(entry.row)) {\n <onemrva-expandable-table-icon\n [isExpanded]=\"isTreeExpanded(entry.row.id)\"\n [detailRowId]=\"treeChildRowIds(entry.row.id)\"\n (expandedChange)=\"toggleTreeExpanded(entry.row.id)\"\n />\n }\n </td>\n </ng-container>\n\n <!--\n Sub-row columns. One per parent column so the cells line up; the code\n takes the Id column, the days take the Period column (they are a\n subdivision of that period) and the Office and Details columns stay empty.\n -->\n <ng-container matColumnDef=\"tree-detail-code\">\n <td mat-cell *matCellDef=\"let entry\" class=\"compact demo-tree-child-cell\">\n {{ entry.detail.code }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-label\">\n <td mat-cell *matCellDef=\"let entry\">{{ entry.detail.label }}</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-office\">\n <td mat-cell *matCellDef=\"let _\"></td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-days\">\n <td mat-cell *matCellDef=\"let entry\">{{ entry.detail.days }} days</td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-amount\">\n <td mat-cell *matCellDef=\"let entry\" class=\"align-right\">\n {{ entry.detail.amount }}\n </td>\n </ng-container>\n\n <ng-container matColumnDef=\"tree-detail-expand\">\n <td mat-cell *matCellDef=\"let _\" class=\"demo-tree-toggle-cell\"></td>\n </ng-container>\n </table>\n</div>\n\n<h2>Inline row editing</h2>\n<p>\n The pen switches one row into edit mode. Only some columns become editable —\n <em>Id</em> is the row's identity and <em>Period</em> comes from the payment\n run, so both keep rendering their value while the rest of the row is a form.\n</p>\n<p>\n Several rows can be open at once. Each one owns its own form, so validity,\n dirtiness and cancel are row-scoped and cancelling one row leaves the others\n untouched. Saving writes to the local signal only — a real screen would call a\n service and handle the failure path — and a dirty row is discarded without\n confirmation, which a real screen would also ask about first.\n</p>\n<p>\n Validation is surfaced on save, not on every keystroke. A row that fails gets\n <code>demo-inline-edit-row--error</code> on the <code><tr></code> as\n well as the per-field <code>mat-error</code>s: an error message makes one cell\n taller than the rest, so the whole row switches to\n <code>vertical-align: top</code> and the cells that have nothing to report\n stay lined up with the ones that do.\n</p>\n\n<!--\n Bulk bar. Mirrors the notice row of the first table, but sits outside the\n table: it acts on rows across the whole page, not on one of them.\n-->\n@if (editingCount() > 0) {\n <p class=\"demo-inline-edit-bar\">\n {{ editingCount() }} row(s) in edit mode. \n <button\n mat-button\n type=\"button\"\n aria-label=\"Save every row in edit mode\"\n (click)=\"saveAll()\"\n >\n Save all\n </button>\n <button\n mat-button\n type=\"button\"\n aria-label=\"Discard every row in edit mode\"\n (click)=\"cancelAll()\"\n >\n Discard all\n </button>\n </p>\n}\n\n<div class=\"demo-inline-edit-table-scroll\">\n <table\n mat-table\n class=\"full-width-table demo-inline-edit-table\"\n [dataSource]=\"inlineRows()\"\n aria-label=\"Benefits, editable in place\"\n >\n <tr mat-header-row *matHeaderRowDef=\"inlineColumns\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: inlineColumns\"\n class=\"demo-inline-edit-row\"\n [class.demo-inline-edit-row--editing]=\"isEditing(row.id)\"\n [class.demo-inline-edit-row--error]=\"hasRowError(row.id)\"\n (keydown.enter)=\"isEditing(row.id) && saveEdit(row.id)\"\n (keydown.escape)=\"cancelEdit(row.id)\"\n ></tr>\n\n <!-- Id Column — read-only in both modes. -->\n <ng-container matColumnDef=\"inline-id\">\n <th mat-header-cell *matHeaderCellDef class=\"compact\">Id</th>\n <td mat-cell *matCellDef=\"let row\" class=\"compact\">{{ row.id }}</td>\n </ng-container>\n\n <!-- Name Column — editable text. -->\n <ng-container matColumnDef=\"inline-name\">\n <th mat-header-cell *matHeaderCellDef>Beneficiary</th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-cell\">\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n matInput\n [id]=\"firstControlId(row.id)\"\n [formControl]=\"formFor(row.id).controls.name\"\n [attr.aria-label]=\"'Beneficiary of row ' + row.id\"\n />\n @if (formFor(row.id).controls.name.hasError('required')) {\n <mat-error>Beneficiary is required</mat-error>\n }\n @if (formFor(row.id).controls.name.hasError('maxlength')) {\n <mat-error>60 characters at most</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.name }}\n }\n </td>\n </ng-container>\n\n <!-- Office Column — editable select over a fixed list. -->\n <ng-container matColumnDef=\"inline-office\">\n <th mat-header-cell *matHeaderCellDef>Office</th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-cell\">\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <mat-select\n [formControl]=\"formFor(row.id).controls.office\"\n [attr.aria-label]=\"'Office of row ' + row.id\"\n >\n @for (office of offices; track office) {\n <mat-option [value]=\"office\">{{ office }}</mat-option>\n }\n </mat-select>\n @if (formFor(row.id).controls.office.hasError('required')) {\n <mat-error>Office is required</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.office }}\n }\n </td>\n </ng-container>\n\n <!-- Period Column — read-only for a business reason, not a technical one. -->\n <ng-container matColumnDef=\"inline-period\">\n <th mat-header-cell *matHeaderCellDef>Period</th>\n <td mat-cell *matCellDef=\"let row\">{{ row.period }}</td>\n </ng-container>\n\n <!--\n Payment date Column — editable through the house datepicker.\n\n Same wiring as the datepicker demo: `onemrvaDateFormat` for the DD/MM/YYYY\n mask, the toggle as a suffix, and `[calendarHeaderComponent]`. The overlay\n the calendar opens in is a CDK overlay outside this row, so its own Escape\n (close the calendar) never reaches the row's Escape (cancel the edit).\n -->\n <ng-container matColumnDef=\"inline-paid-on\">\n <th mat-header-cell *matHeaderCellDef>Paid on</th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-cell\">\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n onemrvaDateFormat\n matInput\n [matDatepicker]=\"paidOnPicker\"\n [max]=\"today\"\n [formControl]=\"formFor(row.id).controls.paidOn\"\n [attr.aria-label]=\"'Payment date of row ' + row.id\"\n />\n <mat-datepicker-toggle matSuffix [for]=\"paidOnPicker\" />\n <mat-datepicker\n #paidOnPicker\n [calendarHeaderComponent]=\"OnemrvaMatDatepickerHeaderComponent\"\n />\n @if (formFor(row.id).controls.paidOn.hasError('required')) {\n <mat-error>Payment date is required</mat-error>\n }\n @if (\n formFor(row.id).controls.paidOn.hasError('matDatepickerParse')\n ) {\n <mat-error>Use DD/MM/YYYY</mat-error>\n }\n @if (formFor(row.id).controls.paidOn.hasError('matDatepickerMax')) {\n <mat-error>Cannot be in the future</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.paidOn | date: 'dd/MM/yyyy' }}\n }\n </td>\n </ng-container>\n\n <!-- Total Column — editable number, right-aligned in both modes. -->\n <ng-container matColumnDef=\"inline-total\">\n <th mat-header-cell *matHeaderCellDef class=\"align-right\">Total</th>\n <td\n mat-cell\n *matCellDef=\"let row\"\n class=\"align-right demo-inline-edit-cell\"\n >\n @if (isEditing(row.id)) {\n <mat-form-field subscriptSizing=\"dynamic\">\n <input\n matInput\n type=\"number\"\n step=\"0.01\"\n [formControl]=\"formFor(row.id).controls.total\"\n [attr.aria-label]=\"'Total of row ' + row.id\"\n />\n @if (formFor(row.id).controls.total.hasError('required')) {\n <mat-error>Total is required</mat-error>\n }\n @if (formFor(row.id).controls.total.hasError('min')) {\n <mat-error>Cannot be negative</mat-error>\n }\n </mat-form-field>\n } @else {\n {{ row.total | currency: 'EUR' }}\n }\n </td>\n </ng-container>\n\n <!-- Actions Column — the mode switch itself. -->\n <ng-container matColumnDef=\"inline-actions\">\n <th mat-header-cell *matHeaderCellDef class=\"demo-inline-edit-actions\">\n Actions\n </th>\n <td mat-cell *matCellDef=\"let row\" class=\"demo-inline-edit-actions\">\n @if (isEditing(row.id)) {\n <button\n mat-icon-button\n type=\"button\"\n [attr.aria-label]=\"'Save changes to ' + row.name\"\n (click)=\"saveEdit(row.id)\"\n >\n <mat-icon>check</mat-icon>\n </button>\n <button\n mat-icon-button\n type=\"button\"\n [attr.aria-label]=\"'Discard changes to ' + row.name\"\n (click)=\"cancelEdit(row.id)\"\n >\n <mat-icon>close</mat-icon>\n </button>\n } @else {\n <!--\n A locked row keeps a disabled button rather than no button at all,\n so the tooltip stays reachable and the cell keeps its width.\n -->\n <button\n mat-icon-button\n type=\"button\"\n [id]=\"editButtonId(row.id)\"\n [disabled]=\"row.locked\"\n [matTooltip]=\"\n row.locked ? 'Already paid out — no longer editable' : 'Edit row'\n \"\n [attr.aria-label]=\"'Edit ' + row.name\"\n (click)=\"startEdit(row)\"\n >\n <mat-icon>edit</mat-icon>\n </button>\n }\n </td>\n </ng-container>\n </table>\n</div>\n","demo-table.component.scss":"table {\n // Medium media query of material\n // the initial is display: table, but it doesn't show responsive on larger screens\n // Block is required for the scroll overflow!\n display: table;\n overflow: auto;\n width: 100%;\n\n .description-cell {\n min-width: 500px;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Expandable row example (second table)\n//\n// This component sets `ViewEncapsulation.None`, so every selector here leaks\n// globally: all of them are scoped under `.demo-expandable-table`.\n// ---------------------------------------------------------------------------\n// --- Width ------------------------------------------------------------------\n//\n// The `table` rule at the top of this file makes every table its own scroll\n// container with `display: block; overflow: auto`. A block-level <table> wraps\n// its rows in an *anonymous table box*, and that box's width is `auto`, i.e.\n// shrink-to-fit: `width: 100%` then sizes the outer block, not the anonymous\n// table inside it, so a table whose content is narrower than its container\n// stops filling that container. The first table hides this because its\n// `min-width: 500px` description cell already makes it wider than the viewport;\n// this one is narrow enough to visibly shrink.\n//\n// So: scroll from a wrapper, and let the table stay a table.\n.demo-expandable-table-scroll {\n width: 100%;\n overflow: auto;\n}\n\n.demo-expandable-table {\n &.mat-mdc-table {\n display: table;\n overflow: visible;\n }\n\n // --- Striping ------------------------------------------------------------\n //\n // `theme/overrides/_table.scss:22` stripes rows positionally, with\n // `tbody tr:nth-child(odd)`. `multiTemplateDataRows` inserts a detail <tr>\n // after every data <tr>, so a data row's DOM position is no longer its\n // position in the data: every data row lands on an odd child (all striped,\n // no alternation) and the detail rows take over the even ones.\n //\n // Approach taken: neutralise the positional rule for this table only, and\n // re-apply the stripe from a class bound to `dataIndex` in the template.\n // The theme override is shared by the whole library, so it is left alone.\n // `:nth-child(4n + 1)` would only work while every data row is followed by a\n // detail row, which is already not the case — rows without sub-rows get no\n // detail row. The index-based class cannot drift from the data.\n //\n // Both selectors below outrank the theme override on specificity alone, so\n // they do not depend on stylesheet order.\n &.mat-mdc-table:not(.mat-card-table) tbody tr:nth-child(odd) {\n background-color: transparent;\n }\n\n &.mat-mdc-table:not(.mat-card-table) tbody tr.demo-expandable-row--striped {\n background-color: var(--mat-sys-primary-container);\n\n & + tr {\n background-color: var(--mat-sys-primary-container);\n }\n }\n\n // --- Rows ----------------------------------------------------------------\n .demo-expandable-toggle-cell {\n width: 4rem;\n // A row without sub-rows carries no toggle button. On a cell `height` is a\n // minimum, so reserving the button's own size here keeps that row exactly\n // as tall as the rows that do have one.\n height: var(--mdc-icon-button-state-layer-size, 40px);\n }\n\n // An open data row and its detail row read as a single block.\n &.mat-mdc-table tbody tr.demo-expandable-row--open td.mat-mdc-cell {\n border-bottom: none;\n }\n\n // A closed detail row has to take up no vertical space at all, and two\n // things stand in the way of that.\n //\n // First, Material puts `height: var(--mat-table-row-item-container-height,\n // 52px)` on `.mat-mdc-row`. On a <tr> that height is a *minimum*, not a\n // target, so a closed detail row still reserved a full, empty row under its\n // data row. Size the row from its cell instead.\n &.mat-mdc-table tbody tr.demo-expandable-detail-row {\n height: auto;\n }\n\n // Second, the theme pads and underlines every cell.\n &.mat-mdc-table tbody td.demo-expandable-detail-cell.mat-mdc-cell {\n padding: 0;\n border-bottom: none;\n }\n\n &.mat-mdc-table tbody td.demo-expandable-detail-cell--open.mat-mdc-cell {\n border-bottom: 1px solid var(--mat-sys-outline-variant);\n }\n\n // --- Open / close transition ---------------------------------------------\n //\n // A `grid-template-rows: 0fr -> 1fr` transition on the wrapper, so the\n // content keeps its natural height without it being measured in TypeScript.\n // No `@angular/animations`: there are no animation triggers in this project.\n .demo-expandable-detail {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows 220ms ease-in-out;\n\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n }\n\n .demo-expandable-detail-cell--open .demo-expandable-detail {\n grid-template-rows: 1fr;\n }\n\n .demo-expandable-detail__inner {\n min-height: 0;\n overflow: hidden;\n }\n\n // --- Sub-rows ------------------------------------------------------------\n .demo-expandable-detail__grid {\n padding: var(--spacer) var(--spacer-and-half) var(--spacer-and-half);\n }\n\n .demo-expandable-detail__row {\n display: grid;\n grid-template-columns: 6rem minmax(12rem, 1fr) 5rem 8rem;\n gap: var(--spacer);\n padding: var(--half-spacer) 0;\n\n &:not(:last-child) {\n border-bottom: 1px solid var(--mat-sys-outline-variant);\n }\n }\n\n .demo-expandable-detail__header {\n font-weight: var(--mat-sys-title-small-weight, 500);\n color: var(--mat-sys-on-surface-variant);\n }\n\n .demo-expandable-detail__numeric {\n text-align: right;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Expandable rows as real rows (third table)\n//\n// Same caveat as above: `ViewEncapsulation.None`, so everything is scoped under\n// `.demo-tree-table`.\n// ---------------------------------------------------------------------------\n.demo-tree-table {\n &.mat-mdc-table {\n display: table;\n overflow: visible;\n }\n\n // --- Striping ------------------------------------------------------------\n //\n // Same problem as the second table, one step worse: the number of rows before\n // a given row now changes as rows are opened, so the theme's positional\n // `tbody tr:nth-child(odd)` (theme/overrides/_table.scss:22) cannot describe\n // this table at all. Neutralise it here and stripe from `groupIndex`, which\n // is the parent's index in the data and so is unaffected by what is open.\n //\n // A parent and its sub-rows carry the same `groupIndex`, hence the same\n // stripe: the group reads as one band, and the alternation still runs one\n // colour per parent whether or not anything is expanded.\n &.mat-mdc-table:not(.mat-card-table) tbody tr:nth-child(odd) {\n background-color: transparent;\n }\n\n &.mat-mdc-table:not(.mat-card-table) tbody tr.demo-tree-row--striped {\n background-color: var(--mat-sys-primary-container);\n }\n\n // --- Group borders -------------------------------------------------------\n //\n // The theme underlines every cell, which would rule off each sub-row and hide\n // where one group ends. Only the last row of a group keeps its border; the\n // rows above it drop theirs.\n &.mat-mdc-table tbody tr.demo-tree-row--grouped td.mat-mdc-cell {\n border-bottom: none;\n }\n\n // --- Cells ---------------------------------------------------------------\n .demo-tree-toggle-cell {\n width: 4rem;\n // A row without sub-rows carries no toggle button. On a cell `height` is a\n // minimum, so reserving the button's own size here keeps that row exactly\n // as tall as the rows that do have one.\n height: var(--mdc-icon-button-state-layer-size, 40px);\n }\n\n // Sub-rows are indented rather than coloured differently, because the group\n // banding has already spent the background on showing what belongs together.\n .demo-tree-child-cell {\n padding-left: var(--spacer-and-half);\n color: var(--mat-sys-on-surface-variant);\n }\n\n &.mat-mdc-table tbody tr.demo-tree-row--child td.mat-mdc-cell {\n // Sub-rows carry less content than their parent, so they need less room.\n height: auto;\n padding-top: var(--half-spacer);\n padding-bottom: var(--half-spacer);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Inline row editing (fourth table)\n//\n// Same discipline as the two examples above: this component sets\n// `ViewEncapsulation.None`, so every selector here is scoped under\n// `.demo-inline-edit-table` (or its wrapper) and none of it leaks.\n//\n// The whole point of the styling here is that *nothing moves* when a row\n// switches mode. A `mat-form-field` is taller and wider than the text it\n// replaces, so without the reservations below the table reflows on every pen\n// click and the row the user aimed at jumps out from under the cursor.\n// ---------------------------------------------------------------------------\n\n// Scroll from a wrapper and let the table stay a table — see the note above\n// `.demo-expandable-table-scroll` for why the `table` rule at the top of this\n// file makes that necessary.\n.demo-inline-edit-table-scroll {\n width: 100%;\n overflow: auto;\n}\n\n.demo-inline-edit-bar {\n display: flex;\n align-items: center;\n gap: var(--half-spacer);\n}\n\n.demo-inline-edit-table {\n &.mat-mdc-table {\n display: table;\n overflow: visible;\n }\n\n // --- No layout jump ------------------------------------------------------\n //\n // Every row is given the height an *editing* row needs, so opening one does\n // not push the rows below it down. Editable cells get a min-width sized for\n // their control for the same reason, in the other axis.\n &.mat-mdc-table tbody tr.demo-inline-edit-row td.mat-mdc-cell {\n height: 42px;\n }\n\n .text-only-cell {\n display: flex;\n height: 42px;\n align-items: center;\n }\n\n .demo-inline-edit-cell {\n min-width: 12rem;\n }\n\n // A form field in a table cell has to fill the cell and lose the margin it\n // would carry in a form layout. `subscriptSizing=\"dynamic\"` in the template\n // is the other half of this: the default reserves ~20px of subscript space\n // under every field, which would make each editing row taller again.\n .demo-inline-edit-cell .mat-mdc-form-field {\n width: 100%;\n margin: 0;\n }\n\n // The actions cell is sized for two icon buttons whether it currently shows\n // one or two, so the column does not resize with the mode.\n .demo-inline-edit-actions {\n width: 7rem;\n min-width: 7rem;\n white-space: nowrap;\n }\n\n // --- Mode cue ------------------------------------------------------------\n //\n // A left accent bar, not a background colour: the cue has to survive both\n // stripes and must not be carried by colour alone (WCAG 1.4.1).\n &.mat-mdc-table tbody tr.demo-inline-edit-row--editing td.mat-mdc-cell {\n //background: var(--mat-sys-surface-container-low);\n\n &:first-child {\n box-shadow: inset 3px 0 0 0 var(--mat-sys-primary);\n }\n }\n\n // --- Failing row ---------------------------------------------------------\n //\n // The one place where the \"nothing moves\" rule above has to give: an error\n // message under a field is extra content, so the row genuinely is taller\n // while it is wrong. What the class fixes is the *alignment* — cells are\n // middle-aligned by default, so the id, the period and the action buttons\n // would float to the vertical centre of a row whose height is set by one\n // field's error text, and drift away from the fields they belong to.\n //\n // The height reservation is dropped as well: with `height: 42px` still in\n // force the taller cell wins anyway, but the shorter ones would keep padding\n // themselves out to 42px and re-introduce the gap this is meant to close.\n &.mat-mdc-table tbody tr.demo-inline-edit-row--error td.mat-mdc-cell {\n height: auto;\n vertical-align: top;\n\n // The accent bar of the mode cue is on the first cell, so it follows the\n // row's new height without anything else changing; only its colour does.\n &:first-child {\n box-shadow: inset 3px 0 0 0 var(--mat-sys-error);\n }\n }\n}\n","demo-table.component.ts":"import {\n afterNextRender,\n AfterViewInit,\n Component,\n computed,\n inject,\n Injector,\n signal,\n ViewEncapsulation,\n viewChild,\n viewChildren,\n} from '@angular/core';\nimport { CurrencyPipe, DatePipe } from '@angular/common';\nimport { LiveAnnouncer } from '@angular/cdk/a11y';\nimport { MatSelectModule } from '@angular/material/select';\nimport { MatError } from '@angular/material/form-field';\nimport { MatTooltip } from '@angular/material/tooltip';\nimport { MatDatepickerModule } from '@angular/material/datepicker';\nimport {\n OnemrvaDateFormatDirective,\n onemrvaDateNativeProvider,\n} from '@onemrvapublic/design-system/shared';\nimport { OnemrvaMatDatepickerHeaderComponent } from '@onemrvapublic/design-system/mat-datepicker-header';\nimport { MatSort, MatSortModule } from '@angular/material/sort';\nimport { MatTable, MatTableModule } from '@angular/material/table';\nimport { ListDataSource } from './demo-table-datasource';\nimport { PageEvent } from '@angular/material/paginator';\nimport {\n FormControl,\n FormGroup,\n FormsModule,\n ReactiveFormsModule,\n Validators,\n} from '@angular/forms';\nimport { MatCheckbox, MatCheckboxChange } from '@angular/material/checkbox';\n\nimport { MatChipsModule } from '@angular/material/chips';\nimport { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';\nimport { MatButton, MatIconButton } from '@angular/material/button';\nimport {\n OnemrvaMatPaginatorComponent,\n OnemrvaMatPaginatorModule,\n} from '@onemrvapublic/design-system/mat-paginator';\nimport {\n MatFormField,\n MatLabel,\n MatSuffix,\n} from '@angular/material/form-field';\nimport { MatInput } from '@angular/material/input';\nimport { OnemrvaMatEmptyRowComponent } from '@onemrvapublic/design-system/mat-empty-row';\nimport { ElementsService, ListItem } from '../../_services/elements.service';\nimport { OnemrvaMatMessageBoxModule } from '@onemrvapublic/design-system/mat-message-box';\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\nimport { take } from 'rxjs';\nimport { MatDialog, MatDialogModule } from '@angular/material/dialog';\nimport { DemoTableDialogContentComponent } from '../table-dialog-content';\nimport { MatIcon } from '@angular/material/icon';\nimport { DemoComponentBase } from '../../_demo/demo-component-base';\nimport { OnemrvaExpandableTableIconComponent } from '@onemrvapublic/design-system/mat-table';\n\n/**\n * A row of the \"expandable row\" example.\n *\n * `ListItem` (from `ElementsService`) carries no nested data, so the second\n * example uses its own local shape instead of extending the service.\n */\ninterface BenefitRow {\n id: number;\n name: string;\n office: string;\n period: string;\n total: string;\n}\n\n/** One sub-row revealed when a {@link BenefitRow} is expanded. */\ninterface BenefitDetailRow {\n code: string;\n label: string;\n days: number;\n amount: string;\n}\n\n/**\n * One rendered row of the \"expandable rows as real rows\" example.\n *\n * Unlike the detail-row example, the sub-rows there are ordinary `<tr mat-row>`\n * elements in the same table, so parents and children have to live in one flat\n * array. A discriminated union keeps the two shapes apart while they share it.\n */\ntype BenefitTreeRow =\n | {\n readonly kind: 'parent';\n /** Stable key for the table's `trackBy`. */\n readonly key: string;\n /** Index of the parent in the data — the stripe is derived from it. */\n readonly groupIndex: number;\n /** Last row of its group, i.e. the row that carries the group's border. */\n readonly lastInGroup: boolean;\n readonly row: BenefitRow;\n }\n | {\n readonly kind: 'child';\n readonly key: string;\n readonly groupIndex: number;\n readonly lastInGroup: boolean;\n readonly parent: BenefitRow;\n readonly detail: BenefitDetailRow;\n };\n\n/**\n * Static data for the expandable-row example.\n *\n * Deliberately not a `ListDataSource`: that class throws when `sort` is unset\n * before `connect()` and expects a paginator, so reusing it would mean a second\n * sort and paginator instance for an example that has neither.\n */\nconst BENEFIT_ROWS: readonly BenefitRow[] = [\n {\n id: 1,\n name: 'Janssens, Marie',\n office: 'Brussel',\n period: 'January 2026',\n total: '€ 1.284,60',\n },\n {\n id: 2,\n name: 'Peeters, Tom',\n office: 'Antwerpen',\n period: 'January 2026',\n total: '€ 962,00',\n },\n {\n id: 3,\n name: 'Dubois, Claire',\n office: 'Liège',\n period: 'January 2026',\n total: '€ 1.431,25',\n },\n {\n id: 4,\n name: 'Van Damme, Lars',\n office: 'Gent',\n period: 'January 2026',\n total: '€ 744,15',\n },\n // No sub-rows: this row has no detail row at all and its toggle is disabled.\n {\n id: 5,\n name: 'Mertens, Hind',\n office: 'Brugge',\n period: 'January 2026',\n total: '€ 0,00',\n },\n];\n\n/** Sub-rows per row id. */\nconst BENEFIT_DETAILS: Readonly<Record<number, readonly BenefitDetailRow[]>> = {\n 1: [\n {\n code: 'A01',\n label: 'Full unemployment benefit',\n days: 18,\n amount: '€ 902,40',\n },\n {\n code: 'A07',\n label: 'Seniority supplement',\n days: 18,\n amount: '€ 241,20',\n },\n { code: 'C12', label: 'Commuting allowance', days: 12, amount: '€ 141,00' },\n ],\n 2: [\n {\n code: 'A01',\n label: 'Full unemployment benefit',\n days: 14,\n amount: '€ 728,00',\n },\n {\n code: 'B03',\n label: 'Temporary unemployment — economic',\n days: 4,\n amount: '€ 234,00',\n },\n ],\n 3: [\n {\n code: 'A01',\n label: 'Full unemployment benefit',\n days: 20,\n amount: '€ 1.002,00',\n },\n {\n code: 'A07',\n label: 'Seniority supplement',\n days: 20,\n amount: '€ 268,00',\n },\n { code: 'C12', label: 'Commuting allowance', days: 15, amount: '€ 161,25' },\n ],\n 4: [\n {\n code: 'B03',\n label: 'Temporary unemployment — economic',\n days: 11,\n amount: '€ 643,50',\n },\n { code: 'C12', label: 'Commuting allowance', days: 9, amount: '€ 100,65' },\n ],\n};\n\n// -----------------------------------------------------------------------------\n// Inline row editing (fourth example)\n// -----------------------------------------------------------------------------\n\n/**\n * A row of the inline-editing example.\n *\n * `total` is a number here, not the pre-formatted string the expandable\n * examples use: it has to round-trip through a form control, so view mode\n * formats it with the currency pipe instead.\n */\ninterface EditableBenefitRow {\n /** Identity. Never editable. */\n readonly id: number;\n name: string;\n office: string;\n /** Derived from the payment run, so read-only for a business reason. */\n readonly period: string;\n /** Editable through a datepicker. A real `Date`, not a formatted string. */\n paidOn: Date;\n total: number;\n /** A row that cannot be edited at all — its pen is disabled. */\n readonly locked?: boolean;\n}\n\n/** The form behind one row in edit mode; only the editable columns appear. */\ntype InlineEditForm = FormGroup<{\n name: FormControl<string>;\n office: FormControl<string>;\n paidOn: FormControl<Date>;\n total: FormControl<number>;\n}>;\n\n/** Options of the `office` select. */\nconst EDIT_OFFICES: readonly string[] = [\n 'Brussel',\n 'Antwerpen',\n 'Liège',\n 'Gent',\n 'Brugge',\n 'Namur',\n];\n\nconst EDITABLE_ROWS: readonly EditableBenefitRow[] = [\n {\n id: 1,\n name: 'Janssens, Marie',\n office: 'Brussel',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 3),\n total: 1284.6,\n },\n {\n id: 2,\n name: 'Peeters, Tom',\n office: 'Antwerpen',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 5),\n total: 962,\n },\n {\n id: 3,\n name: 'Dubois, Claire',\n office: 'Liège',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 5),\n total: 1431.25,\n },\n {\n id: 4,\n name: 'Van Damme, Lars',\n office: 'Gent',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 10),\n total: 744.15,\n },\n // Already paid out, so it is shown but cannot be corrected here.\n {\n id: 5,\n name: 'Mertens, Hind',\n office: 'Brugge',\n period: 'January 2026',\n paidOn: new Date(2026, 1, 2),\n total: 0,\n locked: true,\n },\n];\n\n@Component({\n selector: 'app-demo-table',\n templateUrl: './demo-table.component.html',\n styleUrls: ['./demo-table.component.scss'],\n\n standalone: true,\n imports: [\n ReactiveFormsModule,\n FormsModule,\n MatCheckbox,\n MatChipsModule,\n MatFormField,\n MatIconButton,\n MatInput,\n MatLabel,\n MatMenu,\n MatMenuTrigger,\n MatSortModule,\n MatSuffix,\n MatTableModule,\n MatDialogModule,\n MatIcon,\n OnemrvaMatPaginatorModule,\n OnemrvaMatEmptyRowComponent,\n OnemrvaMatMessageBoxModule,\n OnemrvaMatPaginatorComponent,\n MatMenuItem,\n MatButton,\n OnemrvaExpandableTableIconComponent,\n MatSelectModule,\n MatError,\n MatTooltip,\n MatDatepickerModule,\n OnemrvaDateFormatDirective,\n CurrencyPipe,\n DatePipe,\n ],\n // Native `Date` adapter + the DD/MM/YYYY formats the house datepicker uses.\n // Scoped to this component, so the other examples on the page are untouched.\n providers: [onemrvaDateNativeProvider()],\n encapsulation: ViewEncapsulation.None,\n})\nexport class DemoTableComponent\n extends DemoComponentBase\n implements AfterViewInit\n{\n readonly paginator = viewChild.required(OnemrvaMatPaginatorComponent);\n\n readonly sort = viewChild.required(MatSort);\n\n readonly table = viewChild.required(MatTable);\n\n readonly matCheckboxes = viewChildren<MatCheckbox>('checkboxes');\n\n dataSource: ListDataSource;\n\n // Checked checkbox values' list\n public checkboxes: number[] = [];\n\n // Check all checkbox statuses\n public undetermined = false;\n public checked = false;\n\n // all columns first level\n allColumns = ['check', 'id', 'name', 'description', 'suspension', 'more'];\n // all columns second level\n allSubColumns = [\n 'check',\n 'id',\n 'name',\n 'description',\n 'delay',\n 'days',\n 'more',\n ];\n // removable columns\n columns = ['id', 'name', 'description'];\n\n // Filter form control\n search: FormControl = new FormControl('');\n\n // ---------------------------------------------------------------------------\n // Expandable row example (second table)\n //\n // New state is signal-based per `.junie/guidelines.md`; the plain mutable\n // fields above belong to the first table and are left as they are.\n // ---------------------------------------------------------------------------\n\n /** Rows of the expandable-row example. */\n readonly benefitRows = BENEFIT_ROWS;\n\n /** Columns of the expandable-row example; its length is the detail colspan. */\n readonly benefitColumns = [\n 'id',\n 'name',\n 'office',\n 'period',\n 'total',\n 'expand',\n ];\n\n /** The single column of the detail row, which spans the whole table. */\n readonly detailColumns = ['detail'];\n\n /**\n * Ids of every open row.\n *\n * A set rather than a single id: several rows may be open at the same time,\n * so two rows can be compared without collapsing one of them.\n */\n private readonly expandedIds = signal<ReadonlySet<number>>(new Set<number>());\n\n /** How many rows are open; drives the \"Collapse all\" control. */\n readonly expandedCount = computed(() => this.expandedIds().size);\n\n /**\n * Row predicate for the detail row definition.\n *\n * A detail row exists only for rows that have sub-rows — an empty one would\n * otherwise be rendered (and, without the height reset in the .scss, be\n * visible) under every row. For rows that do have sub-rows the detail row\n * stays in the DOM while closed, so that its wrapper has something to run the\n * open/close CSS transition on.\n * @param _index unused row index required by the `when` signature\n * @param row\n */\n readonly hasDetailRow = (_index: number, row: BenefitRow): boolean =>\n this.hasDetails(row);\n\n /**\n * Does this row have sub-rows to reveal?\n * @param row\n */\n hasDetails(row: BenefitRow): boolean {\n return this.detailsFor(row.id).length > 0;\n }\n\n /**\n * Is the row with this id expanded?\n * @param id\n */\n isExpanded(id: number): boolean {\n return this.expandedIds().has(id);\n }\n\n /**\n * Open a collapsed row, or collapse an open one.\n * @param id\n */\n toggleExpanded(id: number): void {\n this.expandedIds.update(ids => {\n const next = new Set(ids);\n if (!next.delete(id)) {\n next.add(id);\n }\n return next;\n });\n }\n\n /**\n * Collapse every open row.\n */\n collapseAll(): void {\n this.expandedIds.set(new Set<number>());\n }\n\n /**\n * Sub-rows of the row with this id.\n * @param id\n */\n detailsFor(id: number): readonly BenefitDetailRow[] {\n return BENEFIT_DETAILS[id] ?? [];\n }\n\n /**\n * DOM id of a detail row, so the toggle button can point `aria-controls` at it.\n * @param id\n */\n detailRowId(id: number): string {\n return `demo-table-benefit-detail-${id}`;\n }\n\n // ---------------------------------------------------------------------------\n // Expandable rows as real rows (third table)\n //\n // Same data and the same toggle as the example above, but a sub-row is a real\n // <tr mat-row> in the same table rather than a grid inside one wide detail\n // cell. So the sub-rows sit in the table's own column layout, and the table\n // has one flat row array that grows and shrinks as rows are opened.\n // ---------------------------------------------------------------------------\n\n /** Columns of a parent row. */\n readonly treeColumns = [\n 'tree-id',\n 'tree-name',\n 'tree-office',\n 'tree-period',\n 'tree-total',\n 'tree-expand',\n ];\n\n /**\n * Columns of a sub-row.\n *\n * A separate set rather than the parent's, so neither cell template has to\n * branch on the row type. The cells still line up: column widths come from\n * the table layout, which spans every row whatever defined them.\n */\n readonly treeDetailColumns = [\n 'tree-detail-code',\n 'tree-detail-label',\n 'tree-detail-office',\n 'tree-detail-days',\n 'tree-detail-amount',\n 'tree-detail-expand',\n ];\n\n /** Ids of every open row of the third table. */\n private readonly treeExpandedIds = signal<ReadonlySet<number>>(\n new Set<number>(),\n );\n\n /** How many rows are open; drives this example's \"Collapse all\" control. */\n readonly treeExpandedCount = computed(() => this.treeExpandedIds().size);\n\n /**\n * The rows to render: every parent, each followed by its sub-rows while it is\n * open.\n *\n * `groupIndex` is the parent's index in the data, so the stripe alternates per\n * parent and a whole group keeps one colour — see the .scss. `lastInGroup`\n * marks the row that closes a group, which is the only row of it that draws a\n * bottom border.\n */\n readonly treeRows = computed<readonly BenefitTreeRow[]>(() => {\n const open = this.treeExpandedIds();\n\n return this.benefitRows.flatMap((row, groupIndex) => {\n const details = open.has(row.id) ? this.detailsFor(row.id) : [];\n\n return [\n {\n kind: 'parent',\n key: `parent-${row.id}`,\n groupIndex,\n lastInGroup: details.length === 0,\n row,\n } satisfies BenefitTreeRow,\n ...details.map(\n (detail, index) =>\n ({\n kind: 'child',\n key: `child-${row.id}-${detail.code}`,\n groupIndex,\n lastInGroup: index === details.length - 1,\n parent: row,\n detail,\n }) satisfies BenefitTreeRow,\n ),\n ];\n });\n });\n\n /**\n * Row predicate selecting the sub-row template; parents fall through to the\n * default row definition.\n * @param _index unused row index required by the `when` signature\n * @param entry\n */\n readonly isTreeChildRow = (_index: number, entry: BenefitTreeRow): boolean =>\n entry.kind === 'child';\n\n /**\n * Identity of a rendered row, so opening one row does not re-create the others.\n * @param _index unused row index required by the `trackBy` signature\n * @param entry\n */\n readonly treeTrackBy = (_index: number, entry: BenefitTreeRow): string =>\n entry.key;\n\n /**\n * Is the row with this id expanded in the third table?\n * @param id\n */\n isTreeExpanded(id: number): boolean {\n return this.treeExpandedIds().has(id);\n }\n\n /**\n * Open a collapsed row of the third table, or collapse an open one.\n * @param id\n */\n toggleTreeExpanded(id: number): void {\n this.treeExpandedIds.update(ids => {\n const next = new Set(ids);\n if (!next.delete(id)) {\n next.add(id);\n }\n return next;\n });\n }\n\n /**\n * Collapse every open row of the third table.\n */\n collapseAllTree(): void {\n this.treeExpandedIds.set(new Set<number>());\n }\n\n /**\n * DOM id of one sub-row.\n * @param id id of the parent row\n * @param code code of the sub-row\n */\n treeChildRowId(id: number, code: string): string {\n return `demo-table-tree-${id}-${code}`;\n }\n\n /**\n * The ids of every sub-row of a parent, for the toggle's `aria-controls`.\n *\n * `aria-controls` takes a list, which is what a row controlling several rows\n * needs. The ids are only in the DOM while the row is open; naming an absent\n * id is how a collapsed control is normally described.\n * @param id\n */\n treeChildRowIds(id: number): string {\n return this.detailsFor(id)\n .map(detail => this.treeChildRowId(id, detail.code))\n .join(' ');\n }\n\n // ---------------------------------------------------------------------------\n // Inline row editing (fourth table)\n //\n // A pen switches one row into edit mode; only `name`, `office` and `total`\n // become controls, `id` and `period` keep rendering their value. Several rows\n // can be open at once, each with its own form, so validity and cancel are\n // row-scoped and nothing is shared between open rows.\n // ---------------------------------------------------------------------------\n\n /** Rows of the inline-editing example; a save writes back here. */\n private readonly editRows =\n signal<readonly EditableBenefitRow[]>(EDITABLE_ROWS);\n\n /** Read-only view of the rows for the template. */\n readonly inlineRows = this.editRows.asReadonly();\n\n /** Options of the `office` select. */\n readonly offices = EDIT_OFFICES;\n\n /** Columns of the inline-editing example. */\n readonly inlineColumns = [\n 'inline-id',\n 'inline-name',\n 'inline-office',\n 'inline-period',\n 'inline-paid-on',\n 'inline-total',\n 'inline-actions',\n ];\n\n /**\n * The house datepicker header, referenced from the template.\n *\n * Same wiring as the datepicker demo: `[calendarHeaderComponent]` takes the\n * component type, which a template can only reach through a field.\n */\n protected readonly OnemrvaMatDatepickerHeaderComponent =\n OnemrvaMatDatepickerHeaderComponent;\n\n /**\n * Upper bound of the payment-date picker.\n *\n * A payment cannot be dated in the future, so the calendar disables those\n * days outright rather than only rejecting them on save.\n */\n readonly today = new Date();\n\n /** Ids of every row currently in edit mode. */\n private readonly editingIds = signal<ReadonlySet<number>>(new Set<number>());\n\n /**\n * The form of every open row.\n *\n * A plain `Map` and not a signal: a `FormGroup` is a mutable object with its\n * own change streams, so putting it in signal state would only invite it to be\n * re-created on every read. `editingIds` is what the template reacts to.\n */\n private readonly editForms = new Map<number, InlineEditForm>();\n\n /** How many rows are open; drives the bulk bar. */\n readonly editingCount = computed(() => this.editingIds().size);\n\n private readonly announcer = inject(LiveAnnouncer);\n private readonly injector = inject(Injector);\n\n /**\n * Is this row in edit mode?\n * @param id\n */\n isEditing(id: number): boolean {\n return this.editingIds().has(id);\n }\n\n /**\n * Is this row showing validation errors?\n *\n * Row-level, not control-level: the template puts a class on the whole `tr`\n * from this, so the cells of a failing row can be styled together (they are\n * top-aligned, because an error message under one field makes that cell\n * taller than its neighbours and centred content would then look ragged).\n *\n * `touched` is part of the condition because validation is only surfaced\n * after a save attempt — `saveEdit()` calls `markAllAsTouched()`. A row that\n * has just been opened is invalid in the eyes of the form but has not shown\n * the user anything yet, and must not be styled as if it had.\n *\n * A plain method rather than a computed: `editForms` is deliberately not\n * signal state (see its doc), so there is nothing here for a computed to\n * depend on. This is re-read on change detection exactly like the\n * `hasError()` calls already in the template.\n * @param id\n */\n hasRowError(id: number): boolean {\n const form = this.editForms.get(id);\n return !!form && form.invalid && form.touched;\n }\n\n /**\n * The form of an open row.\n *\n * Only ever called from inside an `@if (isEditing(...))`, so the entry exists.\n * @param id\n */\n formFor(id: number): InlineEditForm {\n const form = this.editForms.get(id);\n if (!form) {\n throw new Error(`No inline-edit form for row ${id}.`);\n }\n return form;\n }\n\n /**\n * Put a row into edit mode and focus its first editable control.\n *\n * A locked row is a no-op: its pen is rendered disabled, and this guards the\n * programmatic path as well.\n * @param row\n */\n startEdit(row: EditableBenefitRow): void {\n if (row.locked || this.isEditing(row.id)) {\n return;\n }\n\n this.editForms.set(row.id, this.buildForm(row));\n this.editingIds.update(ids => new Set(ids).add(row.id));\n\n void this.announcer.announce(`Editing ${row.name}`);\n this.focusAfterRender(this.firstControlId(row.id));\n }\n\n /**\n * Discard a row's changes and leave edit mode.\n *\n * The demo discards without confirming even when the form is dirty; a real\n * screen would ask first.\n * @param id\n */\n cancelEdit(id: number): void {\n if (!this.isEditing(id)) {\n return;\n }\n\n this.editForms.delete(id);\n this.editingIds.update(ids => {\n const next = new Set(ids);\n next.delete(id);\n return next;\n });\n\n void this.announcer.announce('Changes discarded');\n this.focusAfterRender(this.editButtonId(id));\n }\n\n /**\n * Commit a row if its form is valid, otherwise leave it open with its errors\n * shown.\n * @param id\n * @returns whether the row was committed\n */\n saveEdit(id: number): boolean {\n const form = this.editForms.get(id);\n if (!form) {\n return false;\n }\n\n // Validation runs here and not on every keystroke — see `.junie/guidelines.md`.\n form.markAllAsTouched();\n if (form.invalid) {\n void this.announcer.announce('Row has errors and was not saved');\n return false;\n }\n\n const value = form.getRawValue();\n this.editRows.update(rows =>\n rows.map(row => (row.id === id ? { ...row, ...value } : row)),\n );\n\n this.editForms.delete(id);\n this.editingIds.update(ids => {\n const next = new Set(ids);\n next.delete(id);\n return next;\n });\n\n void this.announcer.announce('Row saved');\n this.focusAfterRender(this.editButtonId(id));\n return true;\n }\n\n /**\n * Commit every valid open row.\n *\n * Rows that fail validation stay open with their errors shown rather than\n * being silently dropped.\n */\n saveAll(): void {\n const ids = [...this.editingIds()];\n const failed = ids.filter(id => !this.saveEdit(id));\n\n if (failed.length > 0) {\n void this.announcer.announce(\n `${ids.length - failed.length} rows saved, ${failed.length} still have errors`,\n );\n this.focusAfterRender(this.firstControlId(failed[0] as number));\n }\n }\n\n /**\n * Discard every open row.\n */\n cancelAll(): void {\n this.editForms.clear();\n this.editingIds.set(new Set<number>());\n void this.announcer.announce('All changes discarded');\n }\n\n /** DOM id of a row's pen button, so focus can return to it. */\n editButtonId(id: number): string {\n return `demo-table-inline-edit-${id}`;\n }\n\n /** DOM id of the first editable control of a row. */\n firstControlId(id: number): string {\n return `demo-table-inline-name-${id}`;\n }\n\n /**\n * Build the form of one row.\n *\n * `nonNullable` throughout, so `getRawValue()` matches `EditableBenefitRow`\n * without a null-check per field.\n * @param row\n */\n private buildForm(row: EditableBenefitRow): InlineEditForm {\n return new FormGroup({\n name: new FormControl(row.name, {\n nonNullable: true,\n validators: [Validators.required, Validators.maxLength(60)],\n }),\n office: new FormControl(row.office, {\n nonNullable: true,\n validators: [Validators.required],\n }),\n // A fresh `Date`: the picker writes a new instance on pick, but typing an\n // invalid date leaves the control's value alone, and sharing the object\n // with the row would let a cancelled edit still show up in view mode.\n paidOn: new FormControl(new Date(row.paidOn), {\n nonNullable: true,\n validators: [Validators.required],\n }),\n total: new FormControl(row.total, {\n nonNullable: true,\n validators: [Validators.required, Validators.min(0)],\n }),\n });\n }\n\n /**\n * Move focus to an element once the template has caught up.\n *\n * Reading the DOM directly is the exception the guidelines allow for focus\n * management: the element does not exist until the row has re-rendered, so\n * there is nothing for a `viewChild` to resolve to at call time either.\n * @param elementId\n */\n private focusAfterRender(elementId: string): void {\n afterNextRender(() => document.getElementById(elementId)?.focus(), {\n injector: this.injector,\n });\n }\n\n dialog = inject(MatDialog);\n elementsService = inject(ElementsService);\n\n constructor() {\n super();\n // Create a new DataSource\n this.dataSource = new ListDataSource(this.elementsService);\n\n // When user start filtering, we need to update the master checkbox\n this.search.valueChanges.pipe(takeUntilDestroyed()).subscribe(search => {\n console.log(search);\n setTimeout(() => {\n this.updateToggleAllStatus();\n }, 100);\n });\n }\n\n /**\n *\n */\n showHideColumns() {\n const panelClass: string[] = ['large'];\n const dialogRef = this.dialog.open(DemoTableDialogContentComponent, {\n data: {\n allColumns: this.allColumns,\n allSubColumns: this.allSubColumns,\n columns: this.columns,\n },\n panelClass,\n });\n dialogRef\n .afterClosed()\n .pipe(take(1))\n .subscribe((values: boolean[]) => {\n if (values !== undefined) {\n this.allColumns = ['check'];\n this.allSubColumns = ['check'];\n values.forEach((value, index) => {\n if (value) {\n this.allColumns.push(this.columns[index]);\n this.allSubColumns.push(this.columns[index]);\n }\n });\n this.allColumns.push('suspension');\n this.allColumns.push('more');\n this.allSubColumns.push('delay');\n this.allSubColumns.push('days');\n this.allSubColumns.push('more');\n }\n });\n }\n\n /**\n * We set the datasource and paginator\n */\n ngAfterViewInit(): void {\n this.dataSource.sort = this.sort();\n this.dataSource.paginator = this.paginator();\n this.dataSource.search = this.search;\n this.table().dataSource = this.dataSource;\n }\n\n /**\n * returns the column visible state\n * @param column\n */\n columnIsVisible(column: string) {\n return this.allColumns.includes(column);\n }\n /**\n * returns the column visible state\n * @param column\n */\n subColumnIsVisible(column: string) {\n return this.allSubColumns.includes(column);\n }\n\n /**\n * Edit row action\n * @param row\n */\n edit(row: ListItem) {\n alert(`Edit row n°${row.id}: ${row.name}`);\n }\n\n /**\n * Delete row action\n * @param row\n */\n delete(row: ListItem) {\n this.dataSource.delete(row.id);\n }\n\n /**\n * Share row action\n * @param row\n */\n share(row: ListItem) {\n alert(`Share row n°${row.id}: ${row.name}`);\n }\n\n /**\n * clear search\n */\n clear() {\n this.search.setValue('');\n }\n\n /**\n * paginator changes page\n * @param event\n */\n changePage(event: PageEvent) {\n console.log(event);\n // timeout necessary to take the changes in account\n setTimeout(() => {\n this.updateToggleAllStatus();\n }, 100);\n }\n\n /**\n * Is the notice visible\n */\n showNotice() {\n return this.checkboxes.length > 0;\n }\n\n /**\n * is a checkbox checked\n * @param id\n */\n isChecked(id: number) {\n return this.checkboxes.includes(id);\n }\n\n /**\n * Toggle One checkbox\n * @param $event\n * @param id\n */\n toggleOne($event: MatCheckboxChange, id: number) {\n const checked = $event.checked;\n this.toggleCheckBox(id, checked);\n this.updateToggleAllStatus();\n }\n\n /**\n * toggle the checkbox with checks\n * @param id\n * @param checked\n * @private\n */\n private toggleCheckBox(id: number, checked: boolean) {\n if (checked) {\n if (!this.checkboxes.includes(id)) {\n this.checkboxes.push(id);\n }\n } else {\n this.checkboxes.splice(this.checkboxes.indexOf(id), 1);\n }\n }\n\n /**\n * Toggle all visible checkboxes\n * @param $event\n */\n toggleAllVisible($event: MatCheckboxChange) {\n const checked = $event.checked;\n this.checked = checked;\n\n this.matCheckboxes().forEach(matCheckbox => {\n const id = parseInt(matCheckbox.value);\n this.toggleCheckBox(id, checked);\n });\n this.updateToggleAllStatus();\n }\n\n /**\n * Update the status of the toggle all checkbox to reflect on what is checked (checked, unchecked, undetermined)\n */\n updateToggleAllStatus() {\n this.undetermined = false;\n this.checked = false;\n let cnt = 0;\n\n this.matCheckboxes().forEach(matCheckbox => {\n const id = parseInt(matCheckbox.value);\n if (this.checkboxes.includes(id)) {\n cnt++;\n }\n });\n if (cnt > 0) {\n if (cnt === this.matCheckboxes().length) {\n this.checked = true;\n } else {\n this.undetermined = true;\n }\n }\n }\n\n /**\n * Select all checkbox in the full datasource's list\n */\n selectAll() {\n this.dataSource\n .all()\n .pipe(take(1))\n .subscribe(ids => {\n this.checkboxes = ids;\n this.updateToggleAllStatus();\n });\n }\n\n /**\n * Deselect all checkboxes\n */\n deSelectAll() {\n this.checkboxes = [];\n this.updateToggleAllStatus();\n }\n}\n"}
|