SPA en Angular

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda

Aquest és un tutorial per a fer una Single Page Application en Angular. No explica en detall la teoria d'Angular.

Les SPA són pàgines web que, una vegada carregades, no es refresquen ni es carreguen completament més. Utilitza rutes en la URI del navegador, però aquestes no són enviades d'aquesta manera al servidor, ja que és el client web, en Javascript, qui demana fitxers en JSON o XML i modifica el contingut de la web. Aquestes pàgines presenten alguns avantatges i alguns inconvenients com el SEO o la necessitat de Javascript per a funcionar. Anirem veient en més detall el que significa al fer-la.

Instal·lació

Anem a instal·lar l'entorn de desenvolupament per a Angular. Algunes coses cal explicar-les i altres ja les pots instal·lar com vulgues. També suposarem que tenim Ubuntu escriptori per a desenvolupar i Ubuntu server en cas d'enviar a producció. Per a moltes coses hi ha varies opcions, però per simplificar vaig a mencionar quasi sempre sols una.

  • node: Per a fer anar les ferramentes de terminal d'angular i altres coses, és necessari instal·lar node. En ubuntu:
sudo apt install nodejs
  • npm: El gestor de paquets npm també és necessari:
sudo apt install npm
  • Typescript: Angular funciona en Typescript. L'instal·larem de manera global (-g) per a que funcione en tots els projectes:
sudo npm install -g typescript
  • Angular CLI https://cli.angular.io/ Les ferramentes de terminal d'Angular són necessàries per a treballar més còmodament.
sudo npm install -g @angular/cli
  • Editor de text: Visual Studio Code. Es recomana descarregar el .deb de la web oficial.
    • Extensions:
    • Angular 2 TypeScript Emmet: Permet en zen-coding. Això vol dir que autocompleta codi amb unes instruccions determinades i adaptar en aquest cas a Angular.
    • Angular v5 Snippets: Fragments útils de codi ja predefinit que es poden reutilitzar en el projecte.
    • Angular Language Service: Serveix per a crear més fàcilment les plantilles d'Angular.
    • Material icon Theme: Modifica les icones per a trobar més fàcilment els fitxers.
    • Terminal: Encara que es pot utilitzar la terminal d'Ubuntu, aquesta està en la mateixa finestra i és còmoda.

Creació del projecte

En una terminal, naveguem fins al directori del projecte i executem

ng new tenda

El nom del l'aplicació és tenda

Això ha generat molts fitxers i directoris que anirem descrivint quan els necessitem.

Si volem veure el que ha passat, podem llançar ja el servidor amb

ng serve -o

En aquest comandament observem el que estem indicant: ng és el comandament del Angular/CLI. Com hem vist, serveix tant per a llançar una nova aplicació com per a iniciar el servidor. Amb serve li diguem que cree un servidor web per a aquest directori i en -o per a que òbriga el navegador web en el port obert pel servidor.

Instal·lar Bootstrap

Podem copiar el CDN, instal·lar amb npm o descarregar i descomprimir el directori. Anem a instal·lar-lo de forma local amb npm per a que estiga integrat en el projecte.

npm install bootstrap@next --save

Utilitzem --save per a que clave la dependència en el packaje.json

Aquesta versió serà al menys la 5 i no necessita jquery per a funcionar.

Cal modificar aquestes línies en angular.json:

 "styles": [
              "node_modules/bootstrap/scss/bootstrap.scss",
              "src/styles.css"
            ],
            "scripts": ["node_modules/bootstrap/dist/js/bootstrap.js"]

Creació del navbar

Per anar organitzant bé el projecte a l'inici, anem a crear una estructura de directoris. El navbar serà un component compartit per totes les pàgines del projecte. Per això, podem fer un directori components en app i dins un anomenat shared.

cd tenda
mkdir -p src/app/components/shared

Açò no és precís, però sempre podrem trobar millor els components si tenim el codi organitzat.

Ara sí, anem a crear el component navbar:

ng g c components/shared/navbar

Aquest comandament utilitza abreviatures per dir que volem crear un nou component i el directori on es crearà dins del directori 'app.

Dins del directori aquest, el comandament ng ha creat alguns fitxers de .ts, .css i .html. El que ens interessa ara és ficar una plantilla html amb una barra de navegació de manera que es puga afegir a la web. Sols en interessa el html i el ts, ja que no anem a fer tests ni estils propis del component. Si volem, podem inclús esborrar els fitxers que no interessen.

Una altra cosa que ha passat és que en el fitxer app.module.ts que representa al mòdul principal de l'aplicació, s'ha afegir un import del navbar. Amés, s'ha afegit a declarations del decorador de la classe. D'aquesta manera, Angular ja sap que existeix aquest component i es pot utilitzar per tota l'aplicació.

El que anem a fer és aprofitar el navbar que ens dona Bootstrap i pegar-lo en l'arxiu navbar.component.html En aquest moment es poden llevar les elements html no necessaris del navbar per defecte i modificar el nom de les coses que es queden. Una cosa interessant que ja podem fer en la part visual és guardar una icona en assets i ficar-la com a logo del .navbar-brand:

   <a class="navbar-brand" href="#"><img src="assets/images/logo.png" alt="" width="30" height="24" class="d-inline-block align-top">Tenda</a>

Si observem el fitxer navbar.component.ts veurem que els selector és app-navbar. Això és l'etiqueta que podem utilitzar en l'HTML per a cridar a aquest component. Podem anar al fitxer app.component.html que és el principal de l'aplicació i canviar tot l'HTML generat per Angular automàticament per:

  <app-navbar></app-navbar>

Creació de la pàgina home

Podem crear el component per a la pàgina inicial o home amb:

ng g c components/home

Com en el cas del navbar, sols ens interessa el fitxer html i .ts. En el html podem fer una pàgina de benvinguda. Per exemple, podem utilitzar el carousel de bootstrap.

Ara podem afegir el selector, que serà <app-home> en el fitxer app.component.html a continuació del navbar.

Creació de la pàgina catalogue

Estem fent una especie de tenda i necessitarem una llista de productes. Amés, això servirà per a practicar amb funcionalitats d'Angular:

ng g c components/catalogue

De moment la deixarem buida o amb algun html estàtic d'exemple.

Gestió de les rutes

Com ja sabem, estem fent una SPA i les rutes seran gestionades pel client. Les rutes ens permeten anar als diferents components on 'pàgines'.

Si al crear el mòdul hem dit que sí que volem instal·lar les rutes Angular, tindrem un fitxer anomenat app-routing.module.ts. El contingut d'aquest fitxer serà paregut a:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Amb aquesta estructura inicial podem anar treballant.

Les rutes de l'array routes són objectes amb atributs path i component. Per començar, poden crear la ruta al home i la ruta per defecte en cas de no trobar la que diu la URI:

const routes: Routes = [
 { path: 'home', component: HomeComponent},
 { path: '**', pathMatch: 'full', redirectTo: 'home'}
];

Les rutes funcionen com passa en els components perquè la classe de la ruta està exportada i importada en app.module.ts:

...
import { AppRoutingModule } from './app-routing.module';
...
imports: [
    BrowserModule,
    AppRoutingModule
  ],
...

Com es veu, va en la part de imports del decorador. Va ací perquè és un mòdul i els components d'aquest mòdul seran accessibles per el mòdul actual. El fet de fer un mòdul, exportar e importar el mòdul en el mòdul principal és una manera de separar el codi, però pot estar tot en el fitxer del mòdul principal.

Per a que les rutes funcionen, cal un lloc on renderitzar-les. El que cal fer és crear l'element <router-outlet> a la plantilla app.component.html:

<app-navbar></app-navbar>
<div class="container-fluid">
  <router-outlet></router-outlet>
</div>

En aquest moment no funcionen encara les rutes perquè els menús encara no fan res i perquè sols tinguem definida la ruta de home. Podem afegir una altra ruta i provar-la modificant manualment la URI:

const routes: Routes = [
 { path: 'home', component: HomeComponent},
 { path: 'cataloge', component: CatalogueComponent},
 { path: '**', pathMatch: 'full', redirectTo: 'home'}
];

Menús a routes

Anem a modificar el navbar per afegir les distintes pàgines al menú. Per a que funcionen, cal utilitzar [routerLink]:

        <li class="nav-item">
          <a class="nav-link active" aria-current="page" [routerLink]="['home']">Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" [routerLink]="['catalogue']">Cataloge</a>
        </li>

El routerLink accepta un array amb les parts de la ruta. En aquest cas sols és un nivell.

Una altra modificació que podem fer és que la classe active estiga en la que realment està activa. Això ho farem en routerLinkActive:

 <li class="nav-item">
          <a class="nav-link" aria-current="page" 
          [routerLink]="['home']"
          [routerLinkActive]="['active']"
          >Home</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" [routerLink]="['catalogue']" 
          [routerLinkActive]="['active']">catalogue</a>
        </li>

Omplir el catàleg

En aquest projecte hem creat l'estructura de pàgines i rutes abans de clavar contingut en la web. No obstant, es pot fer en distint ordre.

Per a mostrar els productes de la tenda, anem a utilitzar una plantilla de Boostrap molt típica: les cards. En concret, anem a organitzar les cards en un grid. Busquem en la web de Bootstrap en que ens interessa i creem un principi de plantilla en catalogue.component.html:

<h1>Catalogue</h1>
<div class="row row-cols-1 row-cols-md-2 g-4">
  <div class="col">
    <div class="card">
      <img src="..." class="card-img-top" alt="...">
      <div class="card-body">
        <h5 class="card-title">Card title</h5>
        <p class="card-text">This is a longer card with supporting text below as a natural lead-in to additional content. This content is a little bit longer.</p>
      </div>
    </div>
  </div>
</div>

Està clar que açò cal omplir-ho en contingut obtingut d'un servidor. Com que no ens interessa de moment en backend, el que anem a fer és crear uns JSON amb dades.

Servicis

Per poder mostrar alguna cosa en el catàleg, necessitem mantindre les dades i donar-les al component per a que les renderitze.

Anem a crear un directori anomenant, per exemple, services dins del directori app.

ng g service services/products

Aquest comandament crea un fitxer products.service.ts amb aquest contingut inicial:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class ProductsService {

  constructor() { }
}

Com es veu, necessitem importar Injectable i utilitzar-lo com a decorador de la classe. Això fa que Angular puga proveir un objecte d'aquesta classe quan qualsevol component el necessite.

A continuació, cal anar al app.module.ts i afegir l'importació i clavar el ProductsService com a providers

...
import { AppRoutingModule } from './app-routing.module';
...
 providers: [ProductsService],
...

Anem a fer açò pas a pas, per tant, primer anem a provar si funciona el servici fent que retorne unes dades dirèctament i més endavant farem que les arreplegue del servidor. Per a fer això, podem fer una funció que retorne un array d'objectes (en la classe ProductsService dins de products.service.ts):

getProducts(): any[]{
    return [
      {id: 1, name: 'PC', price: 200}, {id: 2, name: 'mac', price: 300}
    ]
  }

Com es veu, Typescript necessita saber de quin tipus són les dades retornades, però com no hem creat interfaces ni res, podem dir que és un array de any[], açò no està bé i serà corregit més endavant, però ara ens interessa que funcione el prototip.

Aquest servici serà consultat per el component catalogue. Cal, per tant, en catalogue.component.ts, importar el servici i afegir aquest atribut al constructor de la classe CatalogueComponent:

  constructor( private productsService: ProductsService) {}

Observem el codi anterior, al constructor de la classe li passem un argument privat de tipus ProductService. Necessita eixe servici, però en compte de cridar al constructor del servici dins del constructor del component, està com a dependència. Angular ja s'encarregarà de crear-lo al ser un Injectable segons aquest patró de disseny. Amés, al ser declarat en la part de paràmetres, Typescript ja el crea sense la necessitat de fer-ho dins de la funció constructora.

Ara falta que el component cride al servici quan el necessite. El necessitarà cada vegada que siga iniciat. Per això cal utilitzar ngOnInit:

  products: any[] = [];

  ngOnInit(): void {
    this.products =  this.productsService.getProducts();
  }

El que tenim que fer a continuació és modificar la plantilla per a que puga mostrar aquestes dades:

<div class="card" *ngFor="let p of products">
      <img src="..." class="card-img-top" alt="...">
      <div class="card-body">
        <h5 class="card-title">{{p.name}}</h5>
        <p class="card-text">{{p.price}}</p>
      </div>

Evidentment ací falten moltes coses, però ja deuria funcionar. Ara el que falta és agafar aquestes dades del servidor i amb alguna imatge.