19 wrz 2023
6 min

Angular Storybook

Component Driven User Interfaces (CDUI) to podejście projektowania i tworzenia interfejsów użytkownika, które skupia się na budowaniu aplikacji poprzez połączenie małych, samodzielnych komponentów. W CDUI, każdy element UI jest traktowany jako oddzielny komponent, który może być projektowany, testowany i rozwijany niezależnie od reszty aplikacji. Głównym celem CDUI jest zwiększenie modularności i skalowalności aplikacji, a także ułatwienie tworzenia spójnego i łatwego do utrzymania interfejsu użytkownika. Poprzez budowanie aplikacji z mniejszych, samodzielnych elementów, zespoły projektowe mogą łatwiej przystosować aplikację do zmieniających się wymagań biznesowych, a także szybciej reagować na ewentualne błędy i problemy. 

Więcej o CDUI:  https://www.componentdriven.org/ 

Storybook:

Storybook to narzędzie do budowania, prezentowania i testowania komponentów interfejsu użytkownika (UI) w izolacji od reszty aplikacji. Umożliwia programistom, projektantom i testerom pracę na poszczególnych elementach UI w odizolowanym środowisku bez konieczności uruchamiania całej aplikacji.

src: https://prateeksurana.me/blog/react-component-library-using-storybook-6/

Storybook stanowi też interaktywną dokumentację komponentów, która może być łatwo udostępniona i wykorzystywana przez całe zespoły projektowe. W ramach Storybooka można tworzyć różne wersje i warianty komponentów, zasilać je różnymi danymi oraz testować je w różnych przypadkach brzegowych, co pozwala na łatwe debugowanie i wykrywanie błędów.

Jednym z kluczowych problemów, które rozwiązuje Storybook, jest zmniejszenie czasu potrzebnego na rozwój aplikacji. Dzięki możliwości pracy na pojedynczych komponentach w izolacji, programiści i projektanci mogą skupić się na rozwoju poszczególnych elementów, bez konieczności ciągłego uruchamiania całej aplikacji.

Zanim zaczniemy…

Aby w angularowym (i każdym innym) projekcie móc w pełni skorzystać z podejścia CDUI z wykorzystaniem Storybooka należy zadbać o wydzielenie interfejsu użytkownika jako osobnej (możliwie niezależnej) warstwy w aplikacji. Jednym ze sposobów na osiągnięcie tego jest wprowadzenie podziału komponentów na te odpowiedzialne za warstwę prezentacyjną (presentational components) oraz te odpowiedzialne za routing i logikę biznesową (container components). 

Więcej o takim podziale: https://blog.angular-university.io/angular-2-smart-components-vs-presentation-components-whats-the-difference-when-to-use-each-and-why/

Dla porządku warto w kodzie źródłowym wprowadzić konwencje porządkujące ten podział:

  • nazewnictwo komponentów (np. wszystkie kontenery oznaczać postfixem *-container.component.ts),
  • struktura katalogów (oba typy komponentów umieszczać w różnych odpowiednio nazwanych katalogach, np. containers i components),
  • struktura modułów (dla reużywalnych komponentów prezentacyjnych tworzyć osobne moduły/biblioteki, niezależne od reszty aplikacji),
  • osobne interfejsy dla komponentów prezentacyjnych (unikanie korzystania bezpośrednio z referencji na modele biznesowe. Niezależny interfejs dla każdego komponentu można umieścić w osobnym pliku *-foo.interface.ts zaraz obok pliku z klasą komponentu *-foo.component.ts.  Interface można również poprzedzić prefixem Ui-*, np. UiUserDetails). 

Oto przykładowa struktura zawierająca folder grupujący moduły z warstwy UI, a w nim moduł user-details zawierający komponent z interfejsem wydzielonym do osobnego pliku. W zależności od wielkości projektu struktura może być uproszczona lub bardziej złożona.

Dodanie Storybooka i pierwsze story:

Dla istniejącego projektu wystarczy uruchomić prostą komendę (znajdując się w głównym katalogu projektu):
npx storybook@latest init

Dzięki temu zainstalujemy wszystkie zewnętrzne zależności, wygenerujemy pliki konfiguracyjne, dodamy przydatne skrypty w package.json a nawet utworzymy przykładowe storybook stories. 

Po uruchomieniu komendy npm run storybook w konsoli zobaczmy mniej więcej taki output:

Pod wskazanym adresem zobaczymy uruchomionego lokalnie Storybooka, wraz z live-reloadem (każda modyfikacja komponentu spowoduje odświeżenie go w Storybooku w przeglądarce w czasie rzeczywistym).

Wróćmy teraz do naszego komponentu ‘user-details’ i rzućmy okiem na jego prostą implementację oraz przygotowany w osobnym pliku interfejs.

// user-details.interface.ts 

export interface UiUserDetails {
 firstName: string;
 lastName: string;
 email: string;
 avatar: {
   url: string;
   alt: string;
 } | null;
}

// user-details.component.ts

import { Component, Input } from "@angular/core";
import { UiUserDetails } from "./user-details.interface";


@Component({
 selector: "app-user-details",
 templateUrl: "./user-details.component.html",
 styleUrls: ["./user-details.component.scss"]
})
export class UserDetailsComponent {
 @Input() user?: UiUserDetails;
}

Dla takiego komponentu utwórzmy tuż obok nowy plik user-details.stories.ts

import type { Meta, StoryObj } from "@storybook/angular";
import { UserDetailsComponent } from "./user-details.component";
import { UiUserDetails } from "./user-details.interface";


// story meta config
const meta: Meta<UserDetailsComponent> = {
 title: "User/UserDetails",
 component: UserDetailsComponent
};
export default meta;


// mocks
const userMock: UiUserDetails = {
 firstName: "John",
 lastName: "Smith",
 email: "john.smith@abc.efd",
 avatar: {
   url: "https://placehold.it/100x100",
   alt: "John Smith avatar"
 }
};


// stories
type UserDetailsStory = StoryObj<UserDetailsComponent>;


export const primary: UserDetailsStory = {
 args: {
   user: userMock
 }
};

Przygotowanie obiektu meta, powiązanie go z konkretnym ui-komponentem i wyeksportowanie jako wartość domyślna pliku pozwoli nam wyświetlić różne warianty komponentu w Storybooku. W dalszej części zdefiniowaliśmy demonstracyjną wartość dla inputu user (tzw. mock), a w ostatnim kroku zdefiniowaliśmy konkretną pojedynczą story z konkretnymi danymi. Wszystko to razem już teraz pozwala nam podejrzeć gotowy komponent w Storybooku (bez uruchamiania angularowej aplikacji!):

Nieco interakcji:

Powyżej zaprezentowany został najprostszy możliwy use-case Storybooka, jednak samo narzędzie pozwala na o wiele więcej. Rozszerzmy nasz demonstracyjny komponent o dodatkowy parametr (input) notificationCount, który będzie wyświetlał liczbę oczekujących na użytkownika powiadomień (o ile jest ona większa, niż 0):

import { Component, Input } from "@angular/core";
import { UiUserDetails } from "./user-details.interface";


@Component({
 selector: "app-user-details",
 templateUrl: "./user-details.component.html",
 styleUrls: ["./user-details.component.scss"]
})
export class UserDetailsComponent {
 @Input() user?: UiUserDetails;
 @Input() notificationCount = 0;
}

Tym razem zamiast hardcodować w pliku user-details.stories.ts kolejną demonstracyjną wartość skorzystamy z dostępnych w Storybooku kontrolek, które pozwolą dynamicznie zmieniać przekazywane do komponentu wartości.

Nasz obiekt meta należy rozbudować o dodatkowy atrybut zawierający konfigurację interaktywnych kontrolek:

const meta: Meta<UserDetailsComponent> = {
 title: "User/UserDetails",
 component: UserDetailsComponent,
 argTypes: {
   notificationCount: {
     options: [0, 1, 9, 15, 99, 123, 999, 2317],
     defaultValue: 0,
     control: { type: "radio" }
   }
 }
};
export default meta;

Dzięki temu pozwolimy w sposób interaktywny przetestować nasz komponent dla różnych możliwych przypadków brzegowych (i bardzo szybko wykryć potencjalne błędy, jak np. ucinanie zbyt długich wartości):

W prawdziwym życiu i prawdziwych projektach nasze komponenty bywają oczywiście dużo bardziej skomplikowane. Czasem konieczne może być zaimportowanie w obiekcie meta dodatkowych zależności, zamockowanie jakichś providerów czy stworzenie wielu osobnych wariantów historyjek dla pojedynczego komponentu. Trzymając się jednak wspomnianych wcześniej zasad dot. wydzielania komponentów UI jako osobnej warstwy tworzenie i utrzymywanie Storybooka dla dowolnych komponentów jest to po prostu szybkie i wygodne. 

Kod źródłowy do pokazanego powyżej przykładu znajduje się tu:
https://github.com/Herdu/storybook-demo

Chromatic:

Chromatic to platforma, która pozwoli nam wycisnąć ze Storybooka to co najlepsze. Dzięki temu narzędziu w bardzo prosty sposób wykonamy następujące rzeczy:

  • udostępnimy gotowy katalog komponentów reszcie zespołu (w tym osobom nietechnicznym). Dajemy możliwość dodawania komentarzy, przez co mocno skrócimy feedback loop,
  • przeprowadzimy zautomatyzowane testy regresji wizualnej,
  • przetestujemy zachowanie komponentów w różnych przeglądarkach,
  • automatycznie utworzymy historię zmian UI,
  • zintegrujemy wszystko z procesami CI/CD.

    src: https://www.chromatic.com/docs/


    Dodanie Chromatica do projektu i korzystanie z niego jest proste (po więcej szczegółów odsyłam na oficjalną stronę:
    https://www.chromatic.com/), a korzyści z niego płynące wynagradzają wysiłek włożony w Storybooka.

Podsumowanie:

Przedstawione narzędzia i koncepcje pozwalają wdrożyć koncepcję CDUI w życie. Tworzenie warstwy interfejsu użytkownika może stać się zadaniem odrębnym, niezależnym, łatwym do wydelegowania innemu programiście, czy nawet innemu zespołowi. Sama praca nad komponentami staje się dużo szybsza i wygodniejsza, a testowanie przypadków brzegowych jeszcze prostsze (zarówno w momencie tworzenia, jak i w późniejszych etapach rozwoju).

Storybook, nawet w najprostszej formie, stanowi zestaw działających przykładów użycia każdego UI komponentu. Korzystając z dodatkowych rozszerzeń (np. https://storybook.js.org/addons/@storybook/addon-docs) możemy bez dodatkowej konfiguracji wygenerować pełną dokumentację naszego katalogu komponentów. 

Stawiając na reużywalność zapewnimy spójność wyglądu i zachowania naszej aplikacji. Idąc krok dalej możemy zbudować bibliotekę komponentów współdzieloną między wieloma projektami (nasze ui-komponenty mają przecież własne interfejsy, nie zależą od kontekstu biznesowego konkretnej aplikacji).  

Źródła:
"Angular UI Design Patterns & Storybook" Angular Warsaw | #5 Angular Meetup https://www.youtube.com/watch?v=PqIYMylxXCY

Dokumentacja Storybook’a https://storybook.js.org/docs/angular/

Dokumentacja Chromatic’a: https://www.chromatic.com/

Najczęściej zadawane pytania

Jak zainstalować Storybooka w istniejącym projekcie Angular?

Dla istniejącego projektu Angular uruchom w głównym katalogu polecenie npx storybook@latest init. Zainstaluje ono zewnętrzne zależności, wygeneruje pliki konfiguracyjne, dododa przydatne skrypty do package.json i utworzy przykładowe stories. Następnie npm run storybook uruchomi Storybooka lokalnie z live-reloadem, więc każda zmiana komponentu odświeża się w przeglądarce w czasie rzeczywistym.

Jak napisać story dla komponentu Angular w Storybooku?

Story tworzysz w pliku *.stories.ts umieszczonym obok komponentu. Najpierw definiujesz obiekt Meta<T> z polami title i component, a następnie eksportujesz stories typu StoryObj<T> z właściwością args. Przykładowo args: { user: userMock } przekazuje dane testowe do inputu user. Każda wyeksportowana story pojawia się w Storybooku jako osobny wariant komponentu, bez uruchamiania całej aplikacji.

Jak dodać kontrolki do inputów komponentu w Storybooku?

Dodaj atrybut argTypes do obiektu meta, definiując dla inputu listę options oraz typ kontrolki, np. control: { type: 'radio' }. Wtedy w Storybooku pojawi się interaktywna kontrolka, która zmienia przekazywaną wartość bez edycji kodu. Pozwala to szybko sprawdzić przypadki brzegowe, na przykład czy zbyt długie wartości liczbowe nie są ucinane przez komponent.

Czym różni się komponent prezentacyjny od kontenera w Angularze?

Komponent prezentacyjny (presentational) tylko wyświetla dane przekazane przez @Input() i nie zajmuje się routingiem ani logiką biznesową, natomiast komponent kontenerowy (container) odpowiada za routing i logikę. Taki podział izoluje warstwę UI, dzięki czemu komponenty prezentacyjne można łatwo rozwijać i testować w Storybooku. Dobrą praktyką jest oznaczanie kontenerów postfixem *-container.component.ts.

Czym jest Chromatic i co daje w połączeniu ze Storybookiem?

Chromatic to platforma współpracująca ze Storybookiem, która udostępnia katalog komponentów całemu zespołowi, także osobom nietechnicznym, i pozwala dodawać do nich komentarze. Automatyzuje testy regresji wizualnej, sprawdza zachowanie komponentów w różnych przeglądarkach, prowadzi historię zmian UI i integruje się z procesami CI/CD.

Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.