Javascript

Kurs Angular 2 – Wiele komponentów, transkluzja

W poprzednim wpisie skupiliśmy się na jednym komponencie będącym korzeniem naszej aplikacji. Takie podejście w żadnym przypadku nie ma najmniejszego sensu. Dzisiaj z kilku mniejszych jednostek stworzymy jedną, spójną całość.

Celem – prosty dashboard!

Aby nauczyć się tego jak tworzyć i łączyć ze sobą poszczególne komponenty, stworzymy aplikację, która docelowo ma wyglądać tak, jak na poniższym zrzucie ekranu:

Dashboard - podgląd

Z powyższego szablonu w ramach ćwiczeń wyodrębnimy – header, footer, card, main i activities-list. Aby maksymalnie ograniczyć tworzenie stylów CSS, wykorzystamy bibliotekę Materialize i dostosujemy ją do swoich potrzeb. Jedyna funkcjonalność, jaką oferować ma nasza aplikacja to możliwość dodawania nowych kart.

Tworzymy pierwszy komponent

Na pierwszy ogień wybierzemy header. Dzięki Angular CLI, utworzenie komponentu ogranicza się jedynie do wywołania następującej komendy z linii wiersza poleceń:


ng g component header

Gdzie g oznacza generate (można stosować zamiennie). Po zakończeniu wykonywania polecenia, w app domyślnie powinien utworzyć nam się folder header z następującą zawartością:


header.component.ts
header.component.spec.ts
header.component.css
header.component.html

Selektor naszego komponentu to <app-header>. Został on dopisany również na listę declarations głównego modułu aplikacji, ale o tym przy okazji omawiania Modules. Nadszedł czas na utworzenie szablonu, tak więc zakasamy rękawy i do dzieła:


<nav>
  <a href="#"><img src="assets/images/logo.svg" alt="Logo" /></a>
  <ul id="nav-mobile" class="right">
    <li>
      <a href="">Activities <span class="new badge">3</span></a></li>
    <li><a href=""><i class="material-icons">more_vert</i></a></li>
  </ul>
</nav>

Kiedy template jest już gotowy, możemy swobodnie wrzucić nasz selektor do głównego szablonu app.component.html:


<app-header></app-header>

Spowoduje to pojawienie się naszego nowego komponentu header na stronie głównej.

Stateless components

W trakcie budowania większych aplikacji, zauważymy, że zadaniem części naszych komponentów jest jedynie ich odpowiednie wyrenderowanie. Przykładem takich komponentów mogą być typowe kontenery, które odpowiadają za układ szablonu fragmentu strony. Takie komponenty określamy jako stateless components albo komponenty prezentacyjne (z ang. presentional).

Nasz dashboard na ten moment posiada tylko jedną funkcjonalność, a jej implementacja pojawia się w komponencie main. Pozostałe – footer, card i activities-list to typowe komponenty prezentacyjne. Uznałem, że w konsekwencji nie ma sensu umieszczać tutaj kodu HTML każdego z nich – można je swobodnie podejrzeć na GitHub.

Main component

Komponent main jest jedynym spośród wszystkich utworzonych, w którym zajrzymy do ciała klasy. Powodem jest przycisk Add card, którego zadaniem jest tworzenie nowych instancji komponentu Card. Najpierw utworzymy szablon:


<div class="page-container row">
  <div class="col s12 page-title">
    <h5>Dashboard</h5>
    <h6 class="grey-text">The most stunning dashboard on the earth. </h6>
  </div>
  <div class="col l8 s12 no-padding">
    <div class="row">
      <div class="col s12">
        <div class="card-panel grey lighten-5 z-depth-1">
          <div class="valign-wrapper">
            <a class="btn red lighten-1" (click)="addCard()">Add card</a>
          </div>
        </div>
      </div>
      <div class="row">
        <div class="col s12 m6" *ngFor="let card of cards">
          <app-card></app-card>
        </div>
      </div>
    </div>
  </div>
  <div class="col l4 m6 s12">
    <app-activities-list></app-activities-list>
  </div>
</div>

Możemy zauważyć, że buttonowi Add card przypięliśmy zdarzenie click, które ma wywoływać metodę addCard(). Dodatkowo, poniżej pojawiła się pętla *ngFor po kolekcji cards – docelowo ma być to kolekcja przechowująca obiekty. Przy każdej kolejnej iteracji, powinna ona umieszczać kolejną instancję komponentu Card oznaczonego selektorem app-card. Jak to wygląda z poziomu klasy?


interface Card {
  title: string;
}

@Component({
  selector: 'app-main',
  templateUrl: './main.component.html',
  styleUrls: ['./main.component.css']
})
export class MainComponent {
  public count = 0;
  public cards: Card[] = [];

  public addCard() {
    this.cards.push({ title: `Card Title ${++this.count}`});
  }
}

Dorzuciłem definicję interfejsu Card, który wymusza na obiektach posiadanie składowej title. Warto podkreślić, że naturalnie powinniśmy umieścić go w osobnym pliku – nie zrobiłem tego, aby nie powiększać objętości artykułu.

