@onemrvapublic/design-system-demos 22.4.0-develop.5 → 22.4.0-develop.6

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.
@@ -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.&nbsp;\n @if (checkboxes.length < paginator.length) {\n <a tabindex=\"0\" (click)=\"selectAll()\"\n >Select {{ paginator.length }} items</a\n >&nbsp;\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","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","demo-table.component.ts":"import {\n AfterViewInit,\n Component,\n computed,\n inject,\n signal,\n ViewEncapsulation,\n viewChild,\n viewChildren,\n} from '@angular/core';\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 { FormControl, FormsModule, ReactiveFormsModule } 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 * 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@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 ],\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 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.&nbsp;\n @if (checkboxes.length < paginator.length) {\n <a tabindex=\"0\" (click)=\"selectAll()\"\n >Select {{ paginator.length }} items</a\n >&nbsp;\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 <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 [attr.aria-expanded]=\"\n hasDetails(entry.row) ? isTreeExpanded(entry.row.id) : null\n \"\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","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","demo-table.component.ts":"import {\n AfterViewInit,\n Component,\n computed,\n inject,\n signal,\n ViewEncapsulation,\n viewChild,\n viewChildren,\n} from '@angular/core';\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 { FormControl, FormsModule, ReactiveFormsModule } 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@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 ],\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 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"}