Cześć! Mamy stworzone dwa moduły aplikacji – wprowadzania i pobierania danych. Przyszła pora na trzeci – wyświetlanie danych. Podobnie, jak w przypadku formularza, użyjemy Angular Material Design.
Najpierw teoria
Do tworzenia i zarządzania tabelami w angularze służy potężne narzędzie – Material Data Table. Jest częścią @angular/material, które zainstalujesz poleceniem:
npm i @angular/material
Jeżeli budujesz tabele z wykorzystaniem Material Data Table musisz zapoznać się z poszczególnymi dyrektywami strukturalnymi, z których te tabele składasz. O nich już za chwilę…
Budowa tabeli w szablonie
Budowę tabeli rozpoczynasz od zdefiniowania bloku <table></table>
, tak samo jak w przypadku standardowej tabeli HTML.
<table mat-table [dataSource]="<property_name>"> ... </table>
Jedynym dodatkowym elementem jest wplecenie komponentu mat-table
, który zapewni styl Material Design. Przy użyciu tego komponentu angular wyświetli kolejne wiersze danych. Ale o tym za chwilę.
Do komponentu mat-table
przekazujesz dane poprzez @Input() dataSourc
e.
Definiowanie kolumn
<table mat-table [dataSource]="<property_name>"> <ng-container matColumnDef="<column_key>"> <th mat-header-cell *matHeaderCellDef>{{<header>}}</th> <td mat-cell *matCellDef="let element">{{<element.fieldToDisplay>}}</td> </ng-container> </table>
W standardowym HTML wewnątrz bloku <table></table>
definiujesz kolejne wiersze i kolumny przy użyciu <td></td>
i <tr></tr>
. Nie inaczej w przypadku Material Table, z tym że podobnie jak w przypadku bloku <table></table>
, wrzucasz odpowiedni komponent (mat-header-cell
i mat-cell
). Komponenty zapewniają odpowiedni styl MaterialDesign, o tym już było. Możesz oczywiście wrzucić tam swoje style, jak wolisz…
ng-container
Sam content tabeli składa się z zagadkowego bloku <ng-container></ng-container>.
Pozwala on zamieścić dodatkową logikę w szablonie, dotyczącą wyświetlania danych. Równie dobrze zamiast ng-container
możesz stworzyć dodatkową sekcję <div></div>
. Różnica jest jednak taka, że <ng-container></ng-container>
to tylko wiadomość dla renderowania widoku. Tworzy miejsce dla matColumnDef
. Sam element nigdy nie trafi do drzewa DOM (a w przypadku <div></div>
stworzysz dodatkowy i zbędny element).
Wewnątrz ng-container
widzisz z kolei matColumnDef
. Identyfikuje konfigurację pojedynczej kolumny znajdującej się w bloku ng-container
. Nadajesz jej <column_key>
, czyli unikatowy id tej kolumny.
Dalej widzisz header kolumny, uzupełniony komponentem i dyrektywą *matHeaderCellDef
. Zawartość kolumny wypełniasz przez dyrektywę *matCellDef
.
Dyrektywa strukturalna
Dodaje lub usuwa poszczególne składowe drzewa DOM. Oznaczasz ją poprzez *directiveName
w kodzie HTML. Dodatkowo w przypadku Material Table przy ich użyciu oznaczasz sekcje pełniące określoną rolę w tej tabeli. Przykładowo: header, kolumna, stopka, wiersz…
*matHeaderCellDef
Definiuje sposób wyświetlania nagłówka kolumny
*matCellDef
Określa sposób wyświetlania zawartości kolumny. Zauważ zapis *matCellDef=let element
. Dyrektywa iteruje po wszystkich kolejnych obiektach określonych w [dataSource]
. Z każdego kolejnego elementu tworzy zmienną, której pole możesz wyświetlić w tej kolumnie {{element.fieldToDisplay}}
.
Cała magia MaterialTable polega na tym, że nieważne ile masz obiektów w [dataSource]. Do wyświetlenia wszystkich wierszy tej kolumny wystarczy te kilka linijek kodu, które masz przed sobą. No plus te dwie:
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
I znowu, dwa komponenty nadające lepsiejszy wygląd (nagłówek + wiersze tym razem). Dwie dyrektywy:
*matHeaderRowDef
Określa konfiguracje nagłówka dla wiersza. Dodatkowo określa kolejność wyświetlania kolumn.
*matRawDef
Określa element zapewniający konfigurację wyglądu wiersza danych, ale bez stylowania go. To zrobi komponent mat-raw.
Mamy tutaj zmienną raw
, która zawiera dane z danego (#o kurde co za wyrażenie) wiersza danych.
Dyrektywy strukturalne nie nanoszą żadnych styli. O NIEEEE… One tylko informują angulara: HEJ! Tu jest header, a tu jest content kolumny! Angular Mat Design wie co z tymi informacjami zrobić.
W tym przypadku dyrektywy *matHeaderRowDef
i *matRawDef
odwołują się do zmiennej szablonu displayedColumns
, w której zawarte są id’ki kolumn:
export class TransactionListComponent { ... public readonly displayedColumns: ColumnType[] = [ColumnType.BeneficiaryName]; ... }
Kolejność wpisywania id’ków w tej tabeli decyduje o kolejności wyświetlania kolumn w szablonie. Wbrew pozorom kolejność ich deklaracji w .html nie ma tutaj nic do rzeczy. Reszta dziwnej tablicy za moment.
Naumieni teori… Czas na prawdziwy kod!
Stwórz katalog nowego modułu: ./show-transactions
. Na ten moment nie wiemy, czy moduł zawierać będzie jedynie listę, czy w przyszłości jakieś inne komponenty (logika filtrowania, sortowania itd.). Generalnie ./show-transactions
powinien zawierać wszystkie elementy połączone z wyświetlaniem listy transakcji. Wygeneruj teraz klasę modułu w nowym katalogu::
npm generate module show-transactions
Teraz wygeneruj komponent listy
npm generate component transaction-list
Upewnij się, że AngularCLI wrzucił Twój komponent do nowego modułu:
@NgModule({ declarations: [TransactionListComponent], exports: [TransactionListComponent] }) export class ShowTransactionsModule { }
Jak widzisz u mnie wszystko zabanglało właściwie. W tablicy declarations[] znajduje się nowo stworzony komponent. Tablica exports[] udostępnia ten komponent na zewnątrz. Czas przejść do implementacji klasy:
export class TransactionListComponent implements OnInit { constructor() { } ngOnInit(): void { } }
Domyślnie każdy komponent tworzony przez AngularCLI implementuje interfejs OnInit. To jeden z ośmiu etapów zarządzania cyklem życia komponentu. O tzw. lifecycle hooks zrobimy niedługo osobny wpis. Na dzisiaj usuń tę implementację, wraz z metodą ngOnInit(). Chwilowo jej nie potrzebujesz.
export class TransactionListComponent { private readonly _transactions: Transaction[]; public constructor() { } public get transactions(): Transaction[] { return this._transactions; } }
TransactionListComponent
powinien wiedzieć jakie transakcje wyświetla. Dlatego stworzyłem prywatne pole _transactions
, w którym tę informację za chwilę zapiszemy. Dodatkowo pole jest readonly
(pamiętasz po co?). Dostęp do niego możesz uzyskać tylko w obrębie klasy TransactionListComponent
. Stworzyłem publiczny getter żeby umożliwić odwoływanie się do listy transakcji z poziomu szablonu. Zwraca on wartość przypisaną do tablicy _transactions
. Dzięki temu możesz odwoływać się do pola z zewnątrz, ale nie dasz rady tej wartości zmienić!
Czas na szablon
<table mat-table [dataSource]="transactions"> <ng-container matColumnDef="beneficiaryName"> <th mat-header-cell *matHeaderCellDef></th> <td mat-cell *matCellDef="let element"> {{element.account.name}}</td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table>
Po zdefiniowaniu kolumn musisz powiedzieć tabeli, które kolumny ma renderować w nagłówku i wierszach danych. Robisz to dyrektywami *matHeaderRowDef
i *matRowDef.
W komponencie tworzysz zmienną zawierającą listę kolumn do wyrenderowania:
export class TransactionListComponent { ... public readonly displayedColumns: ColumnType[] = [ColumnType.BeneficiaryName]; ... }
Klucze odpowiadają poszczególnym kolumnom:
enum ColumnType { BeneficiaryName = 'beneficiaryName' }
Na ten moment wyświetlimy tylko nazwy kont, na które były dokonywane przelewy.
Kiedy kolumn będzie więcej będziesz w stanie manipulować ich kolejnością, a nawet dynamicznie usuwać/dodawać kolumny zmieniając właśnie tablicę kluczy displayedColumns
przesyłaną do wierszy.
Mały trik
W tej chwili za każdą zmianą klucza musisz pamiętać o update szablonu. Przydałoby się jakoś powiązać ze sobą ColumnType
i matColumnDef
. W tym celu wprowadzam dodatkową zmienną w klasie komponentu:
export class TransactionListComponent { ... public readonly columnTypes = ColumnType; ... }
Teraz możesz odwoływać się do wartości enuma bezpośrednio z szablonu:
<ng-container matColumnDef={{columnTypes.BeneficiaryName}}> <th mat-header-cell *matHeaderCellDef></th> <td mat-cell *matCellDef="let element"> {{element.account.name}}</td> </ng-container>
Fajne nie? Często się przydaje, jeżeli lubisz reusability.
Jeszcze trochę o matCellDef
Dyrektywa matCellDef
powinna mieć dostęp do danych klasy, które ma wyświetlić. To właśnie tam masz swoją listę transakcji do zaprezentowania na stronie. W tym przypadku widzisz zapis *matCellDef=”let element”. W połączeniu z [dataSource]=”transactions” dla każdego elementu w tablicy transactions angular stworzy nową kolumnę. Następnie odwoła się do elementu, jak do zwykłej propercji za pomocą interpolacji: {{element.account.name}}.
Ot wszystko! Wyświetlasz już nazwę każdego konta, na które robiłeś przelew. BRAWO!
No wszystko fajnie, ale nic mi się nie wyświetla…
Czas to zmienić. Pytanie jak? Ano, jak najszybciej 🙂 Potem to nieco zmodyfikujemy, ale na dziś wprowadźmy bezpośrednią zależność pomiędzy TransactionListComponent
i TransactionRepositoryService
zdefiniowanym w poprzednim wpisie.
Najpierw stwórzmy metodę, która pobierze i zwróci historię z bazy danych. We wspomnianym wpisie uzgodniliśmy, że łącznikiem między naszym kodem, a bazą danych będzie właśnie TransactionRepositoryService
. Dopiszmy więc tę metodę tam gdzie należy:
public fetchTransactions(): Transaction[] { let entities: ITransactionEntity[] = []; entities = this.database.get(); const transactions: Transaction[] = []; entities.forEach(entity => { const bankAccount = new BankAccount( entity.merchant.name, entity.merchant.accountNumber ); const singleTransaction = new Transaction( bankAccount, entity.transaction.amount, entity.date.valueDate ); transactions.push(singleTransaction); }); return transactions; }
Mega proste. Pobieramy transakcje z bazy danych. NIE OBCHODZI NAS IMPLEMENTACJA get()
! Wiemy tylko, że powinniśmy dostać listę transakcji. Tyle. Magia DI. Pod obiektem this.database
może kryć się cokolwiek, co ma metodę get()
, która zwróci historię transakcji.
Mając listę wykonanych dotychczas transakcji (z pliku .json) iterujemy po każdej z nich za pomocą pętli forEach(() => {}).
To, co zapiszesz w {}
wykona się dla każdego pojedynczego obiektu w tablicy, na której tę pętlę wołasz. Zapis ()=>{}
to inaczej wyrażenie funkcyjne, lub jak kto woli lambda.
Ok, czyli dla każdej transakcji tworzymy BankAccount
, a następnie sam obiekt Transaction
. Potem obiekt wrzucamy do listy, a kiedy pętla przejdzie po wszystkich pobranych transakcjach zwracamy tę listę do gościa, który woła metodę fetchTransactions()
. No właśnie, kto ją zawoła?
TransactionListComponent
export class TransactionListComponent { ... public constructor(transactionRepoService: TransactionRepositoryService) { this._transactions = transactionRepoService.fetchTransactions(); } ... }
I to wszystko! Podczas tworzenia komponentu ten od razu pyta bazę danych o listę dotychczasowych operacji. TYLKO TYLE I AŻ TYLE 🙂
Sprawdźmy, czy działa:
Elegancja Francja 🙂 To znaczy nie do końca elegancja, ale działa. Efekt zamierzony osiągnięty. Stylowanie kiedy indziej. To samo z lepsiejszym kodem 😉
Zaimportuj w AppModule i wyświetl tabelę
Dobra, trochę Cię nabrałem. Kod, który widzisz do tej pory jeszcze nie wyświetli tej tabelki ze screena. Pamiętaj o imporcie nowego modułu i osadzeniu komponentu w którymś z szablonów.
Ja to zrobiłem w app.component.html:
<app-transaction-form></app-transaction-form> <app-transaction-list></app-transaction-list>
I teraz tylko zaimportuj nowy moduł w AppModule
:
@NgModule({ declarations: [...], imports: [ ... ShowTransactionsModule], bootstrap: [...] }) export class AppModule { }
To tyle na dziś…
Jeżeli to czytasz to gratuluję wytrwałości 🙂 Niezła kobyła z tego wpisu wyszła. S’ry że tak dużo za jednym razem. Na przyszłość postaram się poprawić.
P.S. Linka do kodu źródłowego podsyłam w mailingu:)
P.S.2. Idea #start_angular to pomysł zapoznania Cię ze światem aplikacji webowych i programowania. Jeżeli chcesz otrzymywać do dostęp do kodu źródłowego, zadanie do wykonania po przeczytaniu każdego wpisu, listę zrealizowanych tematów oraz powiadomienia o nowych wpisach, dołącz proszę do mojego mailingu.