Cześć! Przyszedł czas na spięcie wszystkich modułów razem. Na koniec dzisiejszego wpisu otrzymasz aplikację spełniającą podstawowe wymagania przedstawione tutaj i poznasz podstawy rxjs.
Stan aplikacji – rxjs
Ostatnia część naszej architektury to StateResolverModule
. Wbrew swojej nazwie nie będzie to moduł w kontekście angulara. Będzie to moduł w kontekście architektonicznym. Kawałek aplikacji odpowiedzialny za zarządzaniem stanem.
Co będzie tym stanem? Dodajemy transakcje, wyświetlamy listę transakcji, zarządzamy transakcjami. Wszystko oscyluje wokół tego terminu. BA! Nawet mamy już dedykowaną klasę Transactions
. Stan aplikacji możesz zatem wyrazić jako listę wykonanych przelewów. Odzwierciedlenie tego, co znajduje się w bazie danych (no akurat u nas jest mock, więc umówmy się, że tam coś realnego jest).
Gdzie to umieścić? Jak tym zarządzać? Jak przekazać do widoku? Pamiętasz definicję serwisów?
- wykonują pracę dla komponentów
- komunikują się z REST API
- zapewniają komunikację między komponentami
- pozwalają na stworzenie stanu aplikacji
No właśnie! Używając serwisu mamy dedykowaną klasę, która powinna tę funkcjonalność spełnić. W katalogu ./modules/state-resolve
r generuję ten serwis:
npm generate service state-resolver
Domyślny provider stworzył się w root’cie. I bardzo dobrze. Tylko jedna instancja klasy trzymającej aktualny stan aplikacji istnieje w czasie jej życia. Dodatkowo klasa ta jest przyzwoita i dostępna w całym kontekście. Nie w jakimś pojedynczym module. I tak niech zostanie.
Przygotujmy teraz ten serwis na trzymanie listy transakcji:
export class StateResolverService { private readonly _transactions: Transaction[] = []; public readonly transactionAdded$ = new Subject<Transaction[]>(); public addTransaction(payload: Transaction): void { this._transactions.push(payload); this.transactionAdded$.next(this._transactions); } }
Mega prosta klasa. SingleResposibilityPrinciple. Trzyma listę transakcji. Lista uzupełnia się po dodaniu transakcji. Ale zaraz… Taka klasa powinna też jakoś udostępnić tę listę, nie? A nie robi tego, bo lista jest private, nie ma gettera, nie ma w implementacji nic co udostępni ją na zewnątrz?
Tu wchodzi rxjs 😀 Zapis:
this.transactionAdded$.next(this._transactions);
Wrzuci aktualną listę tansakcji do strumienia. I stamtąd zainteresowani mogę tę listę odczytać.
Poznaj rxjs
rxjs bazuje na wzorcu projektowym obserwator. To jedna z bibliotek npm służąca do tzw. programowania reaktywnego. Co to jest programowanie reaktywne? Idea jest taka, że zamiast tworzyć kolejne zależności między klasami możesz zmienić koncepcję!
rxjs daje Ci możliwość myślenia o tym co dzieje się w Twojej aplikacji za pomocą zdarzeń, czyli eventów. Załóżmy, że coś się stało (np. button został wciśnięty przez usera). W podejściu reaktywnym w świat powinien zostać wysłany event, który odbiorą zainteresowani. Gość, który powoduje emisję tego eventu (w tym przypadku komponent, w którym jest ten button) nic nie wie o zainteresowanych odbiorcach.
On tylko krzyczy ‚HEJ WCISNĄŁ GUZIK!’. Odbiorca, czyli inny kawałek kodu (nic niewiedzący o tym komponencie z buttonem), ale zapisany na event brzmiący ‚HEJ WCISNĄŁ GUZIK!’ otrzyma to zdarzenie i wie jak zareagować.
Cała idea jest mega prosta. Gość, który chce krzyczeć coś światu definiuje sobie Subject, czyli otwiera strumień. Taki temat komunikatu. Później, chcąc wykrzyczeć już światu ten komunikat wywołuje na tym subject’cie metodę next().
Odbiorca, który chce otrzymywać te komunikaty zapisuje się na nie poprzez subscribe(),
co zrobimy za chwilę. Od teraz za każdym zawołaniem clicked$.next()
wywoła się funkcja doSomething()
.
Dzisiaj zapamiętaj jeszcze tylko, że każdy kto zapisuje się na komunikaty powinien gdzieś się z nich odpisywać. Inaczej prowadzi to do występowania w Twoim kodzie wycieków pamięci (sic!).
To tyle z podstaw. Taki jest schemat działania rxjs. Oczywiście biblioteka jest bardziej rozbudowana, ale w tym wpisie chodzi przecież o wyjaśnienie podstaw 🙂
Spinamy moduły razem
Teraz wstrzyknij nasz nowy serwis do TransactionRepositoryService
, a następnie zaktualizuj metody addTransaction()
i fetchTransactions()
. Powinny wołać update stanu StateResolverService
za każdym razem, gdy w systemie pojawi się nowa transakcja.
export class TransactionRepositoryService { public constructor( @Inject("IDatabaseConnetion") private readonly database: IDatabaseConnetion, private readonly stateResolverService: StateResolverService ) {} … }
public addTransaction(payload: Transaction): Transaction { const entity = this.convertTransactionToEntity(payload); // Simulating success const success = this.database.create(entity); if (success) { this.stateResolverService.addTransaction(payload); } return payload; }
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 ); this.stateResolverService.addTransaction(singleTransaction); }); return transactions; }
Tyle! Od teraz dodanie nowej transakcji spowoduje wysłanie w świat komunikatu z zawartością zaktualizowanej listy.
Wyświetlanie aktualnej listy
StateResolverService zarządza stanem aplikacji, więc masz do niego dostęp z każdego jej modułu. Dlatego użycie go w komponencie listy, nie będzie wykroczeniem.
export class TransactionListComponent { … public constructor(transactionRepoService: TransactionRepositoryService, private readonly stateResolverService: StateResolverService) { … } … }
StateResolverService
wstrzykuję z użyciem private readonly. Dzięki temu deklaruję zmienną stateResolverService
jako pole klasy. Użycie samej nazwy argumentu i typu spowoduje traktowanie tej zmiennej jako parametru funkcji konstruktora. Przez to pole nie będzie zdefiniowane w klasie. Tak jest w przypadku transactionRepoService
.
Teraz mały update samej listy trzymanej w komponencie:
export class TransactionListComponent { private _transactions = new MatTableDataSource<Transaction>([]); … public get transactions(): MatTableDataSource<Transaction> { return this._transactions; } }
MatTableDataSource
Zauważ zmianę typu ze zwykłej tablicy Transaction[]
na MatTableDataSource<Transaction>
. Korzystamy z MatTableDesign, więc określając typ źródła danych musimy skorzystać z odpowiednio przygotowanej do tego klasy. Pozostało poinformowanie angulara, że aktualizacja tego pola powinna znaleźć odzwierciedlenie w szablonie.
To robisz w dwóch krokach. Pierwszy mamy już za sobą, czyli w szablonie tego komponentu wpisaliśmy [dataSource]="transactions"
przy deklaracji tabeli. Jednak to nie wystarczy do pełnej funkcjonalności.
Żeby tabela aktualizowała się o nowe wartości za każdą zmianą w polu _transactions
musisz jeszcze powiedzieć komponentowi, że pole to odpowiedzialne jest za skład tabeli.
Intuicja podpowiada, że po każdej zmianie stanu powinniśmy przypisać nową wartość tabeli do _transactions
, o tak:
this._transactons = newValue;
Biorąc pod uwagę fakt, że update stanu działa w oparciu rxjs powinniśmy zapisać się na niego w komponencie za pomocą subscribe():
export class TransactionListComponent { … public constructor(transactionRepoService: TransactionRepositoryService, private readonly stateResolverService: StateResolverService) { transactionRepoService.fetchTransactions(); this.stateResolverService.transactionAdded$.subscribe( (updatedTransactionList) => { this._transactions = updatedTransactionList; }); } … }
Ten kod jednak nie zadziała. Nie będzie w stanie zaktualizować widoku, pomimo, że aktualizuje tablicę o nową wartość. Brakuje jednego elementu…
this._transactons.data = newValue;
Teraz będzie git! Korzystamy z MatTable, więc nie operujemy bezpośrednio na wartości tabeli. Używamy źródła jej danych i to właśnie te dane mamy zmieniać przy zmianie zawartości tabeli.
3, 2, 1… działa?
Cały kod komponentu wygląda tak:
export class TransactionListComponent { private _transactions = new MatTableDataSource<Transaction>([]); public readonly columnTypes = ColumnType; public readonly displayedColumns: ColumnType[] = [ColumnType.BeneficiaryName]; public constructor(transactionRepoService: TransactionRepositoryService, private readonly stateResolverService: StateResolverService) { transactionRepoService.fetchTransactions(); this.stateResolverService.transactionAdded$.subscribe( (updatedTransactionList) => { this._transactions.data = updatedTransactionList; }); } public get transactions(): MatTableDataSource<Transaction> { return this._transactions; } }
Od teraz pole _transactions
nie jest już readonly. Jego wartość aktualizuje się za każdym razem, gdy nastąpi zmiana stanu w StateResolverService
.
Mały tip: Dobrą praktyką jest tworzenie dedykowanego serwisu dla komponentu. Klasa komponentu zarządza tylko wyświetlaniem danych, a ich zmianą zajmuje się ten serwis. SingleResponsibility. Na dziś zostawmy jednak kod takim, jakim jest.
Możesz odpalić aplikację. Od teraz po wypełnieniu i zatwierdzeniu formularza zobaczysz nową transakcję na liście. Główna funkcjonalność spełniona 🙂 Można iść do domu…
Ostania kwestia do omówinia to powstały memory leak. Wycieki pamięci mogą przysporzyć nie małych problemów w przyszłości, dlatego lepiej od razu im zapobiegać. Zarządzanie pamięcią to jednak temat na osobny wpis. Dzisiaj zapamiętaj jedynie, że w przypadku, gdy nie używasz już danej subskrypcji i nie chcesz otrzymywać więcej updatów stanu musisz gdzieś się z nich wypisać. Robisz to metodą unsubscribe()
na obiekcie subskrypcji zwracanym z subscribe().
Kod nie jest idealny i wymaga poprawy. Nie da się tłumaczyć podstaw od razu klepiąc kod wysokiej jakości. Od czegoś trzeba zacząć 🙂 Będzie tylko lepiej! Czyściej. Przejrzyściej. I tak dalej, i dalej…
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.