Wprowadzenie
W tym poście wytłumaczę jak zbudować prostą galerię Pokemonów, wykorzystując nowy przepływ sterowania (control flow) w Angular 17. Nowy przepływ sterowania jest nową cechą wersji 17 Angulara, która wprowadza nowe, zintegrowane bloki do warunkowego pokazywania i ukrywania elementów, a także renderowania listy elementów w szablonach HTML. Celem tych bloków jest zamiana obecnie stosowanych strukturalnych dyrektyw, i sprawienie, by pisanie kodu HTML było bardziej intuicyjne. Ponadto, bloki te są built-in; oznacza to, że developerzy Angulara nie muszą nic importować do komponentów samodzielnych (standalone).
Nowy przepływ sterowania | Odpowiadające strukturalne dyrektywy | Funkcja |
@if, @else if oraz @else | NgIf, NgElse i NgTemplate | Pokazania i ukrycie komponentu pod danym warunkiem |
@for, @empty | NgFor | Iterowanie tablicy danych, z uwzględnieniem przypadku, w którym tablica jest pusta |
@switch, @case oraz @default | NgSwitch, NgSwitchCase i NgSwitchDefault | Porównanie wartości z określonymi przypadkami i domyślny przypadek, jeśli żaden z warunków nie zostanie spełniony |
Opis demo
W tym demo, galeria Pokemonów pokazuje 300 Pokemonów na przestrzeni 10 stron, czyli 30 Pokemonów na stronę. Widok listy wykorzystuje flexboxa, żeby rozstawić karty Pokemonów, a każda karta zawiera id, imię, wagę i wzrost Pokemona. Kiedy użytkownik kliknie na imię Pokemona, aplikacja przenosi go na stronę szczegółów Pokemona, aby wyświetlić konkretne atrybuty.
Definiowanie ścieżek
Po pierwsze, zdefiniowałam ścieżki, aby móc nawigować do PokemonListComponent (lista pokemonów) i PokemonComponent (szczegóły pokemona)
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 |
// app.routes.ts export const routes: Routes = [ { path: 'list', loadComponent: () => import('./pokemons/pokemon-list/pokemon-list.component') .then((m) => m.PokemonListComponent), title: 'Pokemon List' }, { path: 'list/pokemon/:id', loadComponent: () => import('./pokemons/pokemon/pokemon.component') .then((m) => m.PokemonComponent), title: 'Pokemon Details' }, { path: '', pathMatch: 'full', redirectTo: '/list?page=1', }, { path: '**', redirectTo: '/list?page=1', } ]; |
Po drugie, przekazuję ścieżki do funkcji provideRouter z flagą withComponentInputBinding.
1 2 3 4 5 6 7 8 |
// app.config.ts export const appConfig: ApplicationConfig = { providers: [ provideHttpClient(), provideRouter(routes, withComponentInputBinding()) ] }; |
1 2 3 4 |
// main.ts bootstrapApplication(AppComponent, appConfig) .catch((err) => console.error(err)); |
Wyświetlenie listy pokemonów w PokemonListComponent
Aplikacja ma wyświetlić listę pokemonów w przeglądarce, zatem stworzyłam PokemonListComponent.
pokemons to sygnał, który zawiera tablicę pokemonów.
1 2 3 4 5 6 |
// pokemon-list.component.ts pokemons = toSignal( toObservable(this.currentPage).pipe(switchMap(() => this.pokemonListService.getPokemons())), { initialValue: [] as DisplayPokemon[] } ); |
W szablonie komponentu użyłam </span><b>@for
, aby przeiterować listę pokemonów, i słowa klucz </span><b>track
, które śledzi każdego pokemona po jego id. </span><b>@for
ma istotną zaletę nad </span><b>ngFor
, ponieważ parametr </span><b>track
jest wymagany. </span><b>Track by
może zwiększyć wydajność długiej listy, zawierającej wiele elementów.
1 2 3 |
@for (pokemon of pokemons(); track pokemon.id) { <app-pokemon-card [pokemon]="pokemon" /> } |
Tak jak w przypadku </span><b>ngFor
, w </span><b>@for
możemy wykorzystać zmienne niejawne.
1 2 3 4 5 6 7 8 9 10 11 12 |
@for (ability of abilities; track ability.name; let idx = $index) { <div class="abilities"> <label for="ability_name"> <span>{{ idx + 1 }}. Name: </span><span id="ability_name" name="ability_name">{{ ability.name }}</span> </label> <label for="ability_isHidden"> <span>Effort: </span><span id="ability_isHidden" name="ability_isHidden">{{ ability.isHidden ? 'Yes' : 'No' }}</span> </label> </div> } @empty { <p>No Ability</p> } |
W powyższej pętli, przypisuję zmienną $index do idx, aby wyświetlić liczby wierszy. Inne dostępne niejawne zmienne to $count, $first, $even oraz $odd.
Wyświetlenie szczegółowych informacji Pokemona w PokemonComponent
Kiedy użytkownik kliknie w hiperłącze na karcie pokemona, Angular przekieruje go do PokemonComponent, aby wyświetlić właściciela, umiejętności, statystyki i fizyczne atrybuty danego pokemona.
1 2 3 4 5 6 7 |
// pokemon.component.ts @if (pokemonDetails$ | async; as pokemonDetails) { <app-pokemon-physical [pokemonDetails]="pokemonDetails" /> <app-pokemon-statistics [statistics]="pokemonDetails.stats" /> <app-pokemon-abilities [abilities]="pokemonDetails.abilities" /> } |
pokemonDetails$ to Observabla pokemona, a </span><b>@if
wydobywa z niej wartość, którą przypisuje do zmiennej pokemonDetails. Następnie pokemonDetails jest przekazana jako dana wejściowa kolejno dla komponentów PokemonPhysicalComponent, PokemonStatisticsComponent i PokemonAbilitiesComponent.
Wyświetlenie atrybutów fizycznych, statystyk i umiejętności Pokemonów
PokemonComponent składa się z PokemonPhysicalComponent, PokemonStatisticsComponent, i PokemonAbilitiesComponent. Zarówno PokemonStatisticsComponent, jak i PokemonAbilitiesComponent wykorzystują @for/@empty, aby wyświetlić kolejno listę statystyk i umiejętności.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// pokemon-statistics.component.ts @for (stat of statistics; track stat.name) { <div class="stats"> <label for="stat_name"> <span>Name: </span><span id="stat_name" name="stat_name">{{ stat.name }}</span> </label> <label for="stat_effort"> <span>Effort: </span><span id="stat_effort" name="stat_effort">{{ stat.effort }}</span> </label> <label for="stat_baseStat"> <span>Base Stat: </span><span id="stat_baseStat" name="stat_baseStat">{{ stat.baseStat }}</span> </label> </div> } @empty { <p>No statistics</p> } export class PokemonStatisticsComponent { @Input({ required: true }) statistics!: Statistics[]; } |
Jeśli tablica statystyk nie jest pusta, blok @for
iteruje ją, aby wyświetlić informacje każdego elementu. W innym przypadku, blok @empty
wyświetla tekst “No Statistics”. track stat.name świadczy o tym, że elementy są śledzone po nazwie, ponieważ nazwa atrybutu jest unikalna dla każdego Pokemona.
W podobny sposób, block @for/@empty wyświetla specjalne umiejętności Pokemona. Pokemon nie zawiera zduplikowanych umiejętności, a zatem jest to unikalny klucz dla każdego wiersza umiejętności. Jeśli Pokemon nie ma żadnych umiejętności, blok @empty
wyświetla tekst „No Ability”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// pokemon-abilities.component.ts @for (ability of abilities; track ability.name) { <div class="abilities"> <label for="ability_name"> <span>Name: </span><span id="ability_name" name="ability_name">{{ ability.name }}</span> </label> <label for="ability_isHidden"> <span>Effort: </span><span id="ability_isHidden" name="ability_isHidden">{{ ability.isHidden ? 'Yes' : 'No' }}</span> </label> </div> } @empty { <p>No Ability</p> } export class PokemonAbilitiesComponent { @Input({ required: true }) abilities!: Ability[]; } |
Na końcu użyłam </span><span style="font-weight: 400;">@switch
, aby wyświetlić właściciela kilku ważniejszych Pokemonów. Pikachu, Stayyu, Steelix i Meowth są dosyć znane w serialu animowanym, a ich właściciele to albo protagoniści, albo antagoniści. Kiedy typ znanego pokemona wpada w jeden ze zdefiniowanych przypadków switch, customowy pipe ‘affiliation’ (przynależność) wyświetla jego właściciela. Jeśli typ pokemona jest nieznany, to jest ma wartość nieprzewidzianą przez żaden przypadek, przypadek „unknown” wyświetla tekst “Your team is unknown”. Przypadek @default
nie powinien nigdy zostać rozpatrzony, ponieważ przypadek „unknown” obejmuje wszystkie typy pokemonów, które grają mniejsze role w serialu.
1 2 3 4 5 6 7 8 9 10 11 |
// affiliation.pipe.ts @Pipe({ name: 'affiliation', standalone: true }) export class AffiliationPipe implements PipeTransform { transform(name: string, team: string): string { return `${name} is in Team ${team}.`; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
export type PokemonAffiliation = { type: 'pikachu', owner: 'Ash', } | { type: 'meowth', owner: 'Rocket', } | { type: 'staryu', owner: 'Misty', } | { type: 'steelix', owner: 'Brock', } | { type: 'unknown', warningMessage: 'Your team is unknown', } |
Jeśli wartość affiliation.type to pikachu, meowth, staryu albo steelix, PokemonAffiliation zawęża typ pokemona do obiektu z odpowiednim właścicielem. Przekazuję affiliation.owner jako parametr customowego pipe’a, aby zrenderować jego imię. Jeśli typ pokemona jest nieznany, PokemonAffiliation zawęża typ do obiektu z właściwością warningMessage, której wartością jest tekst „Your team is unknown”. Możemy zastosować takie podejście, ponieważ @switch
potrafi zawężać typy w szablonach HTML.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// pokemon-affiliation.component.ts @switch (affiliation.type) { @case ('pikachu') { <p>{{ affiliation.type | affiliation:affiliation.owner }}</p> } @case ('meowth') { <p>{{ affiliation.type | affiliation:affiliation.owner }}</p> } @case ('staryu') { <p>{{ affiliation.type | affiliation:affiliation.owner }}</p> } @case ('steelix') { <p>{{ affiliation.type | affiliation:affiliation.owner }}</p> } @case ('unknown') { <p>{{ affiliation.warningMessage }}</p> } @default { <p>This should not appear</p> } } export class PokemonAffliationComponent { @Input({ required: true }) affiliation!: PokemonAffiliation; } |
To wszystko! Stworzyłam prostą galerię pokemonów używając nowy przepływ sterowania, aby wyświetlić listę pokemonów. Nowy przepływ sterowania jest bardziej intuicyjny w użytku niż strukturalne dyrektywy. Ponadto, jego składnia jest prostsza w nauce i zapamiętaniu, niż jej obecne odpowiedniki. NgIf, ngSwitch i ngFor nie zostają całkowicie usunięte, jednak przewiduję, że nowy przepływ sterowania będzie od nich częściej widziany w szablonach HTML w Angularze 17 i jego następnych wersjach.
To koniec tego posta. Mam nadzieję, że spodobała się wam jego zawartość, i że dalej będziecie śledzić moje doświadczenie w nauce Angulara i innych technologiach.
Bibliografia:
- Github Repo: https://github.com/railsstudent/ng-new-control-flow-demo
- Strona Github: https://railsstudent.github.io/ng-new-control-flow-demo/list?page=1
- Angular New Control Flow: https://angular-dev-site.web.app/guide/templates/control-flow
- Angular Team Updates: https://www.youtube.com/watch?v=QrEH53tSUf0&t=1684s
Bardzo fajne demo. Dzięki Connie! 🙂
..i tylko taka uwaga: warto sprawdzić co się dzieje z edytorem tekstowym, że do większości inline-code dodaje :/
oraz rozjeżdżają się kolory w sekcjach z kodem