U ovom ću blog postu pokazati kako implementirati DevExtreme Pivot Grid UI komponentu u Angular projekt. Ona omogućava prikaz i analizu višedimenzionalnih podataka iz lokalne pohrane ili OLAP kocke.
Fokusirat ću se na prikaz podataka prema specifičnim parameterima te načinu na koji te podatke uređivati jer Pivot Grid nativno ne nudi takvu mogućnost.
Pivot Grid prikaz podataka
Prva i osnovna značajka Pivot Grida je prikaz i analizu višedimenzionalnih podataka.
U ovom ću primjeru koristiti strukturu podataka prikazanu u JSON-u kojega je moguće vidjeti ispod. S jedne strane biti će poslovnice, a s druge automobili. Cilj je prikazati količinu naručenih tj. isporučenih automobila ovisno o poslovnici.
Logika aplikacije nalazit će se unutar angular-devextreme.component.ts datoteke koja se temelji na PivotGridDataSource objektu. Za sada će se unutar njega nalaziti samo fields niz i store objekt.
Poslovnice će se nalaziti krajnje lijevo area: "row", a automobili na vrhu area: "column". Sve ostalo tj. narudžbe prikazuju se kao podaci area: "data".
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
import { Component, OnInit } from '@angular/core'; import PivotGridDataSource from "devextreme/ui/pivot_grid/data_source"; @Component({ selector: 'app-narudzbe-popis', templateUrl: './narudzbe-popis.component.html', styleUrls: ['./narudzbe-popis.component.scss'] }) export class NarudzbePopisComponent implements OnInit { dataSource: PivotGridDataSource; podaciDemo = [ { "poslovnica_ID": 163, "poslovnica_naziv": "POSLOVNICA VK", "narudzba_ID": 1720303, "narudzba_narucenakolicina": 50, //Od 50 isporučeno 49 automobila "narudzba_isporucenakolicina": 49, "artikl_ar_ID": 69567, "artikl_sifra": 666627, "artikl_naziv": "Tesla Model S" }, { "poslovnica_ID": 163, "poslovnica_naziv": "POSLOVNICA VK", "narudzba_ID": 83572434, "narudzba_narucenakolicina": 20, //Još nije unesena isporučena količina "narudzba_isporucenakolicina": null, "artikl_ar_ID": 69463, "artikl_sifra": 555018, "artikl_naziv": "Fiat Panda" }, { "poslovnica_ID": 163, "poslovnica_naziv": "POSLOVNICA VK", "narudzba_ID": 83572437, "narudzba_narucenakolicina": 6, //Od 6 isporučeno 0 automobila "narudzba_isporucenakolicina": 0, "artikl_ar_ID": 69610, "artikl_sifra": 102836, "artikl_naziv": "Kia Rio" }, ... ] constructor() {} ngOnInit(): void { this.prikaziNarudzbe(); } prikaziNarudzbe(){ this.dataSource = new PivotGridDataSource({ fields: [ { dataField: "poslovnica_naziv", dataType: "string", area: "row", width: 120 }, { dataField: "artikl_naziv", dataType: "string", area: "column", expanded: true }, { caption: "Naručeno", dataField: "narudzba_narucenakolicina", dataType: "number", summaryType: "sum", area: "data" }, { caption: "Isporučeno", dataField: "narudzba_isporucenakolicina", dataType: "number", summaryType: "sum", area: "data" } ], store: { type: "array", data: this.podaciDemo } }); } } |
Izgled sučelja biti će definiran unutar angular-devextreme.component.html datoteke.
Od Pivot Grid UI svojstva koristit ću ih nekoliko:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<dx-pivot-grid [showBorders]="true" [showColumnGrandTotals]="false" [showRowGrandTotals]="true" [dataSource]="dataSource"> <dxo-field-chooser [enabled]="false" [height]="400"></dxo-field-chooser> <dxo-scrolling mode="standard" [useNative]="false" [scrollByContent]="true" [scrollByThumb]="true" showScrollbar="always"> </dxo-scrolling> <!-- "virtual" | "infinite" --> </dx-pivot-grid> |
Sve to zajedno na kraju daje sljedeće:
Pivot Grid – promjena izgleda polja
Ako želim dodatno prilagoditi prikaz određenih polja to mogu napraviti koristeći onCellPrepared metodu.
1 2 3 |
<dx-pivot-grid ... (onCellPrepared)="onCellPrepared($event)"> </dx-pivot-grid> |
U ovom slučaju hoću napraviti tri grafičke izmjene. Stupac “Naručeno” će imati sivu pozadinsku boju i podebljane brojeve, ukupne vrijednosti na dnu također će biti podebljanje, a vrijednosti koje za “Isporučeno” imaju null će imati crvenu pozadinsku boju kako bi bila uočljivija.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
onCellPrepared(e){ if(e.cell.rowType == "GT"){ e.cellElement.style.fontWeight = "bold"; } if(e.area == 'data' && e.columnIndex % 2 == 0){ e.cellElement.style.fontWeight = "bold"; e.cellElement.style.backgroundColor = "rgb(242,242,242)"; } if(e.area === "data" && e.cell.dataIndex === 1 && e.cell.value === null && e.cell.rowType != "GT"){ e.cellElement.style.backgroundColor = "rgb(255 0 0 / 42%)"; } } |
Na ekranu bi to izgledalo ovako:
Međutim, niti jedno polje nema crvenu pozadinsku boju. Razlog je što Pivot Grid sve
null vrijednosti vidi kao 0
i zato uvjet
e.cell.value === null nije zadovoljen. Ali ako stavim
e.cell.value === 0 stanje će biti puno drugačije.
Iz podaciDemo niza se može vidjeti da narudzba_isporucenakolicina može imati vrijednosti null. To znači da bi vrijednost za “POSLOVNICA VK” i automobil “Fiat Panda” trebala imati crvenu pozadinsku boju jer je narudzba_isporucenakolicina = null, a kombinacija POSLOVNICA VK” i “Kia Rio” ne bi trebala imati crvenu pozadinsku boju jer ima vrijednost narudzba_isporucenakolicina = 0.
Pivot Grid – prikaz null vrijednosti
Kako bi u PivotGridu mogao prikazati
null vrijednosti i razlikovati ih od vrijednosti 0
koristiti ću calculateCustomSummary funkciju. Tu ću funkciju vezati uz polje
dataField: "narudzba_isporucenakolicina".
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{ caption: "Isporučeno", dataField: "narudzba_isporucenakolicina", dataType: "number", //summaryType: "sum", area: "data", summaryType: "custom", calculateCustomSummary: function(options) { if (options.summaryProcess == 'start') { options.totalValue = null; } if (options.summaryProcess == 'calculate') { if (options.value !== null) { options.totalValue += options.value; } } } } |
Sada se na ekranu može vidjeti da istovremeno imam prikazane vrijednosti za isporučenu količinu i kao null
i kao 0
.
Omogućiti klik na odabrano polje
Želim omogućiti klik samo na polje koje pokazuje isporučenu količinu, ali isključivo za poslovnice i automobile koji imaju unesenu naručenu količinu. Znači, ne želim da se u koloni sa isporučenom količinom može kliknuti na prazno polje jer nema smisla unositi isporučenu količinu ako ništa nije naručeno.
Za to ću koristiti onCellClick funkciju.
1 2 3 |
<dx-pivot-grid ... (onCellClick)="onPivotCellClick($event)"> </dx-pivot-grid> |
1 2 3 4 5 6 7 8 9 10 |
onPivotCellClick(e) { //Klik na polje "narudzba_isporucenakolicina" ako ima unesenu vrijednost if(e.area == "data" && e.cell.dataIndex == 1 && e.cell.value != undefined && e.cell.rowType != "GT") { console.log(e); } //Klik na polje "narudzba_isporucenakolicina" ako je vrijednosti null tj. ako je polje crvene pozadinske boje if(e.area === "data" && e.cell.dataIndex === 1 && e.cell.value === null && e.cell.rowType != "GT"){ console.log(e); } } |
Iz prikazanog se može vidjeti da klik na prazno polje ne radi ništa, klik na crveno polje dohvaća vrijednost null
, a klik na polje za unesenom količinom dohvaća tu vrijednost. U ovom slučaju ta vrijednost je 10
.
Pivot Grid dohvaćanje vrijednosti polja
Pivot Grid je primarno kreiran s ciljem prikaza podataka tj. prema dokumentaciji nema mogućnost jednostavnog uređivanja podataka kao što je to slučaj kod Data Grida, barem ju nisam našao, ali sam znao da mora postojati neko rješenje.
Za početak želim prikazati nekakvu formu unutar koje će se vrijednost polja moći urediti. Za to ću iskoristiti Popup UI komponentu. Unutar nje definiram dxTemplate što mi omogućava daljnju prilagodbu sučelja za uređivanje isporučene vrijednosti.
Za dohvaćanje vrijednosti kliknutog polja koristit ću Drill Down.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
... </dx-pivot-grid> <dx-popup [closeOnOutsideClick]="false" [showCloseButton]="true" [(visible)]="popupVisible" [title]="popupTitle" [width]="650" [height]="200" > <div *dxTemplate="let data of 'content'"> <dx-data-grid #drillDownDataGrid [dataSource]="drillDownDataSource" > </dx-data-grid> </div> </dx-popup> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import { Component, OnInit, ViewChild } from '@angular/core'; import { DxTextBoxComponent } from 'devextreme-angular'; ... export class NarudzbePopisComponent implements OnInit { popupVisible = false; popupTitle: string; narudzba_ID: number; isporucenakolicina: number; narucenaKolicinaPopup: number; drillDownDataSource: any; @ViewChild("kolicinaUnos", { static: false }) inputName: DxTextBoxComponent; ... onPivotCellClick(e) { //Klik na polje "narudzba_isporucenakolicina" ako ima unesenu vrijednost if(e.area == "data" && e.cell.dataIndex == 1 && e.cell.value != undefined && e.cell.rowType != "GT") { //console.log(e); this.drillDownDataSource = this.dataSource.createDrillDownDataSource(e.cell); let _this = this; this.drillDownDataSource.load().done(function(result) { _this.narucenaKolicinaPopup = result[0].nas_kolicina; }); this.popupTitle = e.cell.rowPath[0] + ' - ' + e.cell.columnPath[0]; this.popupVisible = true; } //Klik na polje "narudzba_isporucenakolicina" ako je vrijednosti null tj. ako je polje crvene pozadinske boje if(e.area === "data" && e.cell.dataIndex === 1 && e.cell.value === null && e.cell.rowType != "GT"){ //console.log(e); this.drillDownDataSource = this.dataSource.createDrillDownDataSource(e.cell); let _this = this; this.drillDownDataSource.load().done(function(result) { _this.narucenaKolicinaPopup = result[0].nas_kolicina; }); this.popupTitle = e.cell.rowPath[0] + ' - ' + e.cell.columnPath[0]; this.popupVisible = true; } } |
Klikom na polje isporučene količine prikazat će se sljedeće:
Kao što se iz prikazanog može vidjeti dohvaćeni su podaci prikazani na vrhu blog posta.
1 2 3 4 5 6 7 8 9 10 11 |
{ "poslovnica_ID": 163, "poslovnica_naziv": "POSLOVNICA VK", "narudzba_ID": 1720303, "narudzba_narucenakolicina": 50, //Od 50 isporučeno 49 automobila "narudzba_isporucenakolicina": 49, "artikl_ar_ID": 69567, "artikl_sifra": 666627, "artikl_naziv": "Tesla Model S" } |
Pivot Grid uređivanje vrijednosti polja
Popup iz gornjeg primjera je potrebno prilagoditi na način da se kreira input polje za izmjenu isporučene količine. Klikom na polje sa već unesenom isporučenom količinom ista će se prikazati unutar input polja. Ako je polje prazno isto će biti i input polje.
U GIF-u se može primjetiti da uređivanje isporučene količine uredno prolazi, ali uz jedan problem. Prilikom svakog spremanja isporučene količine cijeli se Pivot Grid osvježi i vrati u krajnju lijevu poziciju. Naravno da to nije praktično. Uz to, prilikom otvaranja popupa polje za unos količine nije fokusirano (crvena podcrtana linija) nego je na njega prvo potrebno kliknuti.
U nastavku ću pokazati kako riješiti oba problema.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<div *dxTemplate="let data of 'content'"> <dx-data-grid class="dataGridKlasa" #drillDownDataGrid [dataSource]="drillDownDataSource" > <dxi-column dataField="narudzba_isporucenakolicina" caption="Isporučena količina" [visible]="true" cellTemplate="urediKolicinu"> <div *dxTemplate="let data of 'urediKolicinu'"> <dx-number-box #kolicinaUnos style="text-align: center;" [value]="data.value" [placeholder]="data.value" (onValueChanged)="spremiKolicinu($event, data)" (onEnterKey)="spremiKolicinuEnter($event, data)" > </dx-number-box> </div> </dxi-column> <dxi-column cellTemplate="gumbUrediKolicinu"> <div *dxTemplate="let data of 'gumbUrediKolicinu'"> <dx-button style="margin-bottom: 5px;" stylingMode="contained" text="SPREMI" type="success" [width]="120" (onClick)="spremiKolicinuButton($event, data)"> </dx-button> </div> </dxi-column> </dx-data-grid> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
spremiKolicinu(event, data){ this.isporucenakolicina = event.value; this.narudzba_ID = data.row.cells[0].data.narudzba_ID; } spremiKolicinuButton(event, data){ //Slanje podataka na API //if(res.success == true){ this.popupVisible = false; //} else { //Poruka o grešci //} } spremiKolicinuEnter(event, data){ // this.slanjeKolicine(data.key.nas_ID, e.value); //Slanje podataka na API //if(res.success == true){ this.popupVisible = false; //} else { //Poruka o grešci //} } |
Osvježavanje samo uređenog podatka
Jedan od problema koji se stvorio nakon što je omogućeno uređivanje podataka je to što se sada cijeli ekran osvježi i onda se svaki put iznova mora skrolati do točno određenog polja kako bi se nastavilo uređivanje. To nikako nije praktično i bilo je potrebno pronaći rješenje.
U ovom će mi slučaju pomoći ArrayStore, ali tek kada prilagodim prikaziNarudzbe() funkciju. Kao što se može vidjeti this.podaciDemo se sada dohvaćaju upravo kroz ArrayStore. Kroz key parametar identificiram unikatnu vrijednost pomoću koje Pivot Grid može znati koju vrijednost će osvježiti.
Podatke nakon unosa osvježavam pomoću update(key, values) metode unutar funkcija spremiKolicinuButton() i spremiKolicinuEnter().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 |
import { Component, OnInit, ViewChild } from '@angular/core'; import PivotGridDataSource from "devextreme/ui/pivot_grid/data_source"; import ArrayStore from "devextreme/data/array_store"; import { DxTextBoxComponent, DxPivotGridComponent } from 'devextreme-angular'; ... @ViewChild(DxPivotGridComponent, { static: false }) pivotGrid: DxPivotGridComponent; dataSource: PivotGridDataSource; dataStore: any; ... prikaziNarudzbe(){ this.dataStore = new ArrayStore({ key: "narudzba_ID", data: this.podaciDemo }); this.dataSource = new PivotGridDataSource({ fields: [ { dataField: "poslovnica_naziv", dataType: "string", area: "row", width: 120 }, { dataField: "artikl_naziv", dataType: "string", area: "column" }, { caption: "Naručeno", dataField: "narudzba_narucenakolicina", dataType: "number", summaryType: "sum", area: "data" }, { caption: "Isporučeno", dataField: "narudzba_isporucenakolicina", dataType: "number", area: "data", summaryType: "custom", calculateCustomSummary: function(options) { if (options.summaryProcess == 'start') { options.totalValue = null; } if (options.summaryProcess == 'calculate') { if (options.value !== null) { options.totalValue += options.value; } } } } ], store: this.dataStore }); } ... spremiKolicinuButton(event, data){ //Slanje podataka na API //if(res.success == true){ this.dataStore.update(data.key, { 'poslovnica_ID': data.data.poslovnica_ID, 'poslovnica_naziv': data.data.poslovnica_naziv, 'artikl_ar_ID': data.data.artikl_ar_ID, 'artikl_naziv': data.data.artikl_naziv, 'artikl_sifra': data.data.artikl_sifra, 'narudzba_isporucenakolicina': this.isporucenakolicina, 'narudzba_narucenakolicina': data.data.narudzba_narucenakolicina, 'narudzba_ID': data.data.narudzba_ID }); this.isporucenakolicina = null; this.pivotGrid.instance.getDataSource().reload(); this.popupVisible = false; //} else { //Poruka o grešci //} } spremiKolicinuEnter(event, data){ // this.slanjeKolicine(data.key.nas_ID, e.value); //Slanje podataka na API //if(res.success == true){ this.dataStore.update(data.key, { 'poslovnica_ID': data.data.poslovnica_ID, 'poslovnica_naziv': data.data.poslovnica_naziv, 'artikl_ar_ID': data.data.artikl_ar_ID, 'artikl_naziv': data.data.artikl_naziv, 'artikl_sifra': data.data.artikl_sifra, 'narudzba_isporucenakolicina': this.isporucenakolicina, 'narudzba_narucenakolicina': data.data.narudzba_narucenakolicina, 'narudzba_ID': data.data.narudzba_ID }); this.isporucenakolicina = null; this.pivotGrid.instance.getDataSource().reload(); this.popupVisible = false; //} else { //Poruka o grešci //} } |
Uz to, kada se popup otvori polje za unos isporučene količine nije fokusirano i prije unosa potrebno je na njega kliknuti. To također nije praktično. Ovo se vrlo lako riješi kroz onShown funkciju.
1 2 3 4 5 6 7 8 9 |
<dx-popup [closeOnOutsideClick]="false" [showCloseButton]="true" [(visible)]="popupVisible" [title]="popupTitle" [width]="650" [height]="200" (onShown)="onPopupShown($event)" > |
1 2 3 4 5 6 |
onPopupShown(e) { let a = this; setTimeout(() => { a.inputName.instance.focus(); }, 100); } |
Na kraju to izgleda ovako:
Zaključak
Naravno, ovo je samo dio mogućnosti koje DevExtreme Pivot Grid ima, ali dovoljno da pohvatate osnove i shvatite na koji način funkcionira.