Smart-grid¶
konti-smart-grid er ePortal sin gjenbrukbare Angular-tabell for lister, rapporter og administrasjonsflater. Den dekker sortering, kolonnefilter, gruppering, paginering, opt-in rad-virtualisering, Excel-eksport, mobilkort, inline redigering og valgfri kolonnevelger.
Kilde og import¶
| Del | Sti |
|---|---|
| Komponent | ui/ePortal.ui/src/app/shared/smart-grid/smart-grid.component.ts |
| Template | ui/ePortal.ui/src/app/shared/smart-grid/smart-grid.component.html |
| Stil | ui/ePortal.ui/src/app/shared/smart-grid/smart-grid.component.scss |
| Modeller | ui/ePortal.ui/src/app/shared/smart-grid/smart-grid.models.ts |
| Celle-template | ui/ePortal.ui/src/app/shared/smart-grid/smart-grid-cell.directive.ts |
| Edit-template | ui/ePortal.ui/src/app/shared/smart-grid/smart-grid-edit.directive.ts |
| Lokal kolonnepersistens | ui/ePortal.ui/src/app/shared/smart-grid/grid-settings.service.ts |
| Shared export | ui/ePortal.ui/src/app/shared/shared.module.ts |
Komponenten er standalone, men er også eksportert fra SharedModule. I feature-moduler brukes den vanligvis ved å importere SharedModule; standalone-komponenter kan importere SmartGridComponent, SmartGridCellDirective og SmartGridEditDirective direkte.
import {
AggregateFilter,
CellEditEvent,
ColumnFilter,
GridColumn,
GridFilter,
GridGroup,
GridSort,
} from 'src/app/shared/smart-grid/smart-grid.models';
Import i Angular¶
Velg importmønster ut fra om siden din er en klassisk NgModule-side eller en standalone-komponent.
Feature-modul¶
Hvis feature-modulen allerede importerer SharedModule, trengs ingen ekstra Angular-import for template-bruk:
import { NgModule } from '@angular/core';
import { SharedModule } from 'src/app/shared/shared.module';
@NgModule({
imports: [
SharedModule,
],
})
export class CustomerModule {}
Standalone-komponent¶
Standalone-komponenter må importere komponenten og direktivene de bruker i template:
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { SmartGridCellDirective } from 'src/app/shared/smart-grid/smart-grid-cell.directive';
import { SmartGridEditDirective } from 'src/app/shared/smart-grid/smart-grid-edit.directive';
import { SmartGridComponent } from 'src/app/shared/smart-grid/smart-grid.component';
@Component({
selector: 'app-customer-list',
standalone: true,
imports: [
CommonModule,
TranslateModule,
SmartGridComponent,
SmartGridCellDirective,
SmartGridEditDirective,
],
templateUrl: './customer-list.component.html',
})
export class CustomerListComponent {}
Hvis siden bare bruker standardceller og ingen templates, kan SmartGridCellDirective og SmartGridEditDirective utelates.
Når skal den brukes?¶
Bruk smart-grid når siden trenger en tabell med en eller flere av disse funksjonene:
- Kolonnebasert sortering og filtrering
- Gruppering og gruppeaggregater
- Excel-eksport fra samme kolonneoppsett som brukeren ser
- Paginering for større datasett
- Kompakt desktop-tabell med automatisk mobilkort-visning
- Enkle action-knapper eller inline redigering per rad
Ikke bruk smart-grid alene for svært store datasett uten en eksplisitt ytelsesstrategi. Bruk showPagination, serverSidePagination, backend-avgrensning eller opt-in virtualScroll for flate desktop-tabeller.
Før du begynner¶
En ny liste trenger disse delene:
| Del | Hva du lager | Hvorfor |
|---|---|---|
| Radmodell | Et TypeScript-interface som matcher data fra API eller view-model | GridColumn.field må peke på faktiske properties. |
rows |
Array med radmodellen | Gridet renderer det som ligger her. |
columns |
GridColumn<T>[] |
Definerer overskrifter, bredder, sortering, filter, mobil og eksport. |
rowKey |
Funksjon som returnerer stabil id | Brukes av trackBy, mobil expand, inline edit og intern state. |
| Template | <konti-smart-grid ...> |
Binder data, kolonner og events. |
| Event handlers | rowClick, pageChange, cellEdit osv. |
Parent eier navigasjon, API-kall og lagring. |
Praktisk rekkefølge:
- Lag en radmodell med engelske property-navn.
- Map backend-DTO til radmodellen hvis backend bruker andre navn.
- Definer
columnsmedfield,titleKey,sortable,filterable, bredde ogmobileRole. - Legg inn gridet i template med
rowKey. - Legg til templates bare for celler som faktisk trenger HTML, badges, lenker eller knapper.
- Slå på paging, eksport, kolonnevelger eller server-side filter etter behov.
Feature-tabell¶
| Feature | Støtte | Konfigurasjon |
|---|---|---|
| Sortering og filter | Ja | sortable, filterable, filterType, initialSort, initialFilter |
| Server-side søk/filter | Ja | serverSideSearch, serverFilterChange |
| Paginering | Ja | showPagination, pageSize, serverSidePagination, pageChange |
| Gruppering og aggregater | Ja | initialGroups, groupAggregates, showTotalRow, showFieldSum, preserveGroupCollapse |
| Custom celler | Ja | ng-template appSmartGridCell="field" |
| Inline edit | Ja | editable, editType, appSmartGridEdit, cellEdit |
| Excel-eksport | Ja | enableExport, exportFilename, exportOnly |
| Mobilkort | Ja | mobileRole, mobilePriority, customMobile |
| Kolonnevelger | Opt-in | allowColumnConfig, allowColumnReorder, gridId |
| Rad-virtualisering | Opt-in | virtualScroll, rowHeight, virtualOverscan |
| Uendelig scroll (server-viewport) | Opt-in | infiniteScroll, infiniteScrollThreshold, hasMoreRows, moreRowsRequested, acknowledgeMoreRows() |
Første grid¶
Dette er et komplett minimumsmønster for en vanlig klient-side liste. Kopier strukturen, bytt radmodell og i18n-nøkler, og koble til riktig service.
TypeScript:
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { GridColumn } from 'src/app/shared/smart-grid/smart-grid.models';
interface CustomerRow {
id: number;
customerNo: string;
customerName: string;
status: 'Active' | 'Inactive';
updatedDate?: string;
}
@Component({
selector: 'app-customer-list',
templateUrl: './customer-list.component.html',
})
export class CustomerListComponent implements OnInit {
rows: CustomerRow[] = [];
loading = false;
columns: GridColumn<CustomerRow>[] = [];
rowKey = (row: CustomerRow) => row.id;
constructor(
private customerService: CustomerService,
private router: Router,
private translate: TranslateService
) {}
ngOnInit(): void {
this.columns = this.buildColumns();
this.loadRows();
}
private buildColumns(): GridColumn<CustomerRow>[] {
return [
{
field: 'customerNo',
titleKey: 'Customer.List.CustomerNo',
sortable: true,
filterable: true,
filterType: 'text',
width: 120,
mobileRole: 'meta',
},
{
field: 'customerName',
titleKey: 'Customer.List.CustomerName',
sortable: true,
filterable: true,
filterType: 'text',
mobileRole: 'title',
},
{
field: 'status',
titleKey: 'Common.Status',
sortable: true,
filterable: true,
filterType: 'select',
selectOptions: [
{ label: this.translate.instant('Common.Active'), value: 'Active' },
{ label: this.translate.instant('Common.Inactive'), value: 'Inactive' },
],
mobileRole: 'badge',
},
{
field: 'updatedDate',
titleKey: 'Common.Updated',
sortable: true,
filterable: true,
filterType: 'date',
width: 130,
mobileRole: 'meta',
format: row => row.updatedDate ?? '',
},
{
field: '__actions__',
title: '',
width: 96,
resizable: false,
sortable: false,
filterable: false,
mobileRole: 'actions',
},
];
}
loadRows(): void {
this.loading = true;
this.customerService.getCustomers().subscribe({
next: customers => {
this.rows = customers.map(customer => ({
id: customer.id,
customerNo: customer.customerNo,
customerName: customer.customerName,
status: customer.active ? 'Active' : 'Inactive',
updatedDate: customer.updatedDate,
}));
},
complete: () => {
this.loading = false;
},
});
}
openCustomer(row: CustomerRow): void {
this.router.navigate(['/crm/customers', row.id]);
}
editCustomer(row: CustomerRow): void {
this.router.navigate(['/crm/customers', row.id, 'edit']);
}
deleteCustomer(row: CustomerRow): void {
this.customerService.deleteCustomer(row.id).subscribe(() => this.loadRows());
}
}
HTML:
<konti-smart-grid
[data]="rows"
[columns]="columns"
[rowKey]="rowKey"
[loader]="loading"
[initialSort]="[{ field: 'customerName', dir: 'asc' }]"
[showGlobalSearch]="true"
[globalSearchPlaceholder]="'Common.Search' | translate"
[showPagination]="true"
[pageSizes]="[25, 50, 100]"
[pageSize]="25"
[noRecordsFoundPanel]="true"
[noRecordsFoundText]="'Common.NoRecordsFound' | translate"
(rowClick)="openCustomer($event)">
<ng-template appSmartGridCell="status" let-row>
<span class="badge badge-soft-success" *ngIf="row.status === 'Active'">
{{ 'Common.Active' | translate }}
</span>
<span class="badge badge-soft-secondary" *ngIf="row.status !== 'Active'">
{{ 'Common.Inactive' | translate }}
</span>
</ng-template>
<ng-template appSmartGridCell="__actions__" let-row>
<div class="action-capsule btn-group btn-group-sm sg-desktop-only" (click)="$event.stopPropagation()">
<button type="button" class="btn btn-outline-secondary" [title]="'Common.Edit' | translate" (click)="editCustomer(row)">
<i class="bi bi-pencil" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-outline-danger action-danger" [title]="'Common.Delete' | translate" (click)="deleteCustomer(row)">
<i class="bi bi-trash3" aria-hidden="true"></i>
</button>
</div>
</ng-template>
</konti-smart-grid>
selectOptions.label brukes direkte av komponenten i filterdropdown. Hvis du vil vise oversatte labels i select-filteret, bygg arrayet i parent med translate.instant(...) eller eksisterende oversatte verdier.
CustomerService, SettingsService og andre service-navn i eksemplene er representative fagservices. Bruk den faktiske servicen for modulen du bygger i.
Tekster og i18n¶
Bruk titleKey for kolonneoverskrifter når nøkkelen finnes i i18n. For andre tekster som sendes inn som rene strings, bind oversatt verdi fra parent.
| Tekst | Anbefalt mønster |
|---|---|
| Kolonneoverskrift | titleKey: 'Customer.List.CustomerName' |
| Select-filter labels | this.translate.instant('Common.Active') når columns bygges |
| Globalt søk placeholder | [globalSearchPlaceholder]="'Common.Search' | translate" |
| Tomtilstand | [noRecordsFoundText]="'Common.NoRecordsFound' | translate" |
| Eksportknapp | [exportButtonLabel]="'Common.ExportToExcel' | translate" |
| Action-knapp title | [title]="'Common.Edit' | translate" |
Ikke hardkod norsk tekst i komponentkode eller nye eksempler. Hvis smart-grid har eldre interne defaulttekster, overstyr dem i parent når teksten er synlig for brukeren.
GridColumn¶
GridColumn<T> er hovedkontrakten. field må matche property-navnet i radobjektet, inkludert camelCase fra backend-DTO-er. Dot-path støttes for nested data, for eksempel actor.name.
| Felt | Bruk |
|---|---|
field |
Radfeltet som leses. Bruk __actions__, actions eller __actions for handlingskolonne. |
title / titleKey |
Synlig kolonnenavn. Bruk titleKey når teksten finnes i i18n. titleKey oversettes til title i komponenten. |
width, minWidth, resizable |
Bredde og om brukeren kan dra i headerkanten. Action-/checkbox-kolonner bør vanligvis ha resizable: false. Ved beregning av tabellens minimumsbredde teller kolonnen det den faktisk rendres som: er begge satt, teller den største av dem; er én satt, teller den; er ingen satt, brukes 160 px (110 px for mobileRole: 'badge') — se enforceMinimumTableWidth. |
pinned |
Fester kolonnen til venstre eller høyre ved horisontal scroll. Flere pinnede kolonner stables. |
hidden |
Skjuler kolonnen i gridet. |
menuDisabled |
Skjuler tre-prikk-header-menyen for denne ene kolonnen, selv når showColumnMenu er på gridnivå. Se «Kolonnemeny (tre-prikk header-meny)». |
exportOnly |
Tar kolonnen med i eksport, men ikke i DOM-visningen. |
sortable, filterable, filterType |
Slår på sortering og filterrad per kolonne. filterType er text, number, date eller select. |
selectOptions, removeDefaultOption |
Valg for select-filter. Default-valget "Alle" vises hvis removeDefaultOption ikke er true. |
aggregate |
Kan brukes av forbrukere som avleder aggregatoppsett fra kolonner. Selve gridet bruker groupAggregates. |
format |
Returnerer tekst/verdi for cellen og eksporten. HTML blir escapet og skal ikke returneres her. |
exportValue |
Opt-in (row) => unknown for Excel-eksporten (MO-091 #20): når satt foretrekker klient-eksporten denne fremfor format for CELLEVERDIER. Returner rå tall for beløpskolonner hvis format viser lokalisert tekst (f.eks. «1 250 USD») — tallet skrives som ekte tallcelle med numFmt (per decimals) i stedet for tekst. Visning, gruppeoverskrifter og PDF-eksport bruker fortsatt format. Server-side eksport-kontrakt påvirkes ikke (funksjonen kan ikke serialiseres — bruk exportFormatHint). |
groupValueLabel |
Valgfri (value) => string \| null. Når kolonnen brukes som grupperingsfelt, slår denne opp en lesbar etikett for gruppeverdien (f.eks. prosjektnummer → prosjektnavn). Etiketten vises ved siden av rå-verdien i gruppe-overskriften (full tekst ved peking). Returner null/tom streng for å kun vise rå-verdien. Kalles kun per gruppe-overskrift. |
align, className |
Justering og CSS-klasse på celler. |
numberFormat, decimals |
Formaterer tall med norske tusenskilletegn og gitt antall desimaler. Excel-eksporten respekterer decimals. |
editable, editType, editValidator, editMin, editMax, editSelectOptions |
Inline redigering ved dobbeltklikk. |
bulkEditable |
Opt-in: gjør kolonnen til et gyldig mål for bulk-redigering / «kopier verdi til utvalg». Gridet avviser bulk-forespørsler for kolonner uten dette. |
pendingPreviewValue |
(row, col, pendingRow) => unknown. Kun relevant i pending-modus (se «Pending changes (Lagre/Forkast)»). Display-only beregnet forhåndsvisning for en avledet kolonne (f.eks. Beløp = Antall × Enhetspris) mens raden har ulagrede endringer. pendingRow er radens feltverdier med pending-overlayet lagt oppå. Verdien persisteres aldri og er ikke del av pendingSave-batchen. |
pendingBypass |
Opt-out per kolonne i pending-modus: commits på denne kolonnen går UTENOM pending-overlayet og når host via den umiddelbare flyten for kolonnen — inline/lookup-redigering emitter cellEdit; ved blandet innliming emittes KUN bypass-cellene via clipboardPaste (legacy _lastPasteChanges populeres ikke), resten går til overlayet. For kolonner der committen utløser en tung umiddelbar host-flyt (server-validering, avledede oppslag, prisoppslag) som ikke skal ligge i batchen. Ingen effekt utenfor pending-modus. |
cellClassRules |
Betinget celle-styling: { cssClass: (row) => boolean }. Klassen settes på cellen når predikatet er sant. Additivt oppå className. |
mobileRole, mobilePriority |
Styrer auto-generert mobilkort. Roller: title, subtitle, badge, meta, actions, hidden. |
Feltmatching¶
field er den viktigste verdien i hele oppsettet. Den brukes til:
- Å hente cellens verdi fra radobjektet.
- Å matche
appSmartGridCellogappSmartGridEdit. - Å sortere, filtrere, gruppere og eksportere.
- Å persistere kolonnebredde, rekkefølge og synlighet.
Eksempel:
interface CustomerRow {
id: number;
customerName: string;
actor: {
name: string;
};
}
columns: GridColumn<CustomerRow>[] = [
{ field: 'customerName', titleKey: 'Customer.Name' },
{ field: 'actor.name', titleKey: 'Actor.Name' },
];
Bruk dot-path bare når radobjektet faktisk har nested struktur. Hvis API-et returnerer PascalCase eller databasefelt, map det til en frontend view-model først:
const row: CustomerRow = {
id: dto.CustomerID,
customerName: dto.CustomerName,
actor: {
name: dto.ActorName,
},
};
Kolonnedesign¶
| Behov | Anbefalt kolonneoppsett |
|---|---|
| Primær tekstkolonne | mobileRole: 'title', sortable: true, filterable: true, ingen fast bredde hvis den skal fylle resten. |
| Id, nummer eller kode | Fast width, mobileRole: 'meta', ofte filterType: 'text'. |
| Status eller type | filterType: 'select', selectOptions, mobileRole: 'badge', custom cell-template for badge. |
| Beløp eller antall | align: 'right', numberFormat: true, decimals, eventuelt aggregate. |
| Dato | filterType: 'date', fast bredde, mobileRole: 'meta'. |
| Handlinger | field: '__actions__', fast bredde, resizable: false, sortable: false, filterable: false, mobileRole: 'actions'. |
| Kun eksport | exportOnly: true, gjerne uten template. |
Inputs¶
Bind felter, ikke gettere¶
Angular avgjør om ngOnChanges skal kjøre ved å sammenligne input-referanser, og memoiserer
template-literaler gjennom pureFunction*. Det betyr at [pageSizes]="[25, 50, 100, 200]" og
[rowClassRules]="{ aktiv: erValgt }" er trygge — Angular gir gridet samme instans hver syklus.
En getter eller et metodekall kan ikke memoiseres. [data]="filtrerteRader()" eller
[initialGroups]="grupper" der grupper er en getter, gir gridet en ny referanse hver eneste
endringsdeteksjon. Løft verdien til et felt og bytt den bare når den faktisk endres:
// ✅ felt, byttes når noe skjer
gridGroups: GridGroup[] = [];
private oppdaterGruppering(): void {
this.gridGroups = this.flereSelskaper ? [{ field: 'selskap' }] : [];
}
// 🚫 getter — nytt array hver lesning
get gridGroups(): GridGroup[] {
return this.flereSelskaper ? [{ field: 'selskap' }] : [];
}
For config-inputer (initialGroups, pageSizes, groupAggregates og lignende) bygger gridet
seg ikke lenger om når innholdet er uendret, så en slik binding gjør ikke siden ubrukelig — men
verten kjører fortsatt uttrykket hver syklus, og gridet logger én advarsel i dev-modus med
navnet på den skyldige inputen. Se trap #46 i CLAUDE.md.
Rad-inputene er unntaket, og de er ikke dekket. data, pinnedTopRows og pinnedBottomRows
regnes alltid som endret når referansen er ny. Det er en kontrakt flere sider bygger på: muter et
radobjekt og tildel en ny array for å utløse ny rendring (this.linjer = [...this.linjer]). Ville
gridet sammenlignet radidentitet her, ville en ekte redigering blitt sett som ingenting og latt
filter, sortering, gruppering, aggregater og paginering stå igjen utdatert.
Konsekvensen er verdt å være tydelig på: en getter bundet til [data] bygger fortsatt om gridet
hver syklus, og gridet advarer ikke om det. Det kan det ikke — en churnende getter og en ekte
mutate-and-clone gir begge en ny array med de samme radobjektene, og å telle på det ville før eller
siden gitt falsk alarm ved innliming eller en serie redigeringer. [data]-gettere må derfor rettes i
verten, slik bankavstemmingens dashboard ble rettet.
| Input | Default | Beskrivelse |
|---|---|---|
data |
[] |
Alle rader gridet skal vise. Ved klientpaging slices denne lokalt. Ny referanse = nye data, alltid. |
columns |
[] |
Kolonnedefinisjoner. |
initialSort |
[] |
Startsortering, f.eks. { field: 'name', dir: 'asc' }. |
initialGroups |
[] |
Startgruppering. |
groupingExemptRow |
undefined |
Predikat (row) => boolean. Rader som treffer, holdes frittstående i stedet for å legges i en gruppebøtte mens gruppering er aktiv. |
groupingExemptPendingRows |
false |
Holder rader med ulagrede endringer frittstående til lagringen er bekreftet, slik at raden ikke hopper mellom grupper mens du skriver. |
groupingExemptPosition |
'top' |
Hvor de frittstående radene plasseres når gruppering er aktiv: 'top' eller 'bottom'. |
groupingExemptAutoReveal |
false |
Ruller gridet til de frittstående radene (topp eller bunn, styrt av groupingExemptPosition) når en ulagret endring flytter en rad ut av gruppebøttene. Krever groupingExemptPendingRows. |
initialFilter |
[] |
Filter som settes ved init/endring. |
initialGlobalSearch |
undefined |
Forhåndsutfyller det globale søkefeltet ved init (uten å emitte globalSearchChange) — brukes til å gjenopprette et lagret søkeord ved navigasjon. |
showGroupPanel |
true |
Viser verktøylinje for gruppering, søk, eksport og custom toolbar. |
showFilterPanel |
true |
Tillater filterikon og filterrad i headeren. |
showFilterTypePanel |
false |
Viser operatorvalg for filtertyper der det støttes. |
showResetButton |
true |
Viser knapper for å nullstille aktive filtre/sortering. |
showGlobalSearch |
false |
Viser globalt søk i toolbar. |
globalSearchPlaceholder |
Søk... |
Placeholder for globalt søk. Bruk i18n i parent hvis teksten skal overstyres. |
globalSearchDebounce |
400 |
Debounce (ms) på tastetrykk i det globale søket før søket kjøres. Enter, tap av fokus (blur) og søkeknappen inne i feltet søker umiddelbart (forbi debouncen). |
customMobile |
false |
Slår av gridets auto-mobilkort slik at parent kan eie mobilvisningen selv. |
enforceMinimumTableWidth |
true |
Setter beregnet min-width på tabellen = summen av kolonnenes minimumsbredder. Per kolonne teller det den faktisk rendres som: erklærer kolonnen både width og minWidth, teller den største av dem — en kolonne med width: 200, minWidth: 150 bidrar 200, ikke 150, slik at tabellen aldri kan klemmes smalere enn sitt eget innhold. Erklærer den bare én av dem, teller den. Erklærer den ingen, brukes en rollebasert fallback (160 px, 110 px for mobileRole: 'badge', egen verdi for handlingskolonner). Fallbacken er altså ikke et gulv over erklærte verdier: en bevisst smal kolonne blåses ikke opp til 160 px. En brukerdratt kolonnebredde vinner uansett, siden det er bredden som faktisk rendres. Overskytende bredde blir horisontal scroll i .table-responsive. Sett false kun på et grid som bevisst skal klemme kolonnene sammen i stedet for å scrolle. |
singleLineCells |
false |
Holder celler på én linje og bruker ellipsis der CSS tillater det. |
virtualScroll |
false |
Renderer bare synlig radvindu + overscan for flate, ugrupperte desktop-tabeller. |
rowHeight |
40 |
Forventet uniform radhøyde i px når virtualScroll er aktiv. Radene må faktisk rendres med denne høyden. |
virtualOverscan |
8 |
Ekstra rader som rendres over/under viewport i virtualisert modus for å unngå blanke glimt ved scrolling. |
infiniteScroll |
false |
Opt-in: emitter moreRowsRequested når det virtualiserte vinduet nærmer seg slutten av de lastede radene. Krever virtualScroll og rowKey. Se «Uendelig scroll». |
infiniteScrollThreshold |
10 |
Hvor mange rader fra slutten vinduet må nå før flere rader etterspørres. |
mobileRowCap |
0 (av) |
Tak på hvor mange rader mobil-kortlista holder. Kortene virtualiseres ikke — én akkumulert rad er ett levende kort — så uten tak fryser fanen på store sett (målt 390x844: 5 000 kort = 166 764 DOM-noder, 596 MB heap, 1,25 s per scroll-steg; 1 000 kort = 34 671 noder, 274 ms). Taket begrenser både henting og rendring, siden en desktop-økt kan akkumulere forbi taket og så roteres til telefon. Desktop-tabellen er vinduert og har aldri tak. |
hasMoreRows |
false |
Host-eid: finnes det flere rader, OG kan hosten hente dem akkurat nå? Gridet utleder aldri dette selv — «uttømt», «opptatt» og «pauset etter feil» er hostens vurdering. |
moreRowsOnServer |
false |
Host-eid: holder serveren fortsatt rader utover det som er lastet? I motsetning til hasMoreRows slukkes den ikke av en henting som er underveis eller som feilet. Brukes kun til å skille en avkortet mobil-kortliste fra en komplett — en host som bare binder hasMoreRows beholder forrige oppførsel. |
rowKey |
ikke satt | Stabil radnøkkel. Brukes for trackBy, mobil expand-state og inline edit. Anbefales alltid. |
pendingChanges |
false |
Opt-in: se «Pending changes (Lagre/Forkast)». Samler mutasjoner (inline edit, paste, bulk-kopi) i et in-memory overlay til brukeren trykker Lagre, i stedet for å emitte hver endring med det samme. Krever rowKey — uten den logges én console.warn og pending-modus forblir av. |
rowClass |
ikke satt | Funksjon som returnerer CSS-klasser for en rad. |
loader |
false |
Ekstern loading-state. Gridet legger også på intern processing-loader ved tunge operasjoner. |
noRecordsFoundPanel |
false |
Viser tom-tilstand når rows.length === 0. |
noRecordsFoundText |
'' |
Tekst i tom-tilstand. |
bsConfig |
null |
Bruker ngx-bootstrap datepicker i datofilter når satt; ellers native type="date". |
showPagination |
false |
Viser/bruker paginering. |
pageSizes |
[] |
Valgbare sidestørrelser. |
pageSize |
ikke satt | Aktiv sidestørrelse. Må settes når showPagination er på. |
currentPage |
1 |
Aktiv side. Kan bindes sammen med currentPageChange. |
totalRows |
0 |
Totalt antall rader ved server-side eller ekstern paging. |
serverSidePagination |
false |
Parent/API håndterer paging; gridet slicer ikke data. |
serverSideFilter |
false |
Eldre flagg for server-side filter. Ny bruk bør vurdere serverSideSearch. Gridet re-filtrerer ikke de leverte radene klient-side når denne er på — backend har allerede filtrert hele treffsettet, akkurat som serverSideSort gjør for rekkefølge. Uten dette avviser en PSEUDO-kolonne (filtrerbar, beregnet per rad, aldri på DTO-en) hver eneste rad, og gridet viser et tomt resultat for et filter serveren svarte riktig på. |
serverSideFilterDebounce |
300 |
Debounce i ms før serverfilter event. |
serverSideSearch |
false |
Filterendringer emitter serverFilterChange i stedet for klientfiltrering. |
serverSideSort |
false |
Server-side sortering: gridet re-sorterer ALDRI de leverte radene klient-side (serverens sideordning bevares, også i eksport), og header-interaksjon normaliseres til ÉN sortering — shift-klikk akkumulerer ikke flersortering, og en gjenopprettet visning med flere sorteringer kollapses til den første. sortChange emitteres som før; host sender sorteringen til API-et og laster siden på nytt. Brukes sammen med serverSidePagination (referanse: Freshdesk-sakslisten). |
checkedColumn |
'' |
Feltet som får header-checkbox for "velg alle synlige". |
getAllCheckBox |
false |
Nullstiller header-checkbox ved sidebytte og emitter currentPageChange. |
groupAggregates |
{} |
Aggregater per felt, f.eks. { amount: ['sum'] }. Gjelder både gruppe-modus (per gruppe) og tre-modus (rullet per node — se «Rullede aggregater per node»). |
aggregateFilters |
{} |
Per-felt/per-aggregat filterfunksjoner for hvilke rader som skal inngå i aggregater. Samme kontrakt i gruppe- og tre-modus (delt beregning). |
showTotalRow |
false |
Viser totalrad. Ikke i tre-modus (v1-avgrensning — footeren er gated av !treeData). |
showFieldSum |
false |
Viser totalsum under aktuell kolonne i stedet for samlet totalbar. |
aggregateFilterConfig |
ikke satt | Konfigurerer toggle i toolbar som kan filtrere aggregatgrunnlaget. |
autoCollapseThreshold |
5000 |
Auto-kollaps (og advarsel ved «utvid alle») når antallet filtrerte/grupperte rader overstiger terskelen — gjelder kun topp-nivå-grupper. Filtrerer brukeren ned under terskelen, auto-kollapses ikke gruppene. Sett 0 for å slå av terskelen. |
preserveGroupCollapse |
false |
Opt-in (BRQ-39): husker en gruppes kollaps/utvid-tilstand (bruker-toggle eller «utvid/kollaps alle») gjennom en rekalkulering — filterbytte, datarefresh, en gruppe som forsvinner og kommer tilbake. false (standard) er nøyaktig som før: hver rekalkulering løser tilstand friskt fra GridGroup.collapsed ?? autoCollapseThreshold, ingen huske-tilstand slås opp. Nøkkelen er den fulle grupperingsstien (rot til node) og deklinerer bevisst huske-tilstand for en gruppe der noen verdi i stien ikke er en primitiv (objekt/array) — den grupperingen faller alltid tilbake til standardverdien. Tilstanden nullstilles kun når den ORDNEDE listen av grupperingsfelt endres (ikke ved datarefresh/filterbytte). Å slå inputen av og på igjen i runtime starter ikke blankt: tilstand som ble husket mens den sist var på, ligger fortsatt i kartet og dukker opp igjen ved neste rekalkulering. |
enableExport |
false |
Viser eksport-dropdown. |
exportButtonLabel |
Excel |
Tekst på eksportknappen (brukes når kun ['excel'] er aktivert). |
exportFormats |
['excel'] |
Hvilke formater eksport-dropdownen tilbyr. Legg til 'pdf' for å slå på klientbygget PDF-eksport (samme kolonner/rader som Excel). Additivt/opt-in — standard ['excel'] er uendret. |
exportFilename |
ikke satt | Filnavn uten .xlsx/.pdf. |
resizableColumns |
true |
Slår på kolonne-resize globalt. Kan overstyres per kolonne. |
allowColumnConfig |
false |
Viser Kolonner-panel for vis/skjul/rekkefølge/bredde. Opt-in. |
gridId |
ikke satt | LocalStorage-namespace for kolonnekonfigurasjon: smartgrid.colcfg.<gridId>. |
columnConfig |
ikke satt | Parent-styrt startkonfigurasjon for kolonner. Tar presedens over localStorage. |
allowColumnReorder |
false |
Tillater drag-and-drop av kolonneoverskrifter. Opt-in. |
showColumnMenu |
false |
Viser en tre-prikk-meny per kolonne-header som samler sortér/filtrér/pin/skjul/tilpass bredde/administrer kolonner. Opt-in; per-kolonne menuDisabled skjuler menyen. |
autoSizeColumns |
false |
Måler header + et sample av synlige rader og setter bredder etter første render. |
maxAutoWidth |
500 |
Maks automatisk bredde per kolonne. |
autoSizeSampleSize |
50 |
Maks antall rader som samples ved autostørrelse (jevnt fordelt over de synlige radene). |
autoSizeStrategy |
'both' |
Hva som måles: 'header' (headerbredde), 'cells' (celletekst), 'both' (maks av begge). |
autoSizeMode |
'fitContent' |
'fitContent' setter målt bredde; 'growOnly' krymper aldri en kolonne under gjeldende bredde. |
resetSetting |
false |
Tvinger reset av filter/sort/paging-state ved endring. |
selectionMode |
'none' |
Opt-in nøkkelbasert radvalg. 'multiple' aktiverer valg; krever stabil rowKey. |
selectionState |
ikke satt | To-veis ([(selectionState)]) SmartGridSelectionState for å kontrollere/gjenopprette valg deklarativt. |
bulkActions |
[] |
Deklarative masseoperasjon-knapper (SmartGridBulkAction[]) i den innebygde handlingslinjen; klikk utløser bulkActionRequested via requestBulkAction(id). Linjen vises kun når lista er ikke-tom. icon er Bootstrap-suffiks uten bi- (f.eks. 'trash3'). |
showSelectionBar |
false |
Opt-in: vis den innebygde handlingslinjen for merkede rader. Kun relevant med selectionMode='multiple' + bulkActions; default av (eksisterende merkbare grid får ingen linje uten at de ber om det). |
expandableRows |
false |
Slår på utvidbare detalj-rader (full-bredde detalj under raden). |
singleRowExpansion |
false |
Maks én utvidet rad om gangen. |
expandedRowKeys |
[] |
To-veis ([(expandedRowKeys)]) sett av utvidede rad-nøkler. |
detailPanel |
'none' |
'right' (side-ved-side) eller 'bottom' (vertikal splitt under) viser et integrert, justerbart detaljpanel. |
detailPanelWidth |
360 |
Initial bredde i px for right-panelet (brukeren kan dra splitten). |
detailPanelBottomHeight |
240 |
Initial høyde i px for bottom-panelet (brukeren kan dra splitten). |
activeDetailRowKey |
ikke satt | To-veis ([(activeDetailRowKey)]) aktiv rad som driver detaljpanel + highlight. |
enableDetailPanelToggle |
false |
Viser en detaljpanel-kontroll i verktøylinjen (ved Kolonner/Visninger): en 3-delt visningsvelger (Kun liste / Delt / Kun detaljer) pluss høyre/bunn-posisjonsknapper. Posisjonsbytte: gridet skriver aldri detailPanel selv — klikk emitter detailPanelPositionRestore, og hosten flipper inputen (samme kontrakt som view-restore). Visningsmodus eies av gridet (detailViewMode) og trenger ingen host-binding. |
treeData |
false |
Slår på hierarkisk visning (innrykk + expand/collapse). Krever getChildren eller getParentKey. |
getChildren |
ikke satt | (row) => row[] for hierarkisk tre-data (data = røtter). |
getParentKey |
ikke satt | (row) => key \| null for flat liste med foreldre-referanse (røtter = null). |
treeColumn |
ikke satt | Felt som viser innrykk + tre-toggle (default første synlige kolonne). |
treeIndent |
18 |
Innrykk per tre-nivå i px. |
treeExpandedKeys |
[] |
To-veis ([(treeExpandedKeys)]) — kun AKTIVT utvidede tre-noder; passiv restored intent for ulastede lazy-noder finnes bare i view-state (se «Lazy-lasting av barn»). |
loadChildren |
ikke satt | (row) => Promise<row[]> \| Observable<row[]> — async barn-henting ved første utfoldelse (lazy tre, hierarkisk modus). |
treeHasChildrenHint |
ikke satt | (row) => boolean — om en node har barn når de ikke er lastet ennå (driver togglen; eksplisitt [] fra getChildren/cache vinner). |
rowReorder |
false |
Slår på drag-and-drop rad-reordering (grip-håndtak + drop). |
canDropRow |
ikke satt | Valgfri guard (event) => boolean; returner false for å avvise et slipp. |
pinnedTopRows |
[] |
Host-rader klistret øverst, utenfor sort/filter/paging/selection. |
pinnedBottomRows |
[] |
Host-rader klistret nederst, utenfor sort/filter/paging/selection. |
errorState |
false |
Slår på feil-overlay (kun når appSmartGridError-template finnes). |
keyboardNavigation |
false |
Opt-in roving-tabindex + ARIA grid-modell for tastaturnavigasjon. Krever stabil rowKey. |
gridAriaLabel |
undefined |
Tilgjengelig navn på selve tabellen, gjengitt som en visuelt skjult <caption>. Uten den annonserer en skjermleser bare «grid» — på en side med to grid er det ingen måte å skille dem. Sett den når siden har mer enn ett grid, eller når overskriften over griden ikke sitter rett ved. |
virtualColumns |
false |
Opt-in horisontal kolonne-windowing for brede grids. Krever eksplisitte kolonnebredder. |
columnBuffer |
2 |
Antall ekstra kolonner på hver side av viewporten i kolonne-virtualisering. |
rowClassRules |
ikke satt | Betinget rad-styling: { cssClass: (row) => boolean }. Additivt oppå rowClass. |
exportDataProvider |
ikke satt | () => Promise<T[]> \| Observable<T[]> som henter hele datasettet for server-side eksport. |
serverSideExport |
false |
Opt-in (H-SG-MO-006): eksportknappen emitter exportRequested med en SmartGridExportRequest i stedet for å bygge klient-Excel. Se «Server-side eksport-request». |
exportRowScope |
'filteredResult' |
Rad-omfang for server-eksport: 'filteredResult' (hele det filtrerte settet) eller 'selection' (kun query.selection). |
exportTreeOptions |
{ visibleOnly:true, includeCollapsedChildren:false, includeHierarchyPath:false } |
Tre-data-valg for eksport: klient-eksporten bruker valgene direkte (se «Tre-eksport»), server-side eksport sender dem videre i requesten. Brukes kun når treeData er på. |
Outputs¶
| Output | Payload | Når brukes den |
|---|---|---|
rowClick |
Radobjektet | Åpne detaljvisning når bruker klikker en rad. Emitteres også når brukeren trykker Enter på en fokusert celle med keyboardNavigation på — se «Tastaturnavigasjon og tilgjengelighet». Husk å stoppe propagation i action-knapper. |
sortChange |
GridSort[] |
Når parent skal lagre eller sende sortering til backend. |
filterChange |
GridFilter[] |
Klientfilter endret. Debouncet internt. |
serverFilterChange |
ColumnFilter[] |
Server-side søk/filter er aktivt og filter skal sendes til API. |
globalSearchChange |
string |
Emitteres når brukeren endrer teksten i det innebygde globale søkefeltet (showGlobalSearch) og serverSideSearch er på. Gridet filtrerer ikke selv i dette tilfellet — kun én side er lastet server-side — så host må lese den aktive søketeksten (typisk via getQueryState().globalSearch) og laste siden på nytt. Se CRM-kunde-/kontaktlistene for referansebruk. |
groupChange |
GridGroup[] |
Bruker legger til/fjerner gruppering. |
pageChange |
{ currentPage, pageSize } |
Sidestørrelse endres eller parent må laste ny side. |
currentPageChange |
number |
Aktiv side endres. |
allChecked |
{ checked, rows } |
Header-checkbox toggles; rows er synlige rader. |
cellEdit |
CellEditEvent<T> |
Inline edit lagres. Parent må oppdatere data/API. |
columnConfigChange |
ColumnConfigEntry[] |
Bruker endrer kolonnepanel, rekkefølge eller bredde når opt-in er aktiv. |
selectionStateChange |
SmartGridSelectionState |
Valg endres ved brukerhandling (nøkkelbasert selection). |
rowSelectionChange |
{ row, key, selected } |
Per-rad valg-endring i nøkkelbasert selection. |
bulkEditRequested |
SmartGridBulkEditRequest |
«Kopier verdi til utvalg»-intensjon. Host validerer rettigheter og persisterer. |
bulkActionRequested |
SmartGridBulkActionRequest |
Navngitt bulk-handling over utvalget. Host bekrefter og kaller API. |
pendingSave |
SmartGridPendingSaveEvent<T> |
Se «Pending changes (Lagre/Forkast)». Emitteres når brukeren trykker Lagre i pending-modus. Host persisterer changes og kaller complete(result) nøyaktig én gang. |
exportRequested |
SmartGridExportRequest |
Server-side eksport-intensjon (når serverSideExport er på). Host poster requesten til eget endepunkt og streamer fila. Se «Server-side eksport-request». |
rowExpand |
T |
En detalj-rad utvides. Host kan lazy-loade detalj-data. |
rowCollapse |
T |
En detalj-rad kollapses. |
expandedRowKeysChange |
Array<string \| number> |
Utvidet tilstand endres (to-veis [(expandedRowKeys)]). |
activeDetailRowKeyChange |
string \| number \| undefined |
Aktiv detalj-rad endres (to-veis [(activeDetailRowKey)]). |
rowDetailRequested |
T |
Rad aktiveres for detaljpanel. Host kan lazy-loade eller åpne egen skuff. |
detailPanelPositionRestore |
'right' \| 'bottom' |
Lagret visning ELLER verktøylinje-veksleren (enableDetailPanelToggle) ber om orientering. Host setter detailPanel deretter (gridet muterer aldri inputen selv). |
treeExpandedKeysChange |
Array<string \| number> |
AKTIV tre-ekspansjon endres (to-veis [(treeExpandedKeys)]) — passiv restored lazy-intent emittes aldri som aktiv. |
treeChildrenLoaded |
SmartGridTreeChildrenLoadedEvent |
Lazy-barn lastet, validert, cachet og utfoldet (H-SG-MO-007e). |
treeChildrenLoadError |
SmartGridTreeChildrenLoadErrorEvent |
Lazy-lasting feilet — noden forblir kollapset; host viser toast, neste klikk prøver igjen. |
rowReorderChange |
SmartGridRowReorderEvent<T> |
Rad sluppet etter drag. Host beregner og lagrer ny sortnøkkel. |
moreRowsRequested |
SmartGridMoreRowsRequest |
Vinduet har nådd terskelen og hosten skal hente neste serverside. Hosten MÅ kvittere med acknowledgeMoreRows(requestId) nøyaktig én gang. Se «Uendelig scroll». |
Modelltyper¶
Disse typene brukes i inputs og outputs. Importer dem fra smart-grid.models.ts.
| Type | Form | Bruk |
|---|---|---|
FilterType |
text, number, date, select |
Forteller gridet hvilken filterkontroll som skal rendres. |
FilterOp |
contains, startsWith, endsWith, eq, neq, lt, lte, gt, gte |
Operator for aktivt filter. |
GridFilter |
{ field, type, operator, value } |
Klientfilter og initialFilter. |
ColumnFilter |
{ field, operator, value } |
Server-side filterevent fra serverFilterChange. |
GridSort |
{ field, dir } |
Sortering, der dir er asc eller desc. |
GridGroup |
{ field, collapsed } |
Grupperingsnivå. Rekkefølgen i arrayet er gruppenivåene. collapsed (BRQ-39) styrer initial kollaps-tilstand for gruppen på det nivået — groupInstruction.collapsed ?? autoCollapseThreshold-fallback. Utelatt = uendret standard (kun autoCollapseThreshold-styrt). |
CellEditEvent<T> |
{ row, field, oldValue, newValue } |
Payload når inline edit lagres. |
ColumnConfigEntry |
{ field, hidden, order, width, pinned } |
Persistérbar kolonnestate når allowColumnConfig er aktiv. pinned ('left'/'right') lagres når brukeren fester en kolonne via velgeren. |
AggregateFilter<T> |
{ [field]: { [kind]: row => boolean } } |
Filtrerer hvilke rader som teller med i aggregater. |
Eksempel på state som parent kan lagre:
savedSort: GridSort[] = [
{ field: 'customerName', dir: 'asc' },
];
savedFilter: GridFilter[] = [
{ field: 'status', type: 'select', operator: 'eq', value: 'Active' },
];
savedGroups: GridGroup[] = [
{ field: 'department', collapsed: false },
];
savedColumnConfig: ColumnConfigEntry[] = [
{ field: 'customerName', hidden: false, order: 0, width: 260 },
{ field: 'status', hidden: false, order: 1, width: 120 },
];
Filter og sortering¶
Kolonner må eksplisitt få sortable: true og/eller filterable: true. Filteroperatorene støtter blant annet contains, startsWith, endsWith, eq, neq, lt, lte, gt og gte. Default er contains for tekst og eq for andre filtertyper.
Når filterraden er åpen (via filterikonet eller Ctrl+F/Cmd+F med fokus i tabellen) fester den seg sticky rett under kolonneoverskriftene ved vertikal scrolling, så filterfeltene alltid er synlige. Header-radens høyde måles i kjøretid og eksponeres som CSS-variabelen --sg-header-h, så raden legger seg presist under uansett tetthet/font/zoom. Festede kolonner beholder filterfeltene sticky også horisontalt.
columns: GridColumn<OrderRow>[] = [
{ field: 'orderNo', titleKey: 'Order.No', sortable: true, filterable: true, filterType: 'text' },
{ field: 'amount', titleKey: 'Common.Amount', sortable: true, filterable: true, filterType: 'number', align: 'right', numberFormat: true, decimals: 2 },
{ field: 'orderDate', titleKey: 'Common.Date', sortable: true, filterable: true, filterType: 'date' },
{
field: 'status',
titleKey: 'Common.Status',
filterable: true,
filterType: 'select',
selectOptions: [
{ label: 'Open', value: 'Open' },
{ label: 'Closed', value: 'Closed' },
],
},
];
Server-side filtering brukes når datasettet er for stort til å filtreres i nettleseren:
<konti-smart-grid
[data]="rows"
[columns]="columns"
[showPagination]="true"
[serverSidePagination]="true"
[serverSideSearch]="true"
[totalRows]="totalRows"
[pageSize]="pageSize"
[currentPage]="currentPage"
(serverFilterChange)="loadRows({ filters: $event })"
(pageChange)="loadRows($event)">
</konti-smart-grid>
Komplett server-side mønster:
interface CustomerQuery {
page: number;
pageSize: number;
sort: GridSort[];
filters: ColumnFilter[];
}
rows: CustomerRow[] = [];
totalRows = 0;
currentPage = 1;
pageSize = 50;
sort: GridSort[] = [{ field: 'customerName', dir: 'asc' }];
filters: ColumnFilter[] = [];
loading = false;
loadRows(partial: Partial<CustomerQuery> = {}): void {
const query: CustomerQuery = {
page: partial.page ?? this.currentPage,
pageSize: partial.pageSize ?? this.pageSize,
sort: partial.sort ?? this.sort,
filters: partial.filters ?? this.filters,
};
this.currentPage = query.page;
this.pageSize = query.pageSize;
this.sort = query.sort;
this.filters = query.filters;
this.loading = true;
this.customerService.searchCustomers(query).subscribe({
next: result => {
this.rows = result.items;
this.totalRows = result.totalCount;
},
complete: () => {
this.loading = false;
},
});
}
onServerFilters(filters: ColumnFilter[]): void {
this.loadRows({ page: 1, filters });
}
onSortChange(sort: GridSort[]): void {
this.loadRows({ page: 1, sort });
}
onPageChange(event: { currentPage: number; pageSize: number }): void {
this.loadRows({
page: event.currentPage,
pageSize: event.pageSize,
});
}
<konti-smart-grid
[data]="rows"
[columns]="columns"
[rowKey]="rowKey"
[loader]="loading"
[showPagination]="true"
[serverSidePagination]="true"
[serverSideSearch]="true"
[serverSideFilterDebounce]="300"
[totalRows]="totalRows"
[currentPage]="currentPage"
[pageSize]="pageSize"
[pageSizes]="[25, 50, 100]"
(serverFilterChange)="onServerFilters($event)"
(sortChange)="onSortChange($event)"
(pageChange)="onPageChange($event)">
</konti-smart-grid>
Server-side-kontrakten bør være tydelig i API-laget. Gridet sender felt- og operatornavn, men parent må oversette dette til backendens query-format hvis backend bruker andre navn.
Gruppering og aggregater¶
Gruppering kan settes fra parent eller styres av brukeren via gruppepanelet.
<konti-smart-grid
[data]="rows"
[columns]="columns"
[initialGroups]="[{ field: 'department', collapsed: false }]"
[groupAggregates]="{ amount: ['sum'], quantity: ['sum', 'avg'] }"
[showTotalRow]="true"
[showFieldSum]="true">
</konti-smart-grid>
groupAggregates beregner aggregater på radene under hver gruppe. showTotalRow viser summer for hele datasettet. Ved store grupper bør showPagination eller backend-avgrensning brukes, ellers kan expand-all bli tungt.
Hvis et aggregat bare skal telle noen rader, bruk aggregateFilters. Eksempelet summerer bare godkjente timer:
groupAggregates = {
hours: ['sum', 'avg'],
};
aggregateFilters: AggregateFilter<TimeRow> = {
hours: {
sum: row => row.approved,
avg: row => row.approved,
},
};
<konti-smart-grid
[data]="rows"
[columns]="columns"
[initialGroups]="[{ field: 'department', collapsed: false }]"
[groupAggregates]="groupAggregates"
[aggregateFilters]="aggregateFilters"
[showTotalRow]="true">
</konti-smart-grid>
Egendefinert gruppe-header (appSmartGridGroupHeader)¶
Opt-in: erstatter standard felt: verdi - label (antall)-teksten og aggregat-pillene i en gruppe-rad med en egen template. Chevronen og klikk-for-å-kollapse på gruppe-raden er uendret. Default av → standard header vises som før.
| API | Type | Bruk |
|---|---|---|
appSmartGridGroupHeader |
strukturelt direktiv | <ng-template appSmartGridGroupHeader let-node let-level="level" let-leafRows="leafRows"> — én per grid. |
appSmartGridGroupHeaderField |
@Input() string (valgfri) |
Feltnavn direktivet skal gjelde for. Utelatt = gjelder alle grupperingsnivåer. |
Template-konteksten: $implicit er gruppe-noden ({ field, value, items, count, collapsed, aggregates? }), level er nestingsdybden (0 = toppnivå), og leafRows er alle bladrader under noden (nøstede grupper flatet ut), hentet fra treet ETTER filter/søk — samme rader grid-et faktisk viser under gruppen.
<konti-smart-grid
[data]="rows"
[columns]="columns"
[initialGroups]="[{ field: 'accountingClientKey' }]">
<ng-template appSmartGridGroupHeader appSmartGridGroupHeaderField="accountingClientKey"
let-node let-level="level" let-leafRows="leafRows">
<span class="fw-semibold">{{ node.value }}</span>
<span class="text-muted ms-2">{{ leafRows.length }} rader</span>
</ng-template>
</konti-smart-grid>
Feltskoping: med appSmartGridGroupHeaderField satt gjelder templaten kun gruppe-noder gruppert på nettopp det feltet — nøstede grupper på et annet felt beholder standard header. Uten appSmartGridGroupHeaderField gjelder templaten alle nivåer.
Valg av rader¶
Smart-grid kan vise en header-checkbox i en eksisterende kolonne når checkedColumn matcher field. Komponenten eier ikke valgt-state for hver rad; parent må selv oppdatere radene eller en ekstern selection-liste.
interface InvoiceRow {
id: number;
selected: boolean;
invoiceNo: string;
amount: number;
}
columns: GridColumn<InvoiceRow>[] = [
{
field: 'selected',
title: '',
width: 44,
resizable: false,
sortable: false,
filterable: false,
mobileRole: 'hidden',
},
{
field: 'invoiceNo',
titleKey: 'Invoice.No',
sortable: true,
filterable: true,
filterType: 'text',
mobileRole: 'title',
},
];
selectedIds = new Set<number>();
toggleVisibleRows(event: { checked: boolean; rows: InvoiceRow[] }): void {
for (const row of event.rows) {
row.selected = event.checked;
if (event.checked) {
this.selectedIds.add(row.id);
} else {
this.selectedIds.delete(row.id);
}
}
}
<konti-smart-grid
[data]="rows"
[columns]="columns"
[rowKey]="rowKey"
checkedColumn="selected"
(allChecked)="toggleVisibleRows($event)">
<ng-template appSmartGridCell="selected" let-row>
<input
type="checkbox"
class="form-check-input"
[checked]="selectedIds.has(row.id)"
(click)="$event.stopPropagation()"
(change)="toggleRow(row, $any($event.target).checked)">
</ng-template>
</konti-smart-grid>
Bruk getAllCheckBox bare hvis siden trenger å nullstille header-checkbox ved sidebytte i eldre flyter.
Robust selection-state og bulk-handlinger¶
For datatunge flater (enterprise-planen) finnes et nyere, nøkkelbasert selection-API ved siden av den eldre checkbox-flyten over. Det er opt-in og påvirker ikke eksisterende grids: standard selectionMode="none" lar alt være som før.
rowKeyer obligatorisk nårselectionModeer aktiv. Uten en stabilrowKeyer rad-identitet (og dermed eksklusjoner i «alt matchende»-modus) ustabil, og alle selection-/bulk-metoder no-op'er med en dev-advarsel.
Selection-state¶
| API | Type | Bruk |
|---|---|---|
selectionMode |
@Input() 'none' \| 'multiple' |
Aktiver nøkkelbasert valg. Default none. |
[(selectionState)] |
to-veis SmartGridSelectionState |
Kontroller/gjenopprett valg deklarativt. |
selectionStateChange |
@Output() |
Emit ved brukerhandling. |
rowSelectionChange |
@Output() { row, key, selected } |
Per-rad valg-endring. |
isRowSelected(row) / toggleRowSelection(row) |
metoder | Les/veksle ett rad-valg. |
selectAllInScope(scope) |
metode | «Velg alt» i visibleRows / currentPage / filteredResult / allMatchingServerQuery. |
clearSelection() |
metode | Nullstill. |
SmartGridSelectionState har to modi: explicit (valget ER selectedKeys) og allMatching (alt i scope UNNTATT excludedKeys — lar deg «velg alle som matcher» et server-søk uten å laste alle rader). Logikken ligger i den rene, testbare SmartGridSelectionModel.
@ViewChild('grid') grid!: SmartGridComponent<Row>;
// Velg alle rader som matcher gjeldende server-spørring:
this.grid.selectAllInScope('allMatchingServerQuery');
Bulk-handlinger og kopier-til-utvalg¶
Gridet emitter kun intensjon med gjeldende utvalg; fagkomponenten validerer rettigheter, bekrefter (ved irreversible endringer) og kaller API-et.
| API | Bruk |
|---|---|
GridColumn.bulkEditable |
Marker kolonner som er gyldige bulk-mål (opt-in). |
requestBulkValueCopy(field, value, sourceRow?) |
«Kopier denne verdien til valgte rader». Avvises for kolonner uten bulkEditable. |
requestBulkAction(actionId) |
Navngitt handling (f.eks. 'changeStatus') over utvalget. |
bulkEditRequested / bulkActionRequested |
@Output() host lytter på, validerer og persisterer. |
<konti-smart-grid #grid [columns]="columns" [data]="rows" [rowKey]="rowKey"
selectionMode="multiple"
(bulkEditRequested)="onBulkEdit($event)"
(bulkActionRequested)="onBulkAction($event)">
</konti-smart-grid>
// Eksempel: sett status «Lukket» på valgte rader
onBulkAction(e: SmartGridBulkActionRequest) {
if (e.actionId !== 'closeSelected') return;
// host: bekreft → API-kall med e.selection (validér rettigheter)
}
// Eksempel: kopier pris til utvalg
copyPrice(price: number) { this.grid.requestBulkValueCopy('price', price); }
Innebygd handlingslinje (bulkActions / showSelectionBar)¶
I stedet for å bygge en egen verktøylinje kan verten gi gridet en liste bulkActions, så
rendrer gridet en innebygd handlingslinje rett over griden når rader er merket
(selectionMode='multiple'). Linjen viser antall merkede rader («N valgt», eller «alle rader
valgt» i «alt matchende»-modus), en «Fjern valg»-knapp (clearSelection()) og én knapp per
SmartGridBulkAction som utløser bulkActionRequested via requestBulkAction(id). label er
vertens egen (allerede oversatte) tekst — gridet oversetter den ikke. icon er Bootstrap-suffiks
uten bi- (f.eks. 'trash3'). variant: 'danger' gir destruktiv knappestil. Linjen er opt-in:
sett showSelectionBar="true" (default false) OG oppgi minst én bulkActions — eksisterende
merkbare grid får ingen linje uten begge deler.
<konti-smart-grid [columns]="columns" [data]="rows" [rowKey]="rowKey"
selectionMode="multiple" [showSelectionBar]="true"
[bulkActions]="[{ id: 'closeSelected', label: 'Lukk valgte', icon: 'x-circle' },
{ id: 'delete', label: 'Slett', icon: 'trash3', variant: 'danger' }]"
(bulkActionRequested)="onBulkAction($event)">
</konti-smart-grid>
Innebygd selection-kolonne (H-SG-MO-005b)¶
Når selectionMode="multiple" (+ stabil rowKey) rendrer gridet sin egen ledende
avkryssingskolonne — hosten trenger ikke lenger en __select__-kolonne eller cell-template. Flate
grids bruker standard rad-toggle; grupperte grids får i tillegg gruppe-checkboxer (H-SG-GRPSEL, se
under); tre-grids bruker H-SG-MO-007d-atferden beskrevet under. Kolonnen:
- Ligger utenfor
renderColumns(), så den kan ikke skjules, reordnes, eksporteres eller lagres i kolonnekonfigurasjon. Bredden er fast (44px,selectionColumnWidthPx) og deltar eksplisitt icolspanCount(),aria-colcount,tableMinWidth()ogpinLeftOffset(). - Har en tri-state header select-all: checked/indeterminate reflekterer valgt-mengden. Header-en er
scope-bevisst — den betyr «alt header-scopet dekker»: klient-side
filteredResult, server-sideallMatchingServerQuery. EtallMatching-valg lagret med et smalere scope (currentPage,visibleRowseller motsatt sides scope) leses konservativt som indeterminate, aldri fullt checked, så brukeren ikke ved uhell overskriver et smalere valg. Scope-matrise:
| Modus / scope | Header |
|---|---|
klient (explicit ELLER allMatching+filteredResult) |
eksakt scan av de filtrerte radene (isSelected per rad): checked når alle valgt, indeterminate ved reelt delvis, unchecked ved null |
| allMatching + annet scope | { checked:false, indeterminate:true } (konservativt) |
server allMatching+allMatchingServerQuery |
checked uten eksklusjoner, indeterminate med (servertotal ukjent) |
| server explicit | ethvert ikke-tomt valg = indeterminate |
Klikk velger/tømmer hele header-scopet (selectAllInScope(...) / clearSelection()).
- Har per-rad checkbox bundet til isRowSelected / toggleRowSelection. Checkbox-klikk stopper
rad-klikk, så den åpner ikke detaljpanelet.
- Vises ikke ved manglende rowKey eller selectionMode="none" (da er layouten byte-for-byte
uendret). Gruppert modus støttes siden H-SG-GRPSEL og tre-modus siden H-SG-MO-007d — se under.
Gruppert modus: tri-state gruppe-checkbox (H-SG-GRPSEL). I gruppert visning (initialGroups /
brukergruppering) rendres selection-kolonnen for leaf-radene (samme plassering, bredde og
pinning som flat), og hver gruppeoverskrift får en egen tri-state avkryssingsboks:
- Gruppens valgenhet er ALLE leaf-etterkommere på tvers av nestede grupper, uavhengig av
utfoldet tilstand — å krysse av en kollapset gruppe velger også radene som er skjult under den
(speiler tre-cascaden i 007d). Avledet tilstand: checked når alle leaf-rader er valgt,
indeterminate (
aria-checked="mixed") ved reelt delvis valg. - Klikk på gruppe-checkboxen velger alle gruppens leaf-rader når minst én er uvalgt, ellers
avvelges alle. Én
selectionStateChangeper handling uansett gruppestørrelse (ingenrowSelectionChange— det finnes ingen enkelt togglet rad). Checkbox-klikk kollapser aldri gruppen (egen celle, klikk propagerer ikke). - Modell-modus respekteres: i
allMatching-modus legger gruppe-avvalg til eksklusjoner og gruppe-valg fjerner dem — ingen tvungen materialisering til eksplisitte nøkler. - Header select-all teller som før hele det filtrerte leaf-universet (
filteredResult— alle sider/grupper), så header-, gruppe- og rad-tilstand er alltid konsistente. - Valget er nøkkelbasert, ikke visnings-basert: merking overlever gruppér/avgruppér/regruppér og kollaps/utvid uendret.
- Gruppert visning virtualiserer aldri rader (
isVirtual()er false ved gruppering), så hver leaf-checkbox er en reelt rendret celle. - Tastatur-radmerking (Mellomrom / Shift+Mellomrom / Ctrl+A) er aktiv også i gruppert modus: området følger de synlige leaf-radene i rendret rekkefølge og kan krysse gruppegrenser (i motsetning til celle-rangen i 010c, som klemmes til én gruppe).
Tre-modus: parent/child-cascade (H-SG-MO-007d). I tre-grids (treeData) får selection-kolonnen
hierarki-semantikk, mens selve selection-modellen forblir flat og serialiserbar (uendret
SmartGridSelectionState-skjema — gridet oversetter tre-intensjon til eksplisitte nøkler):
- Forelder-klikk cascader: velger/avvelger raden selv og hele under-treet (også kollapsede
etterkommere). Bladrader er vanlig én-nøkkel-toggle. Én
selectionStateChangeper brukerhandling uansett subtre-størrelse (+rowSelectionChangekun for klikket rad). - Avledet tri-state: en forelder er checked når raden selv og alle etterkommere er valgt, indeterminate ved reelt delvis valg. Tilstanden avledes alltid av nøklene — den lagres ikke.
- Header select-all: universet er alle tre-noder, uavhengig av utfoldet tilstand — kollapsede
etterkommere inngår. Valget settes som eksplisitte nøkler (aldri
allMatchingi tre-modus); en restoredallMatching-state materialiseres til eksplisitte nøkler (univers minus eksklusjoner) før første brukerhandling. - Restore cascader ALDRI:
[(selectionState)]-restore tolkes som eksakte eksplisitte nøkler. En visning med kun forelder-nøkkelen gir forelder som indeterminate og barna uvalgte — fremtidig lazy-load-arbeid må ikke omtolke gamle eksplisitte states som hierarkisk intensjon.
Legacy checkedColumn-flyten (over) er en separat opt-in og er urørt; den innebygde kolonnen
gjelder kun selectionMode="multiple".
Utvidbare detalj-rader (row expansion)¶
Opt-in: en rad kan utvides til å vise en full-bredde detalj-rad under seg (kommentarer, historikk, metadata). Default av → ingen detalj-rader. Detaljinnholdet defineres med appSmartGridRowDetail-direktivet (én per grid). Krever stabil rowKey (nøkkelbasert), og hosten trigger utvidelse (typisk en chevron i en celle).
| API | Type | Bruk |
|---|---|---|
expandableRows |
@Input() boolean |
Slå på. Default false. |
singleRowExpansion |
@Input() boolean |
Maks én utvidet rad om gangen. |
[(expandedRowKeys)] |
to-veis Array<string\|number> |
Kontroller/observer utvidet tilstand. |
toggleRowExpansion(row) / isRowExpanded(row) / collapseAllRows() |
metoder | Trigge/lese fra host. |
appSmartGridRowDetail |
strukturelt direktiv | <ng-template appSmartGridRowDetail let-row> — detaljinnholdet. |
<konti-smart-grid #grid [columns]="columns" [data]="rows" [rowKey]="rowKey" [expandableRows]="true">
<ng-template appSmartGridCell="__expand__" let-row>
<button class="btn btn-sm btn-link p-0" (click)="grid.toggleRowExpansion(row)">
<i class="bi" [ngClass]="grid.isRowExpanded(row) ? 'bi-chevron-down' : 'bi-chevron-right'"></i>
</button>
</ng-template>
<ng-template appSmartGridRowDetail let-row>
<div class="p-3">Historikk og detaljer for {{ row.name }}…</div>
</ng-template>
</konti-smart-grid>
Lazy-load: lytt på rowExpand (emittes når en rad utvides) og rowCollapse for å hente detalj-data ved behov, og vis loading/error i din egen appSmartGridRowDetail-template.
v1-begrensninger: kun flate (ikke-grupperte) desktop-tabeller; mobilkort får ikke detalj-rad. Ikke kombiner med virtualScroll — variabel detalj-høyde bryter vindu-matematikken (uniform rowHeight).
Master/detail (integrert detaljpanel)¶
Opt-in: et detaljpanel side-ved-side (right, master venstre / detalj høyre) eller under listen (bottom, vertikal splitt). Detaljen følger live når brukeren klikker eller piler opp/ned gjennom master-radene — som en e-postklient. Default detailPanel="none" → ingen endring. Krever stabil rowKey.
| API | Type | Bruk |
|---|---|---|
detailPanel |
@Input() 'none' \| 'right' \| 'bottom' |
Slå på det integrerte panelet + orientering (host-styrt). |
detailPanelWidth |
@Input() number |
Initial bredde i px for right (default 360). |
detailPanelBottomHeight |
@Input() number |
Initial høyde i px for bottom (default 240). |
[(activeDetailRowKey)] |
to-veis string\|number (activeDetailRowKey + activeDetailRowKeyChange) |
Aktiv rad (driver panel + highlight). |
rowDetailRequested |
@Output() T |
Emit ved aktivering (host kan lazy-loade, eller drive egen offcanvas). |
detailPanelPositionRestore |
@Output() 'right' \| 'bottom' |
Emit når en lagret visning eller verktøylinje-veksleren ber om en annen orientering — host setter detailPanel deretter (gridet muterer aldri inputen selv). |
detailViewModeChange |
@Output() 'split' \| 'master-only' \| 'detail-only' |
Emit når brukeren bytter visningsmodus via verktøylinjen. Valgfritt å konsumere — gridet eier og bruker modusen selv (detailViewMode); eventet er kun for host-persistering. |
enableDetailPanelToggle |
@Input() boolean |
Opt-in verktøylinje-kontroll ved Kolonner/Visninger: en 3-delt visningsvelger (Kun liste / Delt / Kun detaljer) pluss høyre/bunn-posisjonsvekslerne. Posisjonsbytte krever at hosten binder detailPanelPositionRestore → [detailPanel]; visningsmodus eies av gridet og trenger ingen host-binding. Vises kun når et detaljpanel faktisk er aktivt. |
enableFullscreen |
@Input() boolean |
Opt-in: viser en fullskjerm-knapp i verktøylinjen. Løfter hele gridet (toolbar + master/detail) til et fast overlegg over hele viewporten. Grid-eid (isFullscreen); Esc og knappen lukker; kombineres med visningsmodus (fullskjerm + Kun detaljer = ett objekt på hele skjermen). |
fullscreenChange |
@Output() boolean |
Emit når brukeren går inn/ut av fullskjerm. Valgfritt å konsumere — gridet eier og bruker tilstanden selv. |
appSmartGridDetailPanel |
direktiv | <ng-template appSmartGridDetailPanel let-row> — panel-innholdet. |
Justerbar splitt (H-SG-MO-004)¶
Skillelinjen mellom liste og detalj er dragbar (mus + touch via Pointer Events, med pointer-capture
så draget ikke etterlater document-listeners). Piltastene endrer størrelsen deterministisk når håndtaket
(role="separator") har fokus. Min/max sikrer at ingen av flatene kollapser. Splitt-forholdet lagres
som ratio (0..1) i view-state (detailPanelSize + detailPanelPosition), så en lagret visning
gjenoppretter samme layout; resetView() setter tilbake til initial-størrelsen. Størrelsen holdes
per akse (høyre-bredde og bunn-høyde separat), så et bytte av orientering gjenbruker aldri den andre
aksens px. Orienteringen er host-styrt (via detailPanel-inputen), men en lagret visning kan be host om
å bytte orientering gjennom detailPanelPositionRestore-eventet — gridet skriver aldri inputen selv.
Under md kollapses splitten: detaljen stables i full bredde under listen og resizeren skjules. I
bottom-modus er tabell-viewporten høyde-bundet, og rad-virtualiseringen oppdateres etter resize.
Visningsmodus — Kun liste / Delt / Kun detaljer¶
Når enableDetailPanelToggle er på, får verktøylinjen en 3-delt visningsvelger ved siden av
høyre/bunn-posisjonsknappene. Modusen (detailViewMode) er en enkelt gjensidig-utelukkende tilstand:
- Delt (
'split', standard) — både liste og detalj vises (den vanlige splitten). - Kun liste (
'master-only') — detaljpanelet skjules → listen får full bredde. Nyttig når du skanner mange rader eller vil se mange kolonner uten detaljene. - Kun detaljer (
'detail-only') — listen skjules → detaljpanelet maksimeres. Nyttig på små skjermer der du vil bruke hele flaten på ett objekt.
Til forskjell fra posisjonsbytte (host-styrt via detailPanel) eier gridet denne modusen selv,
så den virker uten host-binding; detailViewModeChange emittes for host som vil persistere den.
Modusen lagres i view-state (detailViewMode), så en lagret visning husker den. Posisjonsknappene
(høyre/bunn) er kun aktive i Delt-modus — de gråes ut i Kun liste / Kun detaljer (ingenting å plassere).
Internt speiles dette av tre predikater: isDetailPanelOpen() (kun 'split' — driver resizer,
tastaturnavigasjon og splitt-ratio), isDetailPanelVisible() ('split' + 'detail-only' — panelet
rendres) og isMasterHidden() ('detail-only' — master-griden fjernes fra DOM).
Fullskjerm (enableFullscreen)¶
Med [enableFullscreen]="true" får verktøylinjen en fullskjerm-knapp. Klikk løfter hele gridet
(toolbar + master/detail) til et fast overlegg (position: fixed; inset: 0) over hele viewporten,
slik at små skjermer kan bruke hele flaten. Ut igjen via samme knapp eller Esc; body-scroll låses
mens fullskjerm er aktiv. Det er et rent CSS-overlegg (ikke den native Fullscreen-API-en) fordi iOS
Safari — nettopp «de med mindre skjerm» — ikke støtter requestFullscreen() på vanlige elementer.
Fullskjerm kombineres med visningsmodus: fullskjerm + Kun detaljer gir ett objekt på hele
telefonskjermen; fullskjerm + Kun liste gir hele lista. Tilstanden er grid-eid (isFullscreen);
fullscreenChange emittes for host som vil persistere den. I fullskjerm bruker de høyde-bundne
flatene 100dvh (tar høyde for mobilnettleserens dynamiske adressefelt).
<konti-smart-grid detailPanel="right" [detailPanelWidth]="420" [rowKey]="rowKey"
[columns]="columns" [data]="rows" (rowDetailRequested)="loadDetail($event)">
<ng-template appSmartGridDetailPanel let-row>
<div class="p-3"><h5>{{ row.name }}</h5> …detaljer for valgt rad…</div>
</ng-template>
</konti-smart-grid>
Pil opp/ned flytter aktiv master-rad (og scroller den i view); panelet oppdateres samtidig. Klikk på en rad aktiverer den. v1: flate desktop-tabeller. (Foretrekker du en host-styrt skuff i stedet, lytt på rowDetailRequested og åpne din egen NgbOffcanvas.)
Betinget styling (rowClassRules / cellClassRules)¶
Sett CSS-klasser dynamisk per rad/celle ut fra predikater — for å markere avvik, låste linjer, status osv. Additivt oppå rowClass/className.
rowClassRules = { 'table-danger': (r: Row) => r.overdue, 'fw-bold': (r: Row) => r.flagged };
columns = [{ field: 'price', title: 'Pris', cellClassRules: { 'text-danger': (r: Row) => r.price < 0 } }];
Hold predikatene billige — de kjøres per synlig rad/celle.
Hierarkiske rader (tree data)¶
Opt-in: vis rader hierarkisk med innrykk og expand/collapse på en valgt kolonne. Krever stabil rowKey. Host gir enten getChildren (hierarkisk data) eller getParentKey (flat liste med foreldre-referanse).
| API | Type | Bruk |
|---|---|---|
treeData |
@Input() boolean |
Slå på tre-modus. |
getChildren |
@Input() (row) => row[] |
Hierarkisk: returner barn (data = røtter). |
getParentKey |
@Input() (row) => key\|null |
Flat: returner foreldre-nøkkel (røtter = null). |
treeColumn |
@Input() string |
Felt som viser innrykk + toggle (default første synlige kolonne). |
treeIndent |
@Input() number |
Bredde per nivå-guide (px, default 18). Dybde vises som synlige nivå-linjer + barnekobling — ikke padding, så hierarkiet er lesbart også i høyrejusterte tre-kolonner (f.eks. linjenummer). |
[(treeExpandedKeys)] |
to-veis (string\|number)[] (treeExpandedKeys + treeExpandedKeysChange) |
AKTIVT utvidede noder (passiv restored intent for ulastede lazy-noder er kun i view-state — se lazy-seksjonen). |
loadChildren |
@Input() (row) => Promise<T[]> \| Observable<T[]> |
Async barn-henting ved første utfoldelse (lazy, hierarkisk modus). |
treeHasChildrenHint |
@Input() (row) => boolean |
Om en node har barn når de ikke er lastet ennå (driver togglen). |
treeChildrenLoaded |
@Output() SmartGridTreeChildrenLoadedEvent |
Barn lastet, cachet og utfoldet. |
treeChildrenLoadError |
@Output() SmartGridTreeChildrenLoadErrorEvent |
Lasting feilet — host viser toast; neste klikk prøver igjen. |
<konti-smart-grid [treeData]="true" [getParentKey]="getParentKey" treeColumn="name"
[rowKey]="rowKey" [columns]="columns" [data]="rows">
</konti-smart-grid>
v1-begrensninger: tre-modus er en egen render-pipeline — den bypasser filterraden, sortering, gruppering, paging og virtualisering. Host styrer rekkefølge via dataene. Flate desktop-tabeller.
Lazy-lasting av barn (H-SG-MO-007e)¶
Opt-in for store trær som ikke kan lastes komplett i klienten: sett loadChildren (Promise eller Observable) og treeHasChildrenHint. Første utfoldelse av en node med ukjente barn viser spinner på togglen (aria-busy, dobbel-klikk-guard), henter barna, cacher dem (kollaps/utfold re-fetcher aldri) og utfolder. Feil utfolder ikke — gridet viser ingen egen melding; hosten lytter på treeChildrenLoadError (toast), og neste klikk prøver igjen. Kun hierarkisk modus (getParentKey-lister er fullastet per definisjon — loadChildren ignoreres der med warning).
Kontrakt for getChildren sammen med lazy: undefined/null = «ukjent/ikke lastet» (lazy kan kjøre); eksplisitt [] = «lastet blad» (lazy kjører ALDRI — vinner over hintet; en children ?? []-accessor undertrykker altså lazy-lasting og gir dev-warning når hintet samtidig sier true). Lastede batcher valideres: barn med forelderens nøkkel, duplikater og allerede kjente nøkler droppes med warning.
Robusthet: in-flight kall etter at hosten har byttet data/rowKey/accessors/loadChildren ignoreres (generasjonsguard) — stale svar skriver aldri inn i feil datasett, og feil for noder som er borte emittes ikke.
Restore-semantikk (samspill med lagrede visninger): restored expanded keys for ulastede lazy-noder er passiv intent — chevronen viser kollapset (den lyver aldri), treeExpandedKeys/emits inneholder kun aktivt utvidede noder, og et klikk laster-så-utfolder. Intenten bevares i getViewState() (union), så en lagret visning husker den. resetView() nullstiller både aktiv og passiv tilstand.
Selection-samspill (007d): lastes barn inn under en valgt forelder, materialiseres hele det nykjente under-treet (inkl. forhåndslastede nested barn i batchen) inn i den eksplisitte selection-en — UI-et viser aldri uavkryssede barn under en avkrysset forelder. Uvalgt forelder → barn lastes uavkrysset. (Manuell smoke ved lazy-host-adopsjon: velg forelder → utfold → barna er avkrysset; også når loader-batchen inneholder forhåndslastede nested barn.)
Dekning i øvrige features: rollups (007c), cascade (007d) og klient-eksport (007b) opererer over det lastede under-treet. Fullstendig eksport av delvis lastede trær er server-eksportens jobb (006-kontrakten). Anbefalt mønster for rollups med lazy: serveren leverer ferdig-aggregerte verdier på forelder-radene — egne verdier vinner alltid over klient-rollup i cellevisningen.
Utvidet tilstand i lagrede visninger (H-SG-MO-007a)¶
For tre-grids (treeData + rowKey) inngår utvidede noder nå i view-state (SmartGridViewState.treeExpandedKeys, schema v3): getViewState() fanger settet (også tomt — «alt kollapset» er reell tilstand som round-tripper), applyViewState() gjenoppretter det, og resetView() kollapser tilbake til default. Felt-tilstedeværelse er intensjon ved restore:
treeExpandedKeys i lagret visning |
Effekt |
|---|---|
| Mangler (v1/v2-visning) | No-op — gjeldende utvidelse bevares |
[] (eksplisitt tomt) |
Kollaps alt + emit [] |
[nøkler…] |
Gjenopprett disse — nøkler for ulastede lazy-noder rutes til passiv intent (007e) og emittes IKKE; kun den aktive delen emittes, og bare ved reell endring |
Ikke-tre-grids får aldri feltet — deres lagrede visninger er byte-identiske med v2. treeExpandedKeysChange bærer alltid kun aktiv ekspansjon: restore emitter den routede aktive staten ved reell endring, reset emitter [] når aktiv eller passiv tilstand fantes. I sync-trær (uten loadChildren) er alle nøkler aktive, så oppførselen er som før.
Tre-eksport (H-SG-MO-007b)¶
Klient-eksporten (både «Uten formattering» og «Med formattering») respekterer exportTreeOptions for tre-grids:
| Valg | Effekt i klient-eksporten |
|---|---|
visibleOnly: true + includeCollapsedChildren: false (default) |
Kun ekspanderte/synlige noder — som før |
includeCollapsedChildren: true eller visibleOnly: false |
Hele treet, i tre-rekkefølge (også barn under kollapsede noder) |
includeHierarchyPath: true |
Ledende eksport-kun-kolonne «Hierarki» med foreldrekjeden (rot først, >-separert; rot-rader har tom sti) |
Sti-verdien er tre-kolonnens eksport-verdi (treeColumn når satt, ellers første synlige kolonne; fallback er radnøkkelen). Sti-kolonnen finnes kun i eksporten — aldri i grid-kolonnene eller server-requesten. exportDataProvider-rader tar fortsatt presedens og eksporteres flatt; server-side eksport (serverSideExport) bærer valgene i requesten og lar domene-endepunktet håndtere dem.
Rullede aggregater per node (H-SG-MO-007c)¶
I tre-modus ruller groupAggregates opp per forelder-node: hvert aggregat beregnes over nodens hele under-tre (alle etterkommere, uavhengig av hva som er utfoldet). Beregningen deler implementasjon med gruppe-modus (calcAgg), så aggregateFilters (per felt + aggregat-type), NaN-håndtering og avg-semantikk er identiske — samme konfig gir samme tall i begge moduser.
Semantikk:
- En nodes egen verdi teller ikke i nodens eget rollup («summen under meg»), men teller som barn-verdi i alle forfedres rollups — mellomliggende foreldre mistes aldri.
- Bladnoder har ingen rollup.
- Rollup er uavhengig av expanded/kollapset tilstand.
Visning (default-cellerendring): i en aggregat-konfigurert kolonne viser forelder-celler den rullede verdien dempet/kursiv (.sg-tree-agg) — kun når radens egen råverdi er tom (null/undefined/''). Egen verdi vinner alltid ved kollisjon. Kolonner med custom cell-template er helt urørt — template-forfatteren har full kontroll. Første konfigurerte aggregat-type for feltet vises (rekkefølgen i groupAggregates styrer).
Host-API (v1-kontrakt: @ViewChild): for status-rendering eller custom visning leses rollups via komponent-referansen:
@ViewChild('grid') grid!: SmartGridComponent<Row>;
// AggMap-entry for noden: { sum?, count?, avg?, min?, max? } — undefined for blad/ukonfigurert felt
const agg = this.grid.treeAggregate(row, 'quantity');
Domene-status («alle barn Klar» o.l.) er hostens ansvar — gridet leverer tallene. En template-context-verdi for rollups er en mulig senere utvidelse.
v1-avgrensninger: totalrad (showTotalRow) rendres ikke i tre-modus (footeren er gated av !treeData, tilstanden tømmes eksplisitt). Eksporten viser råverdier — rollups inngår ikke i eksport i v1.
Rad-reordering (drag & drop)¶
Opt-in: la brukeren dra rader for å endre rekkefølge. Krever stabil rowKey. Gridet emitter kun intensjon — host (og helst backend) beregner og lagrer den endelige sorteringsnøkkelen.
| API | Type | Bruk |
|---|---|---|
rowReorder |
@Input() boolean |
Slå på drag-håndtak + drop. |
canDropRow |
@Input() (event) => boolean |
Valgfri guard; returner false for å avvise et slipp. |
rowReorderChange |
@Output() SmartGridRowReorderEvent |
Emittes ved slipp: { row, rowKey, targetRowKey, position: 'before'\|'after'\|'inside', targetParentKey? }. |
<konti-smart-grid [rowReorder]="true" [rowKey]="rowKey" [columns]="columns" [data]="rows"
(rowReorderChange)="onReorder($event)">
</konti-smart-grid>
onReorder(e: SmartGridRowReorderEvent<MyRow>): void {
// e.position er 'before' | 'after' | 'inside' relativt til e.targetRowKey — flytt + persistér sortnøkkel
this.api.reorder(e.rowKey, e.targetRowKey, e.position).subscribe(() => this.reload());
}
Et grip-håndtak (bi-grip-vertical) vises i første kolonne. Drop-indikatoren viser en linje over/under målraden. Krever stabil rowKey — uten den vises ikke håndtaket. Gridet muterer aldri data selv — hosten eier persistering og re-binding.
Tre-modus: sibling og reparent (H-SG-MO-007f)¶
I tre-grids (treeData) har rad-DnD hierarki-semantikk med tre drop-soner per målrad (25/50/25):
| Sone | position |
targetParentKey |
Betydning |
|---|---|---|---|
| Øvre 25 % | before |
Målradens forelder (null for rot-rad) |
Sibling-flytting: sett inn før målraden, under dens forelder |
| Midtre 50 % | inside |
Målradens egen nøkkel | Reparent: målraden blir ny forelder (helrads-ramme som affordance) |
| Nedre 25 % | after |
Målradens forelder (null for rot-rad) |
Sibling-flytting: sett inn etter målraden |
Flat modus beholder 50/50 before/after uten targetParentKey.
- Barneorden ved
insideer host-definert: gridet bestemmer aldri hvor blant målradens barn den flyttede raden lander (append sist, server-default, sortnøkkel-logikk — hostens valg). - «Gjør til rot»: before/after på en rot-rad emitter
targetParentKey: null— ingen egen rot-affordance. - Cross-level: before/after på tvers av nivåer er tillatt (full sibling-intensjon under målets forelder); domene-hosts strammer via
canDropRow. - Hard cycle-guard: slipp på raden selv eller i dens eget under-tre avvises uten affordance og uten emit — på både dragover og drop, uavhengig av
canDropRow. Drop emitter kun fra siste aksepterte dragover-mål. - Lazy-samspill (007e): en ulastet lazy-forelder er gyldig
inside-mål — intensjonen trenger ikke lastede barn; gridet auto-laster/utfolder ikke ved drop (hostens re-binding avgjør).
Tastaturnavigasjon og tilgjengelighet¶
Opt-in: en roving-tabindex fokusmodell med ARIA grid-roller, slik at brukeren kan navigere cellene med tastaturet. Default av — eksisterende grids beholder native tabell-semantikk uendret. Krever stabil rowKey.
| API | Type | Bruk |
|---|---|---|
keyboardNavigation |
@Input() boolean |
Slå på roving tabindex + ARIA grid-modell. |
gridAriaLabel |
@Input() string? |
Tilgjengelig navn på tabellen (visuelt skjult <caption>). |
<konti-smart-grid [keyboardNavigation]="true" [rowKey]="rowKey" [columns]="columns" [data]="rows">
</konti-smart-grid>
Tastatur: piltaster flytter celle-fokus, Home/End til rad-start/slutt, Ctrl+Home/End til grid-hjørner, PageUp/PageDown ±10 rader, Enter aktiverer (rediger > tre-toggle > rad-utvidelse > detalj > rowClick), Escape avbryter redigering.
Enter og nestede kontroller (BRQ-18 T3a): siste trinn i Enter-stigen emitter rowClick, slik at en
side der radklikk er hovedhandlingen også kan betjenes fra tastaturet. Cellen eier Enter kun når
cellen selv har fokus — keydown er bundet på <td>, så et tastetrykk fra en fokuserbar kontroll
inne i en appSmartGridCell-mal bobler opp hit, og da lar gridet standardhandlingen være i fred.
Knappen din trenger derfor ingen egen keydown-håndtering: (click)="$event.stopPropagation()" er
fortsatt alt som kreves for muspekeren. Merk at dette samtidig retter en eldre feil — før denne
endringen avlyste gridet knappens genererte klikk uten å gjøre noe i stedet, så Enter på en knapp i
en celle gjorde ingenting.
Tastatur-radmerking (når selectionMode='multiple' + keyboardNavigation + rowKey): Mellomrom merker/avmerker raden i fokus og setter et anker, Shift+Mellomrom merker et sammenhengende radområde fra ankeret til fokusraden (beholder allerede merkede rader i området), og Ctrl/Cmd+A merker alle rader i utvalget (selectAllInScope). Ankeret overlever vanlig (ikke-Shift) pilnavigasjon og nullstilles først når merkingen tømmes (clearSelection). Virker også i gruppert modus (H-SG-GRPSEL): området følger de synlige leaf-radene i rendret rekkefølge og kan krysse gruppegrenser.
ARIA: role="grid"/row/gridcell/columnheader og aria-rowcount/aria-colcount settes i tastaturmodus. I selectionMode='multiple' får gridet aria-multiselectable og hver rad aria-selected (merket tilstand). Under radvirtualisering får hver rendret datarad aria-rowindex = global radposisjon (virtualStart + lokal indeks + 1), så skjermlesere annonserer riktig posisjon selv om bare et vindu av radene er i DOM. aria-sort på sorterte kolonner er alltid på (trygt tillegg, ingen interaksjonsendring).
Virtualisering: tastaturnavigasjon virker med både rad- (virtualScroll) og kolonnevirtualisering (virtualColumns) — flytter du fokus til en rad eller kolonne utenfor vinduet, ruller griden den inn i vinduet (nærmeste kant) før fokus settes (horisontalt for kolonner, vertikalt for rader; pinnede kolonner er alltid montert). Grupperte grid støttes også (navigasjon over synlige leaf-rader). Under virtualisering får hver rendret celle aria-rowindex/aria-colindex = global posisjon, så skjermlesere annonserer riktig selv når bare et vindu er i DOM. Full tastatur-basert range-selection er senere hardening.
Pinnede topp-/bunn-rader¶
Opt-in: rader levert av host som rendres utenfor den vanlige (sorterbare/filtrerbare/paginerte) radlisten — typisk totaler, status, advarsler eller en oppsummering. De er aldri en del av data, så de ekskluderes automatisk fra sortering, filter, selection og virtualisering.
| API | Type | Bruk |
|---|---|---|
pinnedTopRows |
@Input() T[] |
Rader klistret øverst (under headeren). |
pinnedBottomRows |
@Input() T[] |
Rader klistret nederst i scroll-viewporten. |
appSmartGridPinnedRow |
direktiv (valgfri) | Host-template som emitter radens <td>-celler (full kontroll + colspan). |
<konti-smart-grid [pinnedBottomRows]="[totalRow]" [columns]="columns" [data]="rows">
<ng-template appSmartGridPinnedRow let-row let-columns="columns">
<td [attr.colspan]="columns.length">Total: {{ row.sum }}</td>
</ng-template>
</konti-smart-grid>
Uten appSmartGridPinnedRow rendres pinned-raden kolonne-justert med formatValue per synlig kolonne (egnet for en totalrad). Flere rader stables korrekt: hver pinned rads sticky-offset måles fra de faktiske radhøydene, så N topp-rader legger seg etter hverandre under headeren og N bunn-rader stables oppover fra viewport-bunnen (ingen overlapp). Én rad på hver side oppfører seg som før (topp ved header-høyden, bunn ved 0).
Empty / error / loading overlay-slots¶
Opt-in templates som overstyrer gridets standard-tilstander. Uten dem rendres dagens default-spinner og no-records-tekst uendret.
| API | Type | Bruk |
|---|---|---|
appSmartGridLoading |
direktiv | Overstyrer default-spinner mens loader er sann. |
appSmartGridEmpty |
direktiv | Rik tomtilstand (forklaring/CTA) når det er null rader. |
appSmartGridError |
direktiv | Feiltilstand med retry/CTA når errorState er sann. |
errorState |
@Input() boolean |
Slår på feil-overlay (kun når appSmartGridError finnes). |
<konti-smart-grid [errorState]="loadFailed" [loader]="loading" [data]="rows" [columns]="columns" [noRecordsFoundPanel]="true">
<ng-template appSmartGridLoading>Laster…</ng-template>
<ng-template appSmartGridEmpty>Ingen data — <a (click)="add()">legg til</a></ng-template>
<ng-template appSmartGridError><button (click)="retry()">Prøv igjen</button></ng-template>
</konti-smart-grid>
Tilstandene er gjensidig tydelige: feil-overlay vises kun når ikke laster, og tom-tilstand kun når ikke feil.
Uendelig scroll (server-viewport)¶
Opt-in påbygg til rad-virtualisering: gridet sier ifra når vinduet nærmer seg slutten av radene hosten har gitt det, og hosten henter neste serverside og legger den til. Gridet henter aldri data selv og vet ikke hvor mange rader serveren har. Default av — grid som ikke slår den på er urørt.
| API | Type | Bruk |
|---|---|---|
infiniteScroll |
@Input() boolean |
Slå på server-viewport-kontrakten. |
infiniteScrollThreshold |
@Input() number |
Rader fra slutten før neste side etterspørres (default 10). |
mobileRowCap |
@Input() number |
Tak på antall kort i mobil-layoutet (0 = av). Bruk mobileCapReached() til å vise hvorfor lista stopper, og mobileFetchCapReached() til å hindre henting. |
mobileCapReached() |
public metode |
Klassifisering, brukervendt. Sant kun når kortlista faktisk er avkortet: bufferet er vokst forbi taket (desktop-økt som roteres til telefon), eller det er cap-stort og serveren har mer (moreRowsOnServer, med hasMoreRows som fallback). Et uttømt resultat på nøyaktig mobileRowCap rader er komplett og gir false — hosten skal da si «alt er lastet», ikke at søket må avgrenses. |
mobileFetchCapReached() |
public metode |
Hard hentegrense, ren geometri. Sant når mobil-layoutet holder minst mobileRowCap rader — uavhengig av om en henting er underveis eller har feilet. Enhver host-kontroll som kan starte en henting (typisk en «Prøv igjen»-knapp) må sperre på denne, ikke på mobileCapReached(): ved nøyaktig taket spør klassifiseringen om serveren har mer, og et feilet appende er nettopp der det svaret ellers går tapt. |
hasMoreRows |
@Input() boolean |
Host-eid port: finnes det mer, og kan hosten hente det nå? |
moreRowsOnServer |
@Input() boolean |
Host-eid faktum: har serveren mer, uavhengig av henting/feil? Driver mobileCapReached(). |
moreRowsRequested |
@Output() SmartGridMoreRowsRequest |
{ loadedRows, requestId } — be om neste side. |
acknowledgeMoreRows(requestId) |
public metode |
Kvittering: den identifiserte forespørselen er avsluttet. |
<konti-smart-grid
[data]="orders"
[columns]="columns"
[rowKey]="rowKey"
[virtualScroll]="true"
[rowHeight]="40"
[singleLineCells]="true"
[showPagination]="false"
[serverSidePagination]="true"
[totalRows]="totalRows"
[infiniteScroll]="true"
[hasMoreRows]="hasMoreRows"
(moreRowsRequested)="onMoreRowsRequested($event)">
</konti-smart-grid>
Kontrakten, presist:
- Hver emittert
requestIdskal kvitteres nøyaktig én gang — på hver eneste terminal-sti: suksess, feil, kansellering, uttømt sett, eller synkron avvisning. Gridet emitterer ikke igjen før det har fått kvitteringen. Kvitteringen er id-sjekket, så en sen kvittering fra en kansellert forespørsel kan ikke låse opp en nyere, aktiv. hasMoreRowsbetyr «ikke be om mer nå» når den erfalse— enten fordi settet er uttømt, eller fordi hosten er opptatt eller pauset etter en feil. Gridet skiller ikke mellom disse. Bind den til et felt, aldri en getter (trap #46).- En ny
data-referanse frigjør ikke en pending forespørsel. En immutabel append er selv en ny referanse, så den regelen ville re-armet gridet før hosten var ferdig. rowKeyer påkrevd (nytt krav sammenlignet medvirtualScrollalene): uten stabil nøkkel kan verken dedupe over en append eller seleksjon fungere. Mangler den, logges énconsole.warnog ingenting emitteres.serverSidePagination+showPagination: falsebevarer hostenstotalRowssom serverens totalsum — i alle andre moduser ertotalRowsfortsatt radantallet i bufferet.- Gridet emitterer ikke mens
loaderertrue(en full reset er i gang). - Emit skjer alltid én mikrotask etter at vinduet ble beregnet. Vindusberegningen kjører også fra
performRecompute(), altså inne i en change-detection-syklus, og hosten svarer med å skrivehasMoreRows— en input på det samme gridet. Synkron emit der er NG0100. Request-id-en claimes synkront, så porten er lukket i mellomtiden. - Mobilkort rendres i sin helhet når
infiniteScroller på. Kortlista deler tabellens scroll-container, men har ingen spacer-elementer og ingen fast høyde — en vinduert kortliste gir da en scroll-utstrekning som bare representerer det rendrede utsnittet, og det akkumulerte settet blir uråelig på mobil. UteninfiniteScroller kortlista fortsatt vinduert (H-SG-006s frys-vern). Variabel-høyde-virtualisering av kort er utenfor kontrakten.
Host-ansvar: offset skal følge rader serveren har levert, ikke rader hosten beholdt etter
dedupe. Feil på en append bør være en pause (egen «Prøv igjen»-flate) og ikke automatisk
gjenopptas, ellers sender neste scroll en ny forespørsel brukeren ikke ba om. En egen
appendLoading-flate hører hjemme utenfor gridet — [loader] rendrer et fullgrid-overlegg over
radene brukeren nettopp scrollet til. Referansebruk: masterordre-listen
(docs/manual/moduler/masterordre/index.md).
Konsistens: offset-paging over et sett som endres samtidig kan hoppe over en rad eller la en slettet rad bli stående i det akkumulerte settet. Dedupe fjerner gjentakelser, ikke den risikoen — refresh / nytt søk bygger settet fra side 1 og er veien til et korrekt sett.
Kolonne-virtualisering¶
Opt-in horisontal windowing for brede grids: bare kolonnene i viewporten (+ buffer) ligger i DOM, med venstre/høyre spacer-celler som bevarer layout og scroll. Default av.
| API | Type | Bruk |
|---|---|---|
virtualColumns |
@Input() boolean |
Slå på kolonne-windowing. |
columnBuffer |
@Input() number |
Antall ekstra kolonner på hver side (default 2). |
<konti-smart-grid [virtualColumns]="true" [columns]="manyColumns" [data]="rows"></konti-smart-grid>
v1-krav og -grenser:
- Kolonnene må ha eksplisitte bredder.
autoSizeColumnsignoreres mensvirtualColumnser på. - Pinned-kolonner (
pinned: 'left' | 'right') rendres alltid (virtualiseres aldri bort). - Flat desktop-kropp: av i mobil og gruppert modus.
keyboardNavigationvirker sammen medvirtualColumns: flytter fokus til en kolonne utenfor vinduet, ruller griden kolonnen inn (nærmeste kant, horisontalscrollLeft) før fokus settes — se «Tastaturnavigasjon og tilgjengelighet».- Drag-reorder/resize virker på kolonner som er montert; en kolonne scrollet ut av view må scrolles inn først. Header-meny / kolonne-konfigurasjon virker på hele modellen uansett.
Kolonnevelger («Tilpass kolonner»)¶
Med allowColumnConfig får gridet en kolonnevelger der brukeren styrer hvilke kolonner som vises, rekkefølge og festing:
- Søk — filtrer kolonnelisten på navn (nyttig i brede grids). Mens søk er aktivt skjules flytte-pilene (rekkefølge gjelder hele listen).
- Merk alle / Fjern alle — vis eller skjul alle kolonner på én gang. «Fjern alle» beholder minst én synlig kolonne.
- Fest kolonne — fest-ikonet per kolonne veksler venstre → høyre → av (sticky kolonne,
pinned). Festede kolonner blir liggende fast ved horisontal scroll. - Flytt opp/ned og Tilbakestill som før.
Synlighet, rekkefølge, bredde og festing lagres i kolonneoppsettet (per gridId), og inngår også i lagrede visninger.
Kolonnemeny (tre-prikk header-meny)¶
Opt-in tre-prikk-meny i kolonne-headeren som samler eksisterende kolonne-operasjoner (sortér/filtrér/fest/tilpass bredde/skjul/administrer) ett sted. Introduserer ingen ny funksjonalitet — hvert menyvalg ruter til en operasjon gridet allerede støtter. Default av; eksisterende grids er byte-for-byte uendret inntil den slås på.
| API | Type | Bruk |
|---|---|---|
showColumnMenu |
@Input() boolean |
Slår på tre-prikk-menyen i alle kolonne-headere. |
GridColumn.menuDisabled |
boolean |
Skjuler menyen for én enkelt kolonne, selv når showColumnMenu er på. |
<konti-smart-grid [showColumnMenu]="true" [columns]="columns" [data]="rows"></konti-smart-grid>
Menyen vises kun for vanlige datakolonner (aldri for spacer-, valg- eller handlingskolonner) og kun når minst én seksjon er aktuell for kolonnen. Seksjoner vises betinget:
- Sortér (stigende/synkende/fjern) — når
col.sortableer sann og gridet ikke er i tre-modus. - Filtrér (åpne filterraden fokusert på kolonnen, eller fjern aktivt kolonnefilter) — når
col.filterableogshowFilterPaneler sanne. - Fest (venstre/høyre/av,
pinned) — nårallowColumnConfigellerallowColumnReorderer på. - Tilpass bredde (kjør autostørrelse for denne ene kolonnen, virker selv om
autoSizeColumnsglobalt er av) — skjules mensvirtualColumnser på. - Skjul kolonne / Administrer kolonner… — når
allowColumnConfiger på; «Administrer kolonner…» åpner det eksisterende kolonnevelger-panelet (se over).
Tre-prikk-knappen stopper klikk-propagering og er ikke en del av drag-and-drop for kolonne-reorder.
Custom cell templates¶
Bruk appSmartGridCell når cellen trenger badges, ikoner, knapper, lenker eller annen HTML.
Lenkefarge: en naken <a> i en celle rendres mørk, ikke i portalens gull-lenkefarge — gridet
setter --bs-link-color-rgb/--bs-link-hover-color-rgb på seg selv, så fargen arves ned i
celle-templatene. Du trenger altså ikke class="text-dark" på klikkbar tekst. Elementer som setter
sin egen farge (.btn, .badge, .page-link, .text-*) beholder den.
<ng-template appSmartGridCell="approvalStatus" let-row>
<span class="badge" [ngClass]="row.approved ? 'badge-soft-success' : 'badge-soft-warning'">
{{ (row.approved ? 'Common.Approved' : 'Common.Pending') | translate }}
</span>
</ng-template>
Viktig: GridColumn.format returnerer tekst/verdi som rendres via Angular interpolation. Returnerer du <span>...</span> fra format, vises HTML-en som tekst. For HTML i celler skal ng-template appSmartGridCell brukes.
Action-kolonner¶
For radhandlinger bruker vi en egen action-kolonne og en action-kapsel i template. Ikke legg handlingsknapper inn i format.
{
field: '__actions__',
title: '',
width: 112,
resizable: false,
sortable: false,
filterable: false,
mobileRole: 'actions',
}
<ng-template appSmartGridCell="__actions__" let-row>
<div class="action-capsule btn-group btn-group-sm sg-desktop-only" (click)="$event.stopPropagation()">
<button type="button" class="btn btn-outline-secondary" [title]="'Common.Edit' | translate" (click)="edit(row)">
<i class="bi bi-pencil" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-outline-danger action-danger" [title]="'Common.Delete' | translate" (click)="delete(row)">
<i class="bi bi-trash3" aria-hidden="true"></i>
</button>
</div>
</ng-template>
Retningslinjer:
- Bruk
btn-outline-secondaryfor ikke-destruktive handlinger. - Bruk
btn-outline-danger action-dangerogbi-trash3for sletting. - Stopp click propagation slik at knappen ikke også trigger
rowClick. - Sett action-kolonnen til fast bredde og
resizable: false.
Custom toolbar¶
Legg egne knapper i toolbaren med attributtet smart-grid-toolbar. Slottet rendres i høyre del av gruppepanelets toolbar når showGroupPanel er true.
<konti-smart-grid
[data]="rows"
[columns]="columns"
[showGroupPanel]="true"
[enableExport]="true">
<div smart-grid-toolbar class="d-flex gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="reload()">
<i class="bi bi-arrow-clockwise" aria-hidden="true"></i>
{{ 'Common.Refresh' | translate }}
</button>
<button type="button" class="btn btn-sm btn-primary" (click)="createCustomer()">
<i class="bi bi-plus-lg" aria-hidden="true"></i>
{{ 'Common.New' | translate }}
</button>
</div>
</konti-smart-grid>
Hvis showGroupPanel er false, viser gridet en enklere standalone-toolbar for søk, eksport, reset og kolonnevalg. Denne standalone-toolbaren har samme smart-grid-toolbar-slot som gruppepanelets toolbar — de to toolbarene er gjensidig utelukkende (styrt av showGroupPanel), så projisert innhold rendres i den som faktisk er aktiv.
Radstyling¶
Bruk rowClass når radens status skal påvirke visuell stil. Funksjonen kan returnere string, string-array eller objekt med klassetoggles.
rowClass = (row: CustomerRow) => ({
'table-warning': row.status === 'Inactive',
'fw-semibold': row.id === this.highlightedCustomerId,
});
<konti-smart-grid
[data]="rows"
[columns]="columns"
[rowKey]="rowKey"
[rowClass]="rowClass">
</konti-smart-grid>
Hold rowClass billig. Den kan evalueres mange ganger ved filtrering, sortering og change detection.
Inline redigering¶
Sett editable: true på kolonnen. Brukeren dobbeltklikker cellen, redigerer verdi og lagrer. Gridet emitter cellEdit; parent må selv oppdatere backend og radlisten.
columns: GridColumn<ProductRow>[] = [
{
field: 'quantity',
titleKey: 'Common.Quantity',
editable: true,
editType: 'number',
editMin: 0,
editValidator: (value) => value >= 0 || 'Quantity must be zero or higher',
},
];
onCellEdit(event: CellEditEvent<ProductRow>): void {
const { row, field, newValue } = event;
this.saveQuantity(row.id, String(field), newValue);
}
For custom edit UI brukes appSmartGridEdit:
<ng-template appSmartGridEdit="category" let-row let-value="value" let-save="save" let-cancel="cancel">
<select class="form-select form-select-sm" [ngModel]="value" #category>
<option *ngFor="let option of categoryOptions" [ngValue]="option.value">{{ option.label }}</option>
</select>
<button type="button" class="btn btn-sm btn-success" (click)="save(category.value)">
<i class="bi bi-check-lg" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-sm btn-secondary" (click)="cancel()">
<i class="bi bi-x-lg" aria-hidden="true"></i>
</button>
</ng-template>
Viktig oppførsel:
| Tema | Detalj |
|---|---|
| Start | Brukeren dobbeltklikker en editable celle. |
| Standard editor | text, number, date og select rendres av gridet. |
| Lookup editor | editType: 'lookup' + lookupSearch-callback — se «Lookup-editor» under. |
| Custom editor | Krever editType: 'custom' og matching appSmartGridEdit="field". |
| Validering | editValidator må returnere true eller en feilmelding. |
| Tall | editType: 'number' parser string til number før event. |
| Lagring | Gridet emitter cellEdit, men muterer ikke radobjektet. Parent må lagre og oppdatere rows. |
| Avbryt | Escape eller cancel() i custom template lukker editor uten event. |
Excel-clipboard (enableClipboard)¶
| API | Default | Beskrivelse |
|---|---|---|
enableClipboard |
false |
Kopier/lim inn mot Excel. Krever keyboardNavigation (fokusert celle er anker). |
enableClipboardPaste |
true |
Sett false for kopier-kun: Ctrl+C virker, Ctrl+V (og dermed Ctrl+Z) er inert. Paste er uansett inert i grids uten redigerbare kolonner. |
enableCellRangeSelection |
false |
Excel-lik område-markering: dra med mus, Shift+klikk eller Shift+piltaster markerer et rektangel; Ctrl+C kopierer nøyaktig området. Krever enableClipboard + stabil rowKey. Virker i både flat og gruppert modus (tre-modus er unntatt) — i gruppert modus holder utvalget seg innenfor én gruppe og krysser aldri en gruppegrense (dra/piltaster klampes til ankergruppen; innliming skriver aldri forbi gruppen). Fyll: kopier én celle (1×1) og lim inn i et større markert område → verdien fylles inn i alle de markerte cellene. Vanlige klikk er uendret (suppresjon kun etter reell dra-bevegelse); Esc/naviger uten Shift nuller området. |
clipboardMaxCells |
5000 |
Tak per innliming — celler utover taket hoppes over (out-of-range). |
clipboardPaste |
— | ÉN batch per innliming (og per Ctrl+Z): SmartGridPasteEvent { changes, skipped, isUndo? }. Hosten applyer — griden muterer aldri data. |
- Kopier (Ctrl+C): aktiv område-markering kopierer det markerte rektangelet, valgte rader kopierer hele synlige datarader, og ellers kopieres kun fokusert celle. Fokus på ikke-data-celler lar forrige kopitilstand være urørt. TSV-en limes rett inn i Excel med kolonnene intakt.
- Lim inn (Ctrl+V): TSV fra Excel fyller fra fokusert celle mot høyre/nedover. Parses per
kolonnens
editType: tall (både norsk komma- og engelsk punktum-form), dato (ISO,dd.MM.yyyy,dd/MM/yyyy→ normaliseres til ISO), select (label ELLER verdi, case-insensitivt), tekst rått.editValidatorgjelder som ved inline-edit. - Lookup-kolonner: limes via kolonnens
lookupResolveValue(raw, row)(async, per celle; treff girnewValue+lookupItempå changen). Dedup av like råverdier er OPT-IN vialookupResolveCacheKey(resolvere kan være radkontekst-sensitive). Uten resolver hoppes cellen over medlookup-resolver-missing. - Hoppet over: ikke-redigerbare/
custom-kolonner (readonly— posisjonen konsumeres, som i AG Grid/Excel), parse-feil (parse), select/lookup uten treff (no-match), utenfor gridet/over taket (out-of-range). Hosten kan viseskippedi toast. - Angre (Ctrl+Z): siste innliming re-emittes med old/new byttet og
isUndo: true— ett nivå, host-applyet. - Gruppert modus: en innliming som treffer gruppe-/aggregatfelt utløser samme recompute som inline-edit (rader flytter gruppe, subtotaler/total oppdateres).
Inline-redigering i gruppert modus¶
Celler med editable/editType redigeres også når griden er gruppert — samme dobbeltklikk/
tastatur-UX, samme editorer (inkl. lookup og excel-modus) og samme cellEdit-kontrakt som flat
modus (delt celle-renderer). Regler:
- Krever stabil
rowKey— uten den nektes redigering i gruppert modus (dev-warning). - Gruppe-header-, gruppe-footer- og totalrader er aldri redigerbare.
- Tastaturnavigasjon i gruppert modus går over de synlige løvradene: piltaster/Enter/Tab hopper over gruppehoder, footere og rader under kollapsede grupper (og aldri til en annen side).
- Aggregat-refresh etter commit: endres et felt som inngår i aktiv gruppering (raden kan
tilhøre en annen gruppe) eller i
groupAggregates, rekalkulerer griden — gruppe-subtotaler, gruppe-footere og totalrad oppdateres. Felt som kun refereres avaggregateFilters-predikater er ikke introspiserbare og fanges ikke av denne triggeren (v1-begrensning) — der kommer korrektheten fra hostens dataoppdatering. - Tre-modus (
treeData) er fortsatt utenfor scope for inline-redigering.
Gruppe-subtotaler som kolonneceller (showFieldSum i gruppert modus)¶
Når showFieldSum er aktiv OG griden er gruppert (og ikke i tre-modus), rendres hver gruppes
aggregater som en footer-rad per gruppenivå: én celle per synlig kolonne, verdi kun under
kolonnene i groupAggregates, kolonnejustert med align. Header-pillene ved gruppehodet
skjules da (tallene dupliseres aldri) — og kollapsede grupper viser fortsatt subtotalen
(footer-raden legger seg rett under gruppehodet). Uten gruppering, eller uten showFieldSum,
er ingenting endret.
Tallformatet følger kolonnen: count er alltid heltall, numberFormat-kolonner bruker samme
formatter som datacellene sine (decimals respekteres), og først uten instruks faller den
tilbake til to desimaler. Gjelder også totallinjen i field-sum-modus.
Lagrede filtre (enableSavedFilters)¶
| API | Default | Beskrivelse |
|---|---|---|
enableSavedFilters |
false |
Tar med «Lagrede filtre»-seksjonen i det samlede Filter-panelet i toolbaren. Krever gridId. |
Navngitte, per-bruker lagrede filtre — en raskere inngang enn visninger når kun filtreringen skal byttes (f.eks. «kunde X» eller en statuskombinasjon):
- Lagrer HELE filter-slicen: filterrad-filtre, avansert filtermodell OG globalt søk — uansett hvor brukeren bygde filteret. Kolonner/sortering/gruppering røres aldri.
- Persisteres via samme mekanikk som visninger (
GridViewService/wv_SmartGridView) under navneromet<gridId>::filters— roaming per bruker, og standard-markering (★) kolliderer aldri med visningenes standard. Standardfilteret auto-appliseres ved lasting. - «Oppdater valgt filter» overskriver aktivt filter; «Tøm filter» nuller kun filter-slicen.
- Visninger vs. filtre: en visning lagrer hele oppsettet (layout + filter); et lagret filter bytter bare filterdelen oppå gjeldende layout.
Samlet Filter-knapp (avansert builder + lagrede filtre i ett panel)¶
Toolbaren har én «Filter»-knapp (trakt-ikon) — ikke lenger to nesten-homonyme knapper
(«Filtre» + «Filter»). Ett klikk åpner ett panel som samler begge funksjonene, hver seksjon
uavhengig styrt av sin egen @Input:
Panelet åpnes som en kompakt dropdown etter samme mønster som Visninger, uten å skyve selve
tabellen ned. Dropdownen viser Lagrede filtre øverst (den raske veien for tilbakevendende
brukere), deretter Filterbetingelser, og Lagre dette filteret vises kun når et filter eller
søk faktisk er aktivt. Klikk utenfor eller ESC lukker dropdownen.
- Lagrede filtre (
enableSavedFilters+gridId) — øverst: listen med lagrede filtre («Mine»/«Delt»-gruppert ved deling), «Lagre gjeldende filter» og oppdater/tøm-handlingene. Uten lagrede filtre kollapser seksjonen til én hint-linje. - Avansert filter-builder (
enableAdvancedFilter) — betingelses-UI-en ligger bak «Nytt filter»-knappen (progressive disclosure): builder-kroppen vises først når brukeren ber om den, når en avansert modell allerede er aktiv (f.eks. et anvendt lagret filter), i builder-only paneler, eller for førstegangsbrukere uten lagrede filtre (panelet åpner da builder-først). «Nytt filter» nullstiller builderen til en fersk modell (går viaonAdvancedFilterChange(null), så host-emit og aktiv-lagret-filter-markør oppfører seg som en vanlig tømming). Panelet holdes åpent; lukking av panelet kollapser builderen igjen.
Relative datofiltre («i dag ± n»)¶
Datokolonner i builderen har en modusvelger per verdi: Fast dato (datepicker, som før) eller
Relativ — «I dag ± antall dager/uker/måneder/år». mellom kan blande fritt («fra i dag til
i dag + 2 måneder»). En relativ betingelse lagres som et token (@today, @today-7d,
@today+2m) i filtermodellen/lagrede filtre og evalueres mot dagens dato hver gang filteret
brukes — et delt «Forfalt»-filter (dato < i dag) blir aldri utdatert. Teknisk kontrakt:
- Tokens finnes KUN i UI-modellen og persistert view-state (
getViewState()/lagrede filtre). - Alle server-vendte flater får oppløste kloner:
getQueryState()(host-spørringer) og dermedgetExportRequest()(server-eksport), samt klient-side radevaluering. Backend ser aldri tokens. - Oppløsningen bruker brukerens LOKALE kalenderdato; månedsaritmetikk klemmes til månedens siste
dag (31. jan + 1 mnd → 28./29. feb). Klokken er injiserbar for tester (
nowProvider).
Knappen vises når enten funksjonen er på; er bare én på, viser panelet kun den seksjonen. Når
panelet er lukket viser knappen aktiv-tilstand med tellemerke = antall aktive avansert-betingelser
+ aktive filterrad-filtre (og aktiv-stil også når et lagret filter er valgt). aria-expanded følger
panelet, og ESC lukker det. Alle eksisterende utganger er uendret (advancedFilterChange,
lagret-filter-apply/lagre/slett, toveis [(advancedFilter)]) — ingen konsument trenger å endre
bindingene sine.
Radtetthet (enableDensityToggle / density)¶
| API | Default | Beskrivelse |
|---|---|---|
enableDensityToggle |
false |
Viser kompakt/løs-toggle i toolbaren (samme visuelle språk som detaljpanel-togglen). |
density |
'comfortable' |
Radtetthet. 'compact' strammer celle-, header- og filterrad-padding (kun desktop-tabellen — mobile cards uendret). |
densityChange |
— | Emitter når brukeren bytter tetthet. |
- Tettheten lagres i visninger: feltet skrives i view-state når togglen er på, og gjenopprettes med felt-tilstedeværelse-som-intensjon (gamle visninger uten feltet bevarer gjeldende tetthet). En visning lagret med density restyler aldri et grid hvor hosten ikke har skrudd på togglen.
- virtualScroll-kontrakt: kompakt rad er 32 px og løs 40 px;
effectiveRowHeight()bruker disse som spacer-estimat når hosten ikke binderrowHeighteksplisitt. En eksplisittrowHeightvinner alltid — sett den i takt med faktisk radhøyde hvis du overstyrer CSS.
Excel-modus (editUx)¶
| Input | Default | Beskrivelse |
|---|---|---|
editUx |
'form' |
Redigerings-UX. 'form' beholder dagens UI (form-control + ✓/✗-knapper). 'excel' gir borderless editor som fyller cellen, med Excel-semantikk. |
I editUx="'excel'":
- Enter committer og flytter fokus én rad ned; Tab committer og flytter høyre (Shift+Tab venstre); Esc avbryter.
- Blur (fokus forlater editor-flaten = input + lookup-overlay) committer; valideringsfeil ved blur reverterer (editoren kan ikke stå åpen uten fokus).
- Type-to-edit: printbart tegn på fokusert redigerbar celle starter redigering og erstatter
verdien; F2 redigerer med eksisterende verdi. Krever
keyboardNavigation. Space, modifier- chords, action-/tree-celler ogeditType: 'custom'starter aldri redigering. - ✓/✗-knappene skjules på desktop, men beholdes på touch-enheter (coarse pointer).
Lookup-editor (editType: 'lookup')¶
Oppslags-editor med host-levert søk (H-SG-MO-009). Gridet eier UI/tastatur/debounce/stale-vern; hosten eier datakilden — gridet gjør aldri HTTP selv.
| Kolonne-API | Default | Beskrivelse |
|---|---|---|
lookupSearch |
— | (term, row) => Observable<GridLookupItem[]> \| Promise<GridLookupItem[]>. Påkrevd for lookup. row gir hosten kontekst-filtrering. |
lookupMinChars |
1 |
Minste antall tegn før søket fyrer. |
lookupDebounceMs |
250 |
Debounce på søket. |
lookupAllowFreeText |
false |
Tillat commit av råtekst som ikke er et treff. Committer da syntetisk GridLookupItem med freeText: true. Av: commit av ikke-treff avvises og editoren forblir åpen. |
GridLookupItem = { value, label, description?, meta?, freeText? }—valueblir celleverdien,descriptionvises som sekundærlinje i trefflisten,metaer host-eid og opak for gridet.- Tastatur: ↑/↓ i trefflisten, Enter velger (i excel-modus: + flytt ned), Esc avbryter, Tab committer valgt treff (excel-modus: + flytt høyre).
- Stale-vern: et søkesvar utstedt før nyere input forkastes (generation-guard) — et tregt svar for «ga» kan aldri overskrive resultatene for «gast».
- Tilstander i trefflisten (i18n):
SmartGrid.lookupSearching,SmartGrid.lookupNoResults,SmartGrid.lookupSearchFailed,SmartGrid.lookupNoMatch(avvist ikke-treff-commit). - Overlayet lukkes ved avbryt/commit, recompute (filter/sort/data-endring) og destroy.
- Musevalg i listen håndteres på
mousedown(preventDefault) slik at input aldri blurres før valget er behandlet — trygt sammen med excel-modusens blur-commit. CellEditEvent.lookupItem(hele treff-objektet på commit) leveres i steg 3 av H-SG-MO-009.
Eksempel på trygg lagring:
onCellEdit(event: CellEditEvent<ProductRow>): void {
const { row, field, newValue } = event;
this.productService.updateField(row.id, String(field), newValue).subscribe({
next: updated => {
this.rows = this.rows.map(item =>
item.id === row.id ? { ...item, ...updated } : item
);
},
error: () => {
this.toastService.show({
type: 'error',
data: [this.translate.instant('Common.CouldNotSave')],
});
},
});
}
Pending changes (Lagre/Forkast)¶
Opt-in alternativ til den vanlige inline edit-flyten over, der hver cellEdit sendes til host med
det samme. Med pendingChanges samler gridet i stedet mutasjoner i et in-memory overlay til
brukeren eksplisitt trykker Lagre — nyttig for skjemalignende tabeller (f.eks. ordrelinjer) der
brukeren skal kunne redigere flere celler/rader og angre før noe persisteres.
Kilde (provenance):
ui/ePortal.ui/src/app/shared/smart-grid/smart-grid.component.ts—pendingChanges,pendingSave,isPendingMode(),save(),discard(),hasPendingChanges(),requestClose(),valueFor()(preview-oppslag),tryPendingBulk(),applyPendingEdits(),attemptServerPageChange()/attemptServerFilterChange().ui/ePortal.ui/src/app/shared/smart-grid/smart-grid-pending.ts—PendingOverlay, det rene in-memory overlayet (Map nøkkelt på${rowKey}::${field}).ui/ePortal.ui/src/app/shared/smart-grid/smart-grid-pending-guard.service.ts—SmartGridPendingGuardService, den delte NgbModal-bekreftelsen brukt avrequestClose()og server-side page/filter-interception.ui/ePortal.ui/src/app/shared/smart-grid/smart-grid.models.ts—SmartGridPendingChange,SmartGridPendingSaveEvent,SmartGridPendingSaveResult,GridColumn.pendingPreviewValue,GridColumn.pendingBypass.
Slå på pending-modus¶
<konti-smart-grid
[data]="rows"
[columns]="columns"
[rowKey]="rowKey"
[pendingChanges]="true"
(pendingSave)="onPendingSave($event)">
</konti-smart-grid>
rowKey er obligatorisk. Uten en stabil rowKey kan ikke overlayet knytte en endring til en
rad på tvers av sortering/filter/paging — isPendingMode() logger console.warn('[smart-grid] pendingChanges requires a rowKey input; pending mode is disabled.') én gang og pending-modus forblir av (gridet oppfører seg som om pendingChanges er false).
@Output() pendingSave¶
@Output() pendingSave = new EventEmitter<SmartGridPendingSaveEvent<T>>();
SmartGridPendingSaveEvent<T>:
interface SmartGridPendingSaveEvent<T = any> {
saveId: string; // grid-generert, ett per Lagre-trykk
changes: SmartGridPendingChange<T>[]; // brukerens matrise-rekkefølge, stabil
complete: (result: SmartGridPendingSaveResult) => void;
}
interface SmartGridPendingChange<T = any> {
row: T; // host-radobjektet (ikke mutert av gridet)
rowKey: string | number;
field: string;
originalValue: unknown;
pendingValue: unknown;
lookupItem?: unknown; // samme semantikk som CellEditEvent.lookupItem
}
type SmartGridPendingSaveResult =
| { status: 'confirmed' }
| { status: 'rejected'; message?: string }
| { status: 'partial'; succeededRowKeys: (string | number)[]; message?: string };
Host-eksempel:
onPendingSave(e: SmartGridPendingSaveEvent<OrderLineRow>): void {
this.orderLineService.saveBatch(e.changes).subscribe({
next: () => e.complete({ status: 'confirmed' }),
error: (err) => e.complete({ status: 'rejected', message: err.message }),
});
}
Fullføringskontrakt (complete):
complete({ status: 'confirmed' })— gridet tømmer overlayet OG angre-stacken for pending-operasjoner. Lagre/Forkast-verktøylinjen forsvinner (ingen ulagrede endringer igjen).complete({ status: 'rejected', message? })— overlayet og angre-stacken beholdes uendret, slik at brukeren kan rette opp og prøve igjen. Host kan viseresult.messagei en toast.complete({ status: 'partial', succeededRowKeys, message? })— best-effort delvis suksess. Brukes når host lagrer rad-for-rad over uavhengige (ikke-transaksjonelle) endepunkter, og noen rader lyktes mens andre feilet. Host oppgir kunsucceededRowKeys(rowKey-verdiene til radene den har bevist ble persistert — HTTP 2xx). Gridet dropper nøyaktig de radenes overlay-oppføringer og beholder de feilede radene som pending, og prunes angre-stacken til operasjoner som fortsatt har en levende overlay-oppføring. Gridet — ikke host — eier overlay-mutasjonen: host når aldri inn i gridets private overlay-tilstand, den navngir bare hvilke rader som lyktes. Typisk flyt: host laster tabellen på nytt (henter server-id-er for nyopprettede rader), re-appender fortsatt-feilede nye rader (en feilet Create ble aldri persistert og kommer ikke tilbake fra reload) slik at deres gjenlevende overlay-oppføringer fortsatt peker på en synlig rad, og kaller såpartial. Ingen duplikat-Create ved retry — en lykket Creates temp-nøkkel ligger isucceededRowKeys, så overlay-oppføringen ryddes og raden gjeninnsendes aldri som ny.savingnullstilles ogmarkForCheck()kjøres som for de to andre utfallene.- Idempotent/stale-vern:
complete()skal kalles nøyaktig én gang perpendingSave-event. Gridet sporersaveIdfor det aktive lagringsforsøket internt og ignorerer encomplete()-kall hvissaveIdikke lenger matcher det aktive forsøket (f.eks. en treg første request som fullfører etter at en ny lagring allerede er startet, eller et duplisert kall). Et andre trykk på Lagre er uansett forhindret menssavingertrue— knappene er disablet tilcomplete()kjører.
Visuell tilbakemelding¶
- Rader med ulagrede endringer får en gul stripe langs venstre kant (
.sg-row-pending,box-shadow: inset 3px 0 0 0 var(--bs-warning)). - Så snart minst én rad har en pending endring vises en egen verktøylinje med Lagre(n) (
btn-warning, badge med antall endrede rader) og Forkast (btn-outline-secondary, åpner enNgbModal-bekreftelse — aldriconfirm()). Verktøylinjen er skjult når overlayet er tomt. - Ctrl+Z angrer siste pending-operasjon (én cellekommit, én paste-batch eller én bulk-anvendelse reverteres som én enhet) — reverterer overlay-oppføringene skrevet av den operasjonen, ikke bare siste enkeltcelle.
GridColumn.pendingPreviewValue — beregnet forhåndsvisning¶
{
field: 'lineAmount',
titleKey: 'Order.LineAmount',
pendingPreviewValue: (row, col, pendingRow) =>
Number(pendingRow['quantity']) * Number(pendingRow['unitPrice']),
}
Brukes for avledede/kolonner som ikke er direkte redigerbare, men som skal reflektere ulagrede
endringer i andre celler på samme rad (f.eks. linjesum som følger Antall/Enhetspris før lagring).
pendingRow er et snapshot av radens gjeldende feltverdier overlagt med pending-verdiene for
den raden. Kalles kun mens raden faktisk har minst én pending endring; verdien er ren visning og
inngår aldri i pendingSave-batchen.
applyPendingEdits() — programmatiske pending-endringer (host-seam)¶
this.grid.applyPendingEdits([
{ row: line, field: 'unitPrice', value: 1250 },
{ row: line, field: 'discountPct', value: 10 },
]);
Lar host skrive en batch programmatisk beregnede endringer inn i pending-overlayet — f.eks. en bulk-rekalkulering hvis resultat brukeren skal se over og bekrefte med Lagre (masterordre-linjenes «Rekalkuler priser»). Hele batchen er ÉN angre-operasjon (Ctrl+Z reverterer alt samlet, som en paste-batch). Ukjente felt hoppes over; en verdi lik cellens nåværende verdi etterlater ingen overlay-oppføring. No-op utenfor pending-modus.
Formaterte kolonner (format-funksjon) rendres i pending-modus fra radens verdier overlagt
med pending-verdiene, slik at en ulagret endring vises formatert (dato/beløp) og ikke som den
gamle verdien.
hasPendingChanges() og requestClose() — navigasjonsvern¶
hasPendingChanges(): boolean;
requestClose(): Promise<boolean>;
hasPendingChanges()—truenår pending-modus er aktiv og overlayet ikke er tomt. Brukes fra enCanDeactivate-guard eller en skuff/quick-panel sin lukke-håndtering.requestClose()— kalles av host før en skuff/rute lukkes. Løsertruenår det er trygt å lukke (ingen pending endringer, eller brukeren valgte Forkast). Løserfalsenår brukeren valgte Avbryt, eller valgte Lagre —save()er asynkron, så host må sjekkehasPendingChanges()på nytt etter atpendingSavescomplete()har kjørt, i stedet for å behandle "Lagre" som umiddelbart grønt lys for å lukke.- Gridet viser i tillegg nettleserens innebygde «forlate siden?»-varsel via
beforeunloadnår fanen/vinduet lukkes med ulagrede endringer (ingen egen-stylet variant finnes forbeforeunload).
// Eksempel: CanDeactivate-guard i host-komponenten
canDeactivate(): Promise<boolean> {
return this.grid.requestClose();
}
Bulk-redigering på «alle treff» avvises¶
requestBulkValueCopy(...) ruter inn i pending-overlayet i stedet for å emittere
bulkEditRequested når pendingChanges er aktiv. Et utvalg i allMatching-modus (se «Robust
selection-state og bulk-handlinger») avvises — gridet holder kun de aktuelle side-/filtrerte
radene på klienten og kan derfor ikke rendre, vise eller lagre pending endringer for rader det ikke
har. Avvisningen logges (console.warn) med i18n-nøkkelen SmartGrid.pending.bulkAllMatchingRefused;
bruk server-side lagringsflyten for denne typen masseendring i stedet.
Server-side paging/filter/søk blokkeres bak bekreftelse¶
Mens det finnes ulagrede endringer, avbryter gridet server-side sidebytte
(serverSidePagination) og server-side filter/søk (serverSideSearch) med samme
NgbModal-bekreftelse som Forkast-flyten (via SmartGridPendingGuardService), før host varsles
via pageChange/serverFilterChange:
- Avbryt → ingenting skjer, brukeren blir værende på gjeldende side/filter.
- Forkast → overlayet tømmes, deretter kjøres side-/filterbyttet.
- Lagre →
save()kalles; selve side-/filterbyttet skjer ikke automatisk etter lagring — host trigger det på nytt om ønskelig.
Klient-side paginering/filtrering/gruppering rutes ikke gjennom denne sperren — overlayet er
knyttet til rowKey og overlever klient-side visningsendringer uten problemer.
Sidetall-klikk fra den innebygde ngb-pagination-kontrollen går gjennom samme server-side
sidebytte som endring av sidestørrelse. Når det ikke finnes ulagrede endringer, emitterer gridet
pageChange med både currentPage og pageSize, slik at hosten kan hente riktig side.
Feature-oppsummering¶
| API | Type | Bruk |
|---|---|---|
pendingChanges |
@Input() boolean (default false) |
Slår på pending-overlay. Krever rowKey. |
pendingSave |
@Output() EventEmitter<SmartGridPendingSaveEvent<T>> |
Emitteres ved Lagre. Host persisterer og kaller complete() én gang. |
GridColumn.pendingPreviewValue |
(row, col, pendingRow) => unknown |
Display-only beregnet forhåndsvisning mens raden har pending endringer. |
hasPendingChanges() |
metode | true når overlayet ikke er tomt. |
requestClose() |
metode → Promise<boolean> |
Host-kall før lukking av skuff/rute/quick-panel. |
Grenser (slice 1)¶
Dette er slice 1 — opt-in grunnmur i selve smart-grid-komponenten, uten noen forbruker koblet på ennå. Aktivering på en konkret side (f.eks. masterordre), persistens av faktiske pending-endringer mot backend, og tilhørende release-notat er en separat, senere slice. Denne siden dokumenterer kun den gjenbrukbare smart-grid-kontrakten.
Excel-eksport¶
enableExport viser en Excel-dropdown med to valg:
- Uten formattering: bruker
ReportExportService. - Med formattering: bruker ExcelJS, inkluderer tittel, eksporttidspunkt, headerstil, nummerformat og gruppering der det er aktivt.
<konti-smart-grid
[data]="rows"
[columns]="columns"
[enableExport]="true"
[exportButtonLabel]="'Common.ExportToExcel' | translate"
exportFilename="customer-list">
</konti-smart-grid>
Eksporten bruker effectiveColumns(): synlighet, rekkefølge og bredder fra kolonnekonfigurasjonen tas hensyn til. exportOnly-kolonner kommer med i eksporten, men ikke i gridet. Kolonner der field starter med __ filtreres ut fra eksport. Celleverdier: exportValue (når satt) → format → rå feltverdi; tall skrives som tallceller med norsk numFmt.
PDF-eksport (exportFormats)¶
PDF-eksport er opt-in via exportFormats. Standard ['excel'] er uendret (samme knapp, samme to Excel-valg). Legg til 'pdf' for å tilby «Eksporter til PDF» i eksport-dropdownen:
<konti-smart-grid
[data]="rows"
[columns]="columns"
[enableExport]="true"
[exportFormats]="['excel','pdf']"
[exportDataProvider]="exportProvider"
exportFilename="kunder">
</konti-smart-grid>
PDF-en bygges i nettleseren av ReportExportService.exportPdf (lazy-lastet pdfmake — ingen ny avhengighet, ingen backend-endring). Den bruker de samme eksport-kolonnene og radene som Excel-eksporten (samme effectiveColumns(), exportOnly respekteres, __-kolonner filtreres ut), rendres som liggende A4 med sidetall-footer, og norske tegn (æøå) går rett gjennom. Eksport-knappens tekst/ikon følger exportFormats: kun ['excel'] beholder tekstetiketten fra exportButtonLabel (default «Excel»); kun ['pdf'] og to eller flere formater viser kun ikon, uten synlig knappetekst.
For rapporter som skal sendes videre til backend kan komponenten også bygge en .xlsx-buffer via exportToBuffer(useAllRows), slik enkelte bilagsflyter gjør før opplasting.
import { ViewChild } from '@angular/core';
import { SmartGridComponent } from 'src/app/shared/smart-grid/smart-grid.component';
@ViewChild(SmartGridComponent)
grid?: SmartGridComponent<CustomerRow>;
async uploadCurrentExport(): Promise<void> {
const buffer = await this.grid?.exportToBuffer(true);
if (!buffer) return;
this.customerService.uploadExport(buffer).subscribe();
}
useAllRows = true bruker alle rader gridet har fått inn. Ved server-side paging betyr det bare alle rader i parentens nåværende datasett, ikke automatisk hele databasen. Hvis eksporten skal dekke alt i en server-side liste, må parent hente alle eksport-radene fra API eller bruke backend-eksport.
Server-side eksport (exportDataProvider)¶
For server-paginerte/virtualiserte grids holder klienten bare gjeldende side, så vanlig eksport blir ufullstendig. Sett exportDataProvider til en funksjon som henter hele datasettet (typisk et backend-endepunkt) — eksporten bruker da den i stedet for klientradene. Funksjonen kan returnere Promise<T[]> eller Observable<T[]>. Default uten provider → eksporten er nøyaktig som før.
exportAll = (): Observable<CustomerRow[]> =>
this.customerService.getAllForExport(this.activeFilters);
<konti-smart-grid [columns]="columns" [data]="pageRows" [enableExport]="true"
[serverSidePagination]="true" [exportDataProvider]="exportAll">
</konti-smart-grid>
Provider-rader eksporteres flatt (klient-gruppering re-anvendes ikke på server-datasettet).
Server-side eksport-request (serverSideExport / exportRequested)¶
exportDataProvider krever at hosten selv kjenner spørringen. H-SG-MO-006 gir i stedet en standardisert eksport-request: sett [serverSideExport]="true", og eksportknappen emitter exportRequested med en SmartGridExportRequest i stedet for å bygge klient-Excel. Gridet gjør ingen HTTP selv (samme mønster som bulkActionRequested) — hosten poster requesten til sitt eget domene-endepunkt, som reproduserer spørringen server-side og streamer fila, uten at klienten materialiserer alle radene.
<konti-smart-grid [columns]="columns" [data]="pageRows" [enableExport]="true"
[serverSideExport]="true" (exportRequested)="onExport($event)">
</konti-smart-grid>
onExport(req: SmartGridExportRequest) {
// Post `req` til eget endepunkt; server reproduserer query og streamer fila.
this.api.exportMasterOrders(req).subscribe(/* last ned blob */);
}
SmartGridExportRequest (C#-speil: SmartGridExportRequestDto):
| Felt | Betydning |
|---|---|
schemaVersion |
Request-envelope-versjon (SMART_GRID_EXPORT_REQUEST_VERSION), uavhengig av query.schemaVersion. |
rowScope |
'filteredResult' (hele det filtrerte settet, default) eller 'selection' (kun query.selection). |
query |
Full SmartGridQueryState (filter/søk/sortering/gruppering/scope). Merk: page/pageSize er utelatt — paginering er viewport-tilstand, aldri eksport-omfang. selection er kontekst, ikke omfang, med mindre rowScope === 'selection'. |
columns |
Den eksakte ordnede eksport-projeksjonen (field, title, order, exportOnly, formatHint, numberFormat?, decimals?, currency?). Inneholder aldri skjulte/interne kolonner — full kolonnesynlighet ligger i query.columns. |
tree? |
Tre-data-valg (visibleOnly, includeCollapsedChildren, includeHierarchyPath); kun når treeData. |
formatted? |
Formattert (vs rå) eksport — «Med formattering»-valget sender true, «Uten formattering» false. |
filename? |
Foreslått filnavn (uten filendelse). |
Format-hint per kolonne avledes fra filterType ('text'/'number'/'date'); currency settes kun via eksplisitt GridColumn.exportFormatHint = 'currency' (aldri gjettet fra align/format()/feltnavn) og reiser som formatHint: 'number' + currency: true.
Synkron v1: requesten er en synkron kontrakt. Store datasett / async eksportjobb (operationId + poll) er en planlagt oppfølging (H-SG-MO-006b) som gjenbruker samme
SmartGridExportRequestsom jobb-payload.
Kolonnevelger og rekkefølge¶
Kolonnekonfigurasjon er opt-in. Slå på allowColumnConfig for å vise Kolonner-knappen.
<konti-smart-grid
[data]="rows"
[columns]="columns"
[allowColumnConfig]="true"
[allowColumnReorder]="true"
gridId="fixedAsset.assetOverview"
(columnConfigChange)="onColumnConfigChange($event)">
</konti-smart-grid>
Oppførsel:
columnConfigfra parent vinner over localStorage.- Hvis
gridIder satt, lagres endringer i localStorage med nøkkelsmartgrid.colcfg.<gridId>. - Drag-and-drop-rekkefølge bruker samme config som
Kolonner-panelet. - Pinned columns kan bare flyttes innenfor egen sone: venstre, midt eller høyre.
- Funksjonen er av som standard for å unngå regresjoner i eksisterende tabeller.
Hvis konfigurasjonen skal lagres på brukerprofil eller backend, bind både columnConfig og columnConfigChange:
columnConfig: ColumnConfigEntry[] = [];
ngOnInit(): void {
this.settingsService.getGridSettings('customer.list').subscribe(settings => {
this.columnConfig = settings.columnConfig ?? [];
});
}
saveColumnConfig(config: ColumnConfigEntry[]): void {
this.columnConfig = config;
this.settingsService.saveGridSettings('customer.list', {
columnConfig: config,
}).subscribe();
}
<konti-smart-grid
[data]="rows"
[columns]="columns"
[allowColumnConfig]="true"
[allowColumnReorder]="true"
gridId="customer.list"
[columnConfig]="columnConfig"
(columnConfigChange)="saveColumnConfig($event)">
</konti-smart-grid>
Bruk en stabil gridId som ikke endres når route-parametre eller oversatte tekster endres. Godt mønster er <module>.<page>, for eksempel crm.customerList.
Lagrede visninger (views)¶
Gridet kan lagre og hente brukerens egne visninger — kolonner, sortering, filter, grupper og sidestørrelse — per bruker og grid i databasen (wv_SmartGridView). En visning er ett kombinert øyeblikksbilde lagret som opak JSON. Funksjonen er opt-in: en grid som ikke tar den i bruk er uendret.
Aktiver innebygd views-dropdown med enableSavedViews sammen med en stabil gridId:
<konti-smart-grid
gridId="subscription-grid-workbench"
[enableSavedViews]="true"
[data]="rows"
[columns]="columns"
[rowKey]="rowKey">
</konti-smart-grid>
Når enableSavedViews er true og gridId finnes, laster gridet brukerens
visninger via GridViewService, viser views-dropdown i toolbaren og auto-applier
brukerens default-visning én gang ved init. Når enableSavedViews er false
(default), rendres ingen views-UI og ingen view lastes.
Gridet eksponerer fortsatt to metoder (kall via @ViewChild) for hoster som vil
styre lagring eller testing selv:
getViewState(): SmartGridViewState— øyeblikksbilde av gjeldende oppsett, til lagring.applyViewState(state)— bruker et lagret oppsett. Kolonnetilstand settes kun i minnet og rører ikkeallowColumnConfig-lagringen (egen localStorage-nøkkel).
Foreldede felt i en lagret visning forkastes ved restore. En visning kan overleve kolonnene den ble lagret mot (en kolonne får nytt feltnavn eller fjernes). applyViewState() slipper derfor bare gjennom grupperinger, sorteringer og filtre hvis feltet fortsatt har en kolonne på gridet — samme validering som kolonnedelen alltid har hatt. Faller alle grupperingene bort, brukes vertens [initialGroups] i stedet; en visning som er lagret eksplisitt uten gruppering (tom liste) forblir ugruppert. Det samme gjelder betingelsene i den avanserte filtermodellen (filterModel): betingelser på felt uten kolonne forkastes, modellens logic og de gjenværende betingelsene beholdes, og en modell uten betingelser igjen normaliseres til null. Uten dette ville en gammel visning vise det rå feltnavnet i gruppeoverskriften og holde liv i et filter brukeren ikke har noen kolonne å fjerne det fra — en avansert betingelse fortsetter nå å filtrere rader selv om kolonnen er borte, siden raden fortsatt har feltet. Merk at et felt som verten selv fortsatt ber om via [initialSort]/[initialGroups] regnes som levende selv om det ikke har en kolonne (typisk et utregnet sorteringsfelt). Samme validering gjelder lagrede filtre (applyFilterSlice).
Lagring/henting går via GridViewService (/api/SmartGridView), per innlogget bruker:
@ViewChild('grid') grid!: SmartGridComponent<MyRow>;
constructor(private views: GridViewService) {}
lagre(navn: string) {
const state = this.grid.getViewState();
this.views.createView({
gridId: 'crm.customerList', viewName: navn,
viewState: this.views.serializeViewState(state), isDefault: false,
}).subscribe();
}
bruk(view: SmartGridView) {
this.grid.applyViewState(this.views.parseViewState(view));
}
Den innebygde «Visninger»-nedtrekken (opt-in via [enableSavedViews]="true" + gridId) lar brukeren lagre ny, hente/anvende, oppdatere valgt visning (updateSelectedView()), tilbakestille til grid-default (resetView()), sette som standard (auto-anvendes ved last) og slette — via metodene med samme navn på SmartGridComponent.
Deling med alle (SG-SHARE)¶
Både lagrede visninger og lagrede filtre kan deles med alle brukere i klienten:
- «Del med alle»-avkryssingen ved lagre-feltet gjør den nye visningen/filteret globalt;
people-knappen på egne rader slår deling av/på i etterkant (
toggleViewShare()/toggleFilterShare()). Å slå av deling fjerner samtidig andres standard-valg som pekte på raden. - Listene grupperes i «Mine» og «Delt» når begge finnes. Andres delte rader viser «Delt av <navn>» og er apply/stjerne-only — kun eieren kan oppdatere, endre deling eller slette (backend avviser uansett skriv fra andre enn eieren).
- Stjernen fungerer også på andres delte rader: valget lagres per bruker (peker-mekanikk i
wv_SmartGridView), og følger alltid eierens siste versjon av visningen/filteret. Se datamodell-siden Smart-grid views.
Status: innebygd views-dropdown er opt-in. Live
viewStateChange-output og avansert filterbygger-UI er fortsatt planlagte oppfølginger. Se utviklernotatetdocs/SMART_GRID_VIEWS_DOCUMENTATION.mdfor detaljer.
scrollToBottom() — rull til siste rad¶
Public metode (kall via @ViewChild) som ruller gridets rulleområde til bunnen.
Brukes av hoster som legger til rader og vil vise den nye raden uten manuell
scrolling — f.eks. masterordre-ordrelinjer etter «+ Ny linje» eller produktsøk.
Kall den etter at endringsdeteksjon har rendret de nye radene (typisk i en
setTimeout(..., 0)), siden radene ikke er i DOM synkront. Virker både med og uten
virtualScroll (bunn-spaceren padder ut ikke-rendrede rader). No-op før view finnes.
@ViewChild('lineGrid') grid!: SmartGridComponent<Row>;
leggTilRad(nyRad: Row) {
this.rader = [...this.rader, nyRad];
setTimeout(() => this.grid?.scrollToBottom(), 0);
}
Server-side query-kontrakt (SmartGridQueryState)¶
SmartGridQueryState er én serialiserbar kontrakt for hele gridets spørring mot
serveren — alt en backend trenger for å reprodusere nøyaktig det brukeren ser: side,
sidestørrelse, globalt søk, sortering, filtre (både legacy filterrad og normalisert
avansert filtermodell), gruppering, kolonnetilstand og selection-scope.
Den er et supersett av view-state (SmartGridViewState, som lagrede visninger
bruker) pluss kjøretidsbitene side og selection. SmartGridQueryState
er den kanoniske server-side query-kontrakten som avansert filter-UX, bulk dry-run,
server-side eksport og kommende Masterordre-dataproviders skal bygge på.
filters (legacy hurtigfilter-rad) og filterModel (avansert modell) er separate og
kombineres som filters AND filterModel. De merges aldri på klienten — en flat modell kan
ikke uttrykke f.eks. status=Aktiv AND (land=NO OR land=SE) — så server-side consumers MÅ
anvende begge listene.
Paritet lukket (H-SG-MO-003):
getViewState()/applyViewState()/resetView()persisterer og gjenoppretter nå ogsåglobalSearchogfilterModel, så en lagret visning tar med globalt søk og avansert filter. View-state og query-state er dermed samstemte på filter/søk.
- TypeScript-modell:
SmartGridQueryStateiui/ePortal.ui/src/app/shared/smart-grid/smart-grid.models.ts. - Bygg-helper:
SmartGridComponent.getQueryState()iui/ePortal.ui/src/app/shared/smart-grid/smart-grid.component.ts(komponerergetViewState()+ globalt søk +currentPage+SmartGridSelectionState). - C#-speil for backend-mapping:
SmartGridQueryStateDtoiapi/ePortal.API/ePortal.Base/Models/SmartGrid/SmartGridQueryStateDto.cs(gjenbrukerSmartGridFilterModelDto).
Hent kontrakten fra en host via @ViewChild:
@ViewChild('grid') grid!: SmartGridComponent;
const query = this.grid.getQueryState(); // send til backend / eksport / bulk dry-run
Mapping til backend¶
Felt i SmartGridQueryState |
C#-speil (SmartGridQueryStateDto) |
Backend-bruk |
|---|---|---|
page, pageSize |
Page, PageSize |
Paginering |
globalSearch |
GlobalSearch |
Fritekstsøk på tvers av synlige kolonner |
sort[] ({field, dir}) |
Sort[] (Field, Dir) |
ORDER BY |
filters[] (legacy filterrad) |
Filters[] |
Enkel per-kolonne-filtrering (bakoverkompatibelt) |
filterModel (logic, conditions[]) |
FilterModel (SmartGridFilterModelDto) |
Avansert filtrering (in/between/isNull …) — separat fra filters, anvendes som filters AND filterModel |
groups[] |
Groups[] |
Gruppering / server-side grupperingsnivåer |
columns[] |
Columns[] |
Kolonnevalg for eksport (synlig/rekkefølge/bredde/pinned) |
selection (mode, selectedKeys, excludedKeys, scope) |
Selection |
Bulk dry-run / eksport «alle som matcher» uten å laste alle rader |
Avansert filter (opt-in, H-SG-MO-003)¶
Opt-in avansert filtermodell (SmartGridFilterModel) med operatorene in, between, isNull,
isNotNull, gt/gte/lt/lte, contains/startsWith/endsWith, eq/neq og topp-nivå and/or-logikk.
Holdes separat fra den eldre hurtigfilter-raden og kombineres som filters AND filterModel.
Modellen inngår i lagrede visninger og i SmartGridQueryState.
| API | Type | Beskrivelse |
|---|---|---|
enableAdvancedFilter |
@Input() boolean |
Slår på avansert filter (builder-UI kommer i eget steg). Default false → ingen avansert-filter-DOM, alle eksisterende grids uendret. |
advancedFilter |
@Input() SmartGridFilterModel \| null |
Toveis: seed/erstatt modellen fra host ([(advancedFilter)]). Kilde for operatorer/logikk filterraden ikke kan uttrykke. |
advancedFilterChange |
@Output() SmartGridFilterModel \| null |
Emittes når modellen endres (builder-redigering, visnings-restore, reset). |
Modellen persisteres i SmartGridViewState.filterModel, gjenopprettes av applyViewState() og
nullstilles av resetView() (sammen med filters og globalSearch). Server-side validering skal
skje per consumer: whitelist av feltnøkler + operatorer per felt/filterType, coerce/valider verdier
server-side (ikke stol på klientens dataType), og deterministisk avvisning av ugyldige betingelser.
Server-side signalflyt: eksisterende
serverFilterChange-consumers mottar KUNColumnFilter[]— de får ikke automatiskglobalSearch/filterModel. En server-side grid som tar i bruk avansert filter må lesegetQueryState()via@ViewChildpå sin refetch-trigger (eller konsumere et eksplisitt full-query-event). Steg (a) er derfor kun grunnmur til en ekte server-side consumer er koblet på.
Builder-komponent (konti-smart-grid-filter-builder)¶
Selve byggeren er en egen delt, presentasjons-komponent som gridet rendrer i det kollapsible panelet
når enableAdvancedFilter er på. Den redigerer en SmartGridFilterModel (flat and/or, per-operator
value-editor) og er ellers uavhengig av gridet.
| API | Type | Beskrivelse |
|---|---|---|
columns |
@Input() GridColumn[] |
Filtrerbare kolonner å velge felt fra (felt + filterType + evt. selectOptions). |
model |
@Input() SmartGridFilterModel \| null |
Gjeldende modell (toveis via modelChange). En ekko av eget siste emit ignoreres så redigering ikke nullstilles. |
modelChange |
@Output() SmartGridFilterModel \| null |
Emittes ved endring; null når ingen gyldig betingelse finnes. |
Gridet binder selv [columns], [model]="advancedFilterModel" og (modelChange)="onAdvancedFilterChange($event)"
— host trenger normalt bare sette [enableAdvancedFilter]="true".
Bulk-bekreftelse (opt-in, H-SG-MO-005a)¶
Massehandlinger følger flyten velg → dry-run (preview) → bekreft → execute. Ingen handling kjøres uten
forhåndsvisning + bekreftelse. Kontrakten er serialiserbar (SmartGridBulkDryRunRequest/Result,
SmartGridBulkExecuteRequest/Result i smart-grid.models.ts + C#-speil SmartGridBulkDto.cs). Selection
er den kanoniske query.selection (én kilde serveren hasher/validerer). allMatching sender
query + selection, aldri alle rader.
Domenegrense: SmartGrid eier KUN de serialiserbare kontraktene + bekreftelses-komponenten under. Et domene-eid endepunkt/adapter (ikke en generisk SmartGrid-controller) eier action-whitelist, authz, allowlist-query-mapping (
filters AND filterModel), token-/batch-validering (validationToken+expiresAt, idempotent) og audit (auditId).
Bekreftelses-komponent (konti-smart-grid-bulk-confirm)¶
Delt, presentasjons-only komponent som host rendrer inne i en NgbModal. Den viser dry-run-resultatet
og emitter kun bekreft/avbryt — host eier dry-run-kallet, execute-kallet, action-semantikk, tilgang og toast.
| API | Type | Beskrivelse |
|---|---|---|
dryRun |
@Input() SmartGridBulkDryRunResult \| null |
Forhåndsvisningen som skal bekreftes (null mens host henter den). |
actionLabel |
@Input() string |
Menneskelig navn på handlingen (host-oversatt). |
executing |
@Input() boolean |
true mens host kjører execute — deaktiverer bekreft-knappen + viser spinner. |
confirm |
@Output() void |
Bruker bekreftet — host kjører execute. |
cancel |
@Output() void |
Bruker avbrøt/lukket. |
Bekreft-knappen er sperret til et dry-run-resultat finnes med minst én berørt rad (og ikke midt i execute).
All bruker-tekst bruker SmartGrid.bulk.*; avvist-årsaker er host/domene-i18n-nøkler (reasonKey) som
oversettes direkte.
Egenskaper¶
- Additiv:
filterChange,serverFilterChange,GridFilterogColumnFilterer uendret. Kontrakten bygges på forespørsel og endrer ingen eksisterende events. - To separate filterlister: både
filters(legacy hurtigrad) ogfilterModel(avansert) følger med når de finnes.getQueryState()avleder ikke lengerfilterModelfra filterraden; server-side consumers må anvende dem separat somfilters AND filterModel. - Selection kun når aktiv:
selectioninkluderes bare nårselectionMode !== 'none'.
Kontrakten kan inspiseres live i testbenken /subscription/grid-workbench (panelet
«Kanonisk query-state (getQueryState)»), som brukes som røyktest for Masterordre-scenariet.
Mobilvisning¶
Når customMobile er false, lager gridet automatisk mobilkort basert på mobileRole og mobilePriority.
| Rolle | Effekt |
|---|---|
title |
Primær tittel i kortet |
subtitle |
Sekundærtekst under tittel |
badge |
Vises som badge/pill øverst |
meta |
Kompakt metadata i kort-header |
actions |
Vises i kort-footer |
hidden |
Skjules på mobil |
Hvis mobileRole ikke settes, auto-detekterer gridet basert på field-navn. Status-, type- og active-felt blir ofte badge; dato-/time-felt blir metadata; action-felt blir actions. Sett rollen eksplisitt på viktige sider så mobilvisningen ikke endrer seg når felt får nye navn.
Bruk customMobile="true" bare når parent-komponenten har en egen mobilvisning og smart-grid bare skal eie desktop-tabellen.
Rad-virtualisering¶
Smart-grid rendrer som standard alle rader i DOM-en. For store flate lister kan du i stedet slå på opt-in rad-virtualisering: da holdes bare radene i (viewport + buffer) i DOM-en, med usynlige avstands-rader øverst/nederst som bevarer scrollbaren. Default av → ingen endring for eksisterende grids.
| Input | Default | Bruk |
|---|---|---|
virtualScroll |
false |
Slå på virtualisering for denne tabellen. |
rowHeight |
40 |
Antatt uniform radhøyde i px. Radene må faktisk rendres i denne høyden. |
virtualOverscan |
8 |
Antall ekstra rader over/under viewport (mot blanke flekker ved scroll). |
<konti-smart-grid [columns]="columns" [data]="rows" [rowKey]="rowKey"
[virtualScroll]="true" [rowHeight]="40" [singleLineCells]="true">
</konti-smart-grid>
v1-begrensninger (viktig):
- Kun flate (ikke-grupperte) tabeller. Gruppering (
isGrouped()) virtualiseres ikke — der rendres alt som før. - Mobilkort vindueres også (samme rad-vindu som desktop-tabellen) når
virtualScroller på. Det er nødvendig fordi mobilkort-seksjonen ligger i DOM-en (CSS-skjult) på desktop — uten vindu ville f.eks. 72k skjulte kort fryse fanen. Siden korthøyde ≠rowHeighter mobil-scroll-posisjonen omtrentlig; desktop er primærmålet for virtualisering i v1. - Uniform radhøyde. Virtualiseringen antar at hver rad er
rowHeightpx. BruksingleLineCellsog unngå flerlinje-/wrappende celler, ellers blir scroll-posisjonen unøyaktig. - Sticky header og pinnede kolonner fungerer uendret (CSS-basert).
- Kombinasjon med
showPaginationer tillatt (virtualiserer innenfor siden), men vanligvis bruker du virtualisering i stedet for paging på lange lister. - Eksport er upåvirket: bruk
exportDataProviderfor server-side full eksport (radene utenfor viewport er ikke i DOM).
Hver grid som tar i bruk
virtualScrollskal gjennom en egen adopsjons-sjekkliste, inkl. ytelses-/visuell QA med prod-datamengde.
Ytelse¶
| Risiko | Anbefalt tiltak |
|---|---|
| Mange tusen rader uten paging | Bruk showPagination, serverSidePagination, opt-in virtualScroll (flate tabeller) eller hent færre rader fra API. |
| Mange kolonner med auto-width | Sett eksplisitte width-verdier eller begrens autoSizeColumns; auto-size måler header + et jevnt fordelt sample (autoSizeSampleSize, default 50) av de synlige radene — celletekst måles via canvas (data-basert, virtualiserings-trygt), custom-template/tre/reorder-kolonner faller tilbake på DOM-måling. |
| Store grupper | Bruk autoCollapseThreshold, showPagination eller backend-aggregering. |
Dyr format-funksjon |
Hold formattering ren og billig. For HTML, bruk template. |
| Ustabile rader | Sett alltid rowKey når radene kan byttes, pagineres eller redigeres. |
Kjente fallgruver¶
| Fallgruve | Riktig mønster |
|---|---|
| Bruke Kendo/AG Grid-konfigurasjon | Bruk GridColumn, initialSort, initialGroups og smart-grid sine egne inputs. |
Returnere HTML fra format |
Bruk <ng-template appSmartGridCell="field" let-row>. |
| Hardkode norsk tekst i Angular-kode | Bruk titleKey, translate pipe eller oversatte labels fra i18n. |
Glemme rowKey |
Sett stabil nøkkel, særlig ved paging, mobil expand og inline edit. |
Action-knapper trigger rowClick |
Legg (click)="$event.stopPropagation()" på action-wrapperen. |
| Store datasett uten paginering | Bruk paging eller server-side filter/search. |
| Skjulte kolonner forventes i eksport | Bruk exportOnly for kolonner som skal med i eksport, men ikke i grid. |
| Binde en getter eller et metodekall til en grid-input | Hoist verdien til et felt. Angular memoiserer template-literaler ([pageSizes]="[25,50,100,200]" er samme instans hver syklus), men en getter kan ikke memoiseres og leses på nytt hver endringsdeteksjon. Gridet absorberer nå config-inputs som ikke har endret innhold og logger én dev-advarsel som navngir bindingen, men radinputene data, pinnedTopRows og pinnedBottomRows er ikke dekket: en muter-og-klon (this.rows = [...this.rows]) er det dokumenterte refresh-signalet og kan ikke skilles fra en churnende getter. Se trap #46. |
| Bruker skjuler en kritisk action-kolonne | Ikke slå på allowColumnConfig for action-/systemkolonner uten å teste config-panelet. |
Feilsøking¶
| Symptom | Sannsynlig årsak | Sjekk |
|---|---|---|
| Kolonnen er tom | field matcher ikke radmodellen, eller dot-path finnes ikke |
Logg én rad og kontroller exact camelCase property. |
| Custom template vises ikke | appSmartGridCell matcher ikke field etter string-conversion |
Bruk samme tekst som field, for eksempel appSmartGridCell="status". |
| Klikk på action-knapp åpner raden | Event bobler til rowClick |
Legg (click)="$event.stopPropagation()" på wrapper eller knapp. |
| Filter gir ingen treff | Feil filterType, verdi-format eller select value matcher ikke radverdi |
Sjekk om raden har string, number, boolean eller enum-verdi. |
| Dato-filter virker ikke | Radverdi og datepicker/native input bruker ulike formater | Normaliser dato i parent eller bruk format bare for visning. |
| Server-side filter filtrerer også klient-side | Feil flaggkombinasjon | Bruk serverSideSearch="true" og håndter serverFilterChange. |
| Paging viser feil antall | totalRows, currentPage eller pageSize er ikke synkronisert |
Oppdater alle tre etter API-respons eller page event. |
| Inline edit lagrer ikke visuelt | Parent oppdaterer ikke rows etter cellEdit |
Muter ikke bare backend; erstatt eller oppdater raden i rows. |
| Kolonnevalg resetter seg | Ustabil eller manglende gridId |
Bruk fast gridId, eller send inn stabil columnConfig. |
| Mobilkort viser feil hovedtekst | mobileRole mangler eller auto-detektering velger feil |
Sett mobileRole eksplisitt på viktige kolonner. |
| Excel mangler kolonne | Kolonnen er skjult, actionfelt starter med __, eller exportOnly er misforstått |
Sjekk effectiveColumns, hidden, exportOnly og field-navn. |
Test og QA¶
Når smart-grid tas i bruk på en ny side, test minst dette:
| Område | Hva skal testes |
|---|---|
| Desktop | Kolonnebredder, pinned kolonner, action-knapper, hover/title og horisontal scroll. |
| Mobil | Korttittel, badge, metadata, actions og expand/collapse. |
| Data | Tom liste, én rad, mange rader, nullverdier og lange tekster. |
| Sort/filter | Alle filtertyper, reset-knapp, initialSort og initialFilter. |
| Paging | Første side, siste side, endret sidestørrelse og server-side total. |
| Templates | Custom celler i både vanlig, gruppert og mobil visning. |
| Inline edit | Lagre, valideringsfeil, Escape/cancel og API-feil. |
| Eksport | Synlige/skjulte kolonner, exportOnly, tallformat, grupper og filnavn. |
| Kolonnevalg | Vis/skjul, reorder, resize, reset og reload av siden. |
| Tilgang | Skjul action-knapper i parent hvis bruker mangler rettighet. |
For risikable endringer bør du minst ha en komponenttest eller manuell sjekkliste som dekker sort/filter, action-klikk og mobilvisning.
Verifiser før ny bruk¶
- Kolonner bruker riktige camelCase-felt fra TypeScript-modellen.
- Alle brukerrettede tekster kommer fra i18n eller eksisterende oversatte verdier.
rowKeyer satt til en stabil id.- Store datasett har paginering eller server-side filter.
- Action-knapper stopper propagation og følger
action-capsule-mønsteret. - HTML/badges/ikoner ligger i
appSmartGridCell, ikke iformat. - Mobilkort er kontrollert med
mobileRolefor viktige kolonner. - Excel-eksport er testet med synlige/skjulte kolonner, tallformat og eventuelle grupper.
- Inline edit håndterer
cellEditi parent og oppdaterer backend/data eksplisitt.