Szybki overview

Posiadamy już wszystkie komponenty, a szablon naszego głównego – root componentu – prezentuje się następująco:


<app-header></app-header>
<app-main></app-main>
<app-footer></app-footer>

Całość zachowuje się tak, jak na załączonym obrazku:

Dashboard - część 1

W oczy rzuca nam się kilka niedociągnięć. Po pierwsze – każda karta posiada ten sam tytuł. Po drugie – nasze komponenty w żaden sposób nie przypominają typowych tagów języka HTML. Każdy selektor otwieramy i od razu zamykamy, przez co całość szablonu jest zahermetyzowana w danym komponencie. Czasem takie zachowanie jest pożądane, ale co w przypadku, gdybyśmy chcieli dla innej zakładki wykorzystać stopkę zawierającą inny tekst? Albo wymienić logo na zwyczajny nagłówek?

Nasze komponenty powinny być projektowane tak, aby były wielokrotnego użytku (z ang. reusable). Zdarza się, że pewne fragmenty szablonu powinny być wyciągnięte „na zewnątrz”, np. w przypadku typowych kontenerów. Możemy to osiągnąć wykorzystując mechanizm zwany transkluzją, z którym ściśle powiązane jest słowo kluczowe ng-content.

Czym jest ng-content?

Najłatwiej zaprezentować to na przykładzie. Spróbujmy przekształcić fragment szablonu main.component.html odpowiadający za wyświetlanie kart do takiej postaci:


<div class="row">
  <div class="col s12 m6" *ngFor="let card of cards">
    <app-card>
      {{ card.title }}
    </app-card>
  </div>
</div>

Teraz – aby móc „przechwycić” fragment kodu umieszczony pomiędzy znacznikami app-card, musimy wykorzystać wspomniane słowo kluczowe ng-content w szablonie komponentu CardComponent w ten sposób:


<div class="card">
  <div class="card-content black-text">
    <span class="card-title"><ng-content></ng-content></span>
    <p>
      I am a very simple card. I am good at containing small bits of information.
      I am convenient because I require little markup to use effectively.
    </p>
  </div>
</div>

Wielokrotna transkluzja – mechanizm selektorów

Powyżej rozwiązaliśmy najprostszy przypadek – mieliśmy jeden ng-content i zagnieżdżony fragment szablonu. Jednak czasem dochodzi do sytuacji, kiedy potrzebujemy ich więcej – np. MainComponent mógłby udostępniać możliwość umieszczenia dowolnej treści w miejsce zarówno nagłówka (w tym momencie Dashboard) jak i sidebara (obecnie znajduje się tam activities-list). Co w tym przypadku?

ng-content posiada dodatkowy parametr – select. Dzięki niemu możemy wskazać z którego elementu chcemy w danym momencie skorzystać. Składnia prezentuje się następująco:


<ng-content select="element"></ng-content>

Do dyspozycji mamy podstawowe możliwości wskazywania elementów języka CSS:

  • podanie nazwy tagu, np. h5,
  • wskazanie klasy, np. .sidebar,
  • wykorzystanie atrybutu, np. [atrybut].

Przekształćmy nasz szablon main.component.html tak, aby korzystał z zewnętrznych zależności:


<div class="page-container row">
  <div class="col s12 page-title">
    <ng-content select="h5"></ng-content>
    <h6 class="grey-text">The most stunning dashboard on the earth. </h6>
  </div>
  <div class="col l8 s12 no-padding">
    <div class="row">
      <div class="col s12">
        <div class="card-panel grey lighten-5 z-depth-1">
          <div class="valign-wrapper">
            <a class="btn red lighten-1" (click)="addCard()">Add card</a>
          </div>
        </div>
      </div>
      <div class="row">
        <div class="col s12 m6" *ngFor="let card of cards">
          <app-card>
            {{ card.title }}
          </app-card>
        </div>
      </div>
    </div>
  </div>
  <div class="col l4 m6 s12">
    <ng-content select="[sidebar]"></ng-content>
  </div>
</div>

Teraz umieśćmy nasze zaciągane fragmenty kodu w szablonie głównym app.component.html. Docelowo powinno wyglądać to tak:


<app-header>
  <a href="#">
    <img src="assets/images/logo.svg" alt="Logo" />
  </a>
</app-header>
<app-main>
  <h5>Dashboard</h5>
  <app-activities-list sidebar></app-activities-list>
</app-main>
<app-footer>
  <p class="grey-text text-lighten-4">
    © 2016 Copyright Text
  </p>
</app-footer>

To wszystko. Nasza prosta aplikacja powinna prezentować się następująco:

Dashboard final

Podsumowanie

Kod w artykule miał cel jedynie dydaktyczny. Starałem się w jak najprostszy, możliwy sposób, przekazać wiedzę dotyczącą tworzenia i funkcjonowania komponentów. Wiele rzeczy można było zorganizować inaczej, aczkolwiek wykracza to poza zakres tego wpisu.

Pełny kod źródłowy dostępny na GitHub.