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.

El codi resultant estarà en: https://github.com/xxjcaxx/dwec-2020-2021/tree/master/holamonAngular/tenda

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.

Interfaces

Les interfaces són més cosa de Typescript que d'Angular, però és important centralitzar-les per que siguen més fàcilment gestionables. Amés, l'aplicació va fent-se cada vegada més gran, així que cal organitzar-se un poc. De moment sols anem a fer un directori per gestionar els productes en el que clavar les interfaces que utilitzarem en l'aplicació.

ng g interface product/product

Crea l'interficie que ja es pot utilitzar tant el servici com en el component del catàleg.

Detalls dels productes

Anem a fer que al fer clic en un botó del producte es puga entrar a veure el seu detall. Per a fer-ho necessitem modificar coses en les rutes, servicis i components.

El primer és ficar un botó dins de cada card:

<a [routerLink]="['/product',p.id]" class="btn btn-outline-primary">Detalls</a>

Aquest botó fa referència a una ruta que no existeix que apuntarà a un component que encara no està definit. Per això cal fer primer el component. Com hem dit abans, l'aplicació està cada vegada més gran i cal organitzar-la d'alguna manera. El component del detall de producte el crearem en el directori components però de dins del directori product, on està ja el fitxer de l'interficie:

ng g component product/productDetail

També tenim que afegir la ruta en app-routing.module.ts:

 { path: 'product/:id', component: ProductDetailComponent}

Modificarem el html del component per a mostrar els detalls del producte, ja que de moment funciona però no mostra res:

<h1>{{product.name}}</h1>
<div class="row ">
  <div class="col-md-4">

      <img src="..." class="card-img-top" alt="...">
      <p>{{product.price}}</p>

       <a href="cataloge" class="btn btn-outline-primary">Tornar</a>
      </div>
    </div>

Amés, necessitem obtindre la informació que mostrar, ho farem en un ActivatedRoute. Així quedarà una primera versió del product-detail.component.ts:

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-product-detail',
  templateUrl: './product-detail.component.html',
})
export class ProductDetailComponent implements OnInit {

  constructor( private activatedRoute: ActivatedRoute) { }

  ngOnInit(): void {
    this.activatedRoute.params.subscribe( params => { console.log(params)})
  }
}

El problema és que el servici sols està fet per a mostrar tots els productes, ara cal modificar-lo per a mostrar un sols del productes:

import { Injectable } from '@angular/core';
import { Product } from '../product/product';

@Injectable({
  providedIn: 'root'
})
export class ProductsService {
  productList: Product[] = [{id: 1, name: 'PC', price: 200}, {id: 2, name: 'mac', price: 300}]

  constructor() { }
  getProducts(): Product[]{
    return this.productList;
  }
  getProduct(id: number){
   // return this.productList[id]; // no funcionarà bé, ja que id no és index de l'array.
    return this.productList.find(p => p.id == id);
  }
}

Com es pot veure, hi ha una línia que diu que no funcionarà bé. En aquest cas hem optat per buscar amb la funció find() en l'array en un criteri determinat. Si volem utilitzar l'index de l'array es pot afegir un codi al catàleg per a poder utilitzar aquest índex com a variable i poder passar-la al botó:

<h1>Catalogue</h1>
<div class="row row-cols-1 row-cols-md-2 g-4">
  <div class="col">
    <div class="card" *ngFor="let p of products; let i = index ">
      <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>
        <button class="btn btn-outline-primary" (click)="detailsProduct(i)">Detalls</button>
      </div>
    </div>
  </div>
</div>

Però no és necessari i es pot considerar més consistent utilitzar el 'id' en compte de l'index en l'array, que pot canviar. Així que el deixarem com està.

Separar els productes en components niuats

De moment res és massa complicat, però deguem anar separant el codi. El catàleg de productes de moment s'encarrega del propi catàleg i de cadascun dels productes. És millor fer un component per a cada producte, de manera que es puga reutilitzar aquest component per a altres catàlegs, carrets de la compra o similars. Anem a fer un component per a cada producte i el farem ja al directori product que hem fet per als detalls:

ng g component product/product-item

En ell copiarem el fragment d'HTML que necessitem que estava en el catàleg:

<div class="card">
  <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>
    <button class="btn btn-outline-primary" (click)="detailsProduct(p.id)">Detalls</button>
   <!--<a [routerLink]="['/product',p.id]" class="btn btn-outline-primary">Detalls</a> -->
  </div>
</div>

Ara en el catàleg sols tenim que deixar el selector del product-item:

<h1>Catalogue</h1>
<div class="row row-cols-1 row-cols-md-2 g-4">
  <div class="col">
    <app-product-item *ngFor="let product of products"></app-product-item>
  </div>
</div>

Encara falla perquè el product-item no té res en codi del component per a mostrar les dades necessàries. Anem a fer-ho amb unes dades fixes que més endavant obtindrem del pare:

import { Component, OnInit } from '@angular/core';
import { Product } from '../product';
import { Router } from '@angular/router';

@Component({
  selector: 'app-product-item',
  templateUrl: './product-item.component.html',
})
export class ProductItemComponent implements OnInit {
  p: Product = { id: 1, name: 'PC', price: 300 }
  constructor( private router: Router) { }
  ngOnInit(): void {
  }
  detailsProduct(id: number): void{
    this.router.navigate(['/product', id]);
  }
}

Com es pot veure, hem aprofitat la funció per a anars als detalls del producte i hemtingut que importar el Router i injectar-lo en el constructor del component.

Ara el que falta és comunicar el component pare (catàleg) i el fill (product-item) per a que el fill tinga la informació del pare. Per a que funcione, cal modificar l'atribut p del product-item per a que en compte de tindre una informació fixa, tinga el decorador @input():

...
 @Input() p: Product; // = { id: 1, name: 'PC', price: 300 }
...

I també modificarem el catàleg per a que li passe la informació en el selector:

<h1>Catalogue</h1>
<div class="row row-cols-1 row-cols-md-2 g-4">
  <div class="col">
    <app-product-item
    [p]="product"
    *ngFor="let product of products">
    </app-product-item>
  </div>
</div>

Obtindre les dades del servidor

De moment hem mostrat dos productes en un array literal dins del servici que ha de oferir les dades. El més normal és que les dades estiguen en un servidor. Com que estem fent un SPA, no carregarem al servidor de la feina de construir l'HTML. El servidor sols consultarà la base de dades i retornarà un JSON amb la informació mínima per a que el client (Angular) la transforme en HTML.

Com que no ens interessa de moment el que fa el servidor, sols anem a utilitzar un JSON estàtic que Angular demanarà per AJAX.

El primer que tenim que fer és importar el HttpClientModule. Com que és un mòdul, anirà en imports:

...
import { HttpClientModule } from '@angular/common/http';
...
...
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
  ],
...
export class AppModule { }

A continuació, cal modificar la manera en la que el servici demana les dades:

import { Injectable } from '@angular/core';
import { Product } from '../product/product';
import { HttpClient} from '@angular/common/http';
import { Observable } from 'rxjs';
import {map,filter} from 'rxjs/operators'


@Injectable({
  providedIn: 'root'
})
export class ProductsService {
  productList: Product[] = [{id: 1, name: 'PC', price: 200}, {id: 2, name: 'mac', price: 300}]
  private productURL = './assets/productes.json';

  constructor(private http: HttpClient) { }
  getProducts(): Observable<Product[]>{   // retorna un observable al que cal subscriure's
    return this.http.get<{products: Product[]}>(this.productURL).pipe( // get retorna un observable i pipe accepta funcions de manipulació de les dades
      map(response => response.products) // de la resposta traguem l'array que ens interessa
      );
  }
  getProduct(id: number){
    return this.http.get<{products: Product[]}>(this.productURL).pipe(
      map(response => response.products.filter(p => p.id == id)[0]), // de la resposta sols traguem el producte amb el mateix id
      );
  }
}

Tant en el catàleg com en els detalls dels productes cal modificar la cridada al servici amb una subscripció a l'observable que retorna ara.

  ngOnInit(): void {
    this.productsService.getProducts().subscribe(
      prods => this.products = prods, // Success function
      error => console.error(error), // Error function (optional)
      () => console.log('Products loaded') // Finally function (optional)
      );
  }
 ngOnInit(): void {
    this.activatedRoute.params.subscribe( params => {
    this.productsService.getProduct(params.id).subscribe(p=> this.product = p);
    });
  }

Millora en la visualització dels productes

Ja que estem, anem a ficar ja les dades més completes. Per això, hem de modificar l'interfície dels productes per a acceptar descriptions. També crearem un JSON amb varis productes, imatges i descripcions llargues. Les imatges les guardarem en el directori assets, on també està el JSON. Com que pot ser molt llarg, ací tenim un exemple de cóm pot quedar en un producte:

 {"id": 1, "name": "PC", "price": 300, "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum"},

En aquest moment ja comencem a gaudir dels avantatges de treballar en un framework i amb un projecte ben estructurat. Tan sols tenim que afegir Plantilla:Product.description a la plantilla HTML i ja es pot veure.

En el component product-detail no hi ha cap problema en posar una descripció llarga. El problema està en el catàleg, ja que cada producte queda massa llarg. Per això anem a aprofitar una ferramenta del framework, les Pipes que transformen les dades abans de ser mostrades. Angular ja té una per a reduir la quantitat de caràcters a mostrar. Utilitzarem el Pipe slice:

<p class="card-text">{{p.description | slice:0:50}}</p>
En manuals antics trobarem limitTo com a Pipe que fa el que hem fet, però ha quedat obsolet. Ara s'utilitza slice

Quan tenim molts productes tenim que trobar la manera de poder buscar per ells. Anem a crear un buscador. En concret, anem a experimentar dos maneres de fer el buscardor. La primera serà en una Pipe personalitzada que modificarà la llista de productes per filtrar el que s'han de mostrar. La segona serà més permanent, ja que modificarem el servici per a que ens retorne sols els productes que passen el filtre.

Filtrar en Pipes:

El que cal fer és una Pipe personalitzada, un input en el mateix component del catàleg i un [(ngModel)] en el input per filtrar en temps real.

ng g pipe pipes/product-filter

Dins de product-filter.pipe.ts:

import { Pipe, PipeTransform } from '@angular/core';
import { Product } from '../product/product';
@Pipe({
  name: 'productFilter'
})
export class ProductFilterPipe implements PipeTransform { //al implementar PipeTransform cal fer la funció Transform

  transform(products: Product[], filterBy: string): Product[] { // el primer argument és el que cal filtrar i després una llista d'arguments
  // en aquest cas sols és un, el criteri de búsqueda
    const filter = filterBy ? filterBy.toLocaleLowerCase() : null; // passem el filtre a minúscules o a null si no està
    return filter ?  // Si no és null filtra
    products.filter(p => p.name.toLocaleLowerCase().includes(filter))
    : products; // si és null trau l'array sense filtre
  }
}

Ara en la plantilla HTML cal afegir aquest input per poder buscar. En ngModel associem l'input bidireccionalment amb una variable:

<input type="text" name="searchProduct" id="searchProduct" [(ngModel)]="searchProduct">

Creem eixa variable en el component, però per a que funcione ngModel cal afegir a app.module.ts FormsModule en imports:

...
imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,  // Per a que funcione ngModel
  ],
...

Filtrar en el Servici:

Per a que tot funcione necessitem modificar el navbar que conté el formulari de cerca:

   <form class="d-flex">
        <input class="form-control me-2" type="search" placeholder="Buscar Producte" aria-label="Search" (keyup.enter)="buscarProducte(buscarTexto.value)" #buscarTexto>
        <button class="btn btn-outline-success" type="button" (click)="buscarProducte(buscarTexto.value)">Buscar</button>
      </form>

Mirem que hem afegit l'esdeveniment keyup.enter (polsar Enter al teclat). El manejador de l'esdeveniment és la funció buscarTexto a la que li passem l'identificador de l'input que hem ficar a continuació: #buscarTexto. Falta crear la funció en el component: