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/