Angular
Índex
Nom i versions
Característiques
Tipus de webs
Requisits previs
Actualitzar node:
sudo npm install -g n�sudo n stable
sudo npm install -g npm
Abans de continuar…
Introducció a TypeScript
Què és Typescript
Transpilar TypeScript
Exemple: Situació inicial
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Introducción</title>
</head>
<body>
<script src="app.js"></script>
</body>
</html>
function saludar( nombre ) {
console.table( 'Hola ' + nombre ); // Hola John
}
const persona = {
nombre: 'John'
};
saludar();
Quin problema trobem?
Exemple: Passant a Typescript
(function(){
function saludar( nombre:string ) {
console.table( 'Hola ' + nombre );
}
const persona = {
nombre: 'John'
};
saludar(persona.nombre);
})();
Configurar TypeScript
Angular ja farà açò per nosaltres
Estàndards
let nombre = 'Joaquin';
if(true){
let nombre = 'Chimo';
}
console.log(nombre);
Tipus de dades
let nombre: string = 'Joaquin';
let numero: number = 123; // (minúscula)
let qualquierdato; // any
let booleano: boolean;
let hoy: Date = new Date(); // Tipus classe
let persona = {
nombre: 'Pepe',
edad: 30
}
let personajes: string[] = []
let p: Array<string> = []
// intentem assignar a persona altres dades
Paràmetres de les funcions
(function(){
function saludar(quien:string, // Obligatori
momento?: string, // Opcional
objecto: string = 'la mano', ){ // Per defecte
if(momento){
console.log(`${quien} saludó con ${objecto} ${momento}`);
}
else {
console.log(`${quien} saludó con ${objecto}`);
}
}
saludar('Paul');
saludar('Leto', 'por la tarde', 'el crys');
saludar('Gurney', 'el basilet');
})();
Return de les funcions
(() =>{
function sumar(a:number, b: number): number{ return a+b; }
function nombre():string {return 'Pepe';}
})();
Funcions fletxa
(function(){
const toptero = {
posicion: 'aire',
comunica(){
setTimeout(function(){
console.log(`Posición: ${this.posicion}`);
},1000);
}
}
toptero.comunica();
})();
(function(){
const toptero = {
posicion: 'aire',
comunica(){
setTimeout( () => console.log(`Posición: ${this.posicion}`)
,1000);
}
}
toptero.comunica();
})();
Quin problema donarà?
Cóm quedarà el .js?
Promeses i tipat
(() =>{
const recogerEsencia = (cantidad: number): Promise<number> => {
let cantidadActual = 1000;
return new Promise ( (resolve,reject) =>{
if(cantidad > cantidadActual){
reject('No queda');
} else {
cantidadActual -= cantidad;
resolve(cantidadActual);
}
});
}
recogerEsencia(500)
.then( cantidadActual => console.log(`Queda ${cantidadActual}`))
.catch ( err => console.warn(err));
})();
Interfaces
(() =>{
function enviar ( persona: { nombre: string }){ // Problemàtic
console.log(`Enviando a ${persona.nombre} a Arrakis`);
}
let persona = { nombre: 'Jessica', edad: 30 }
enviar (persona);
///////////////////// Interfaces ///////////////////////
interface Caracter {
nombre: string,
edad: number,
familia?: string // opcional
}
let personaInterface: Caracter = { nombre: 'Hawat', edad: 80}
function enviarInterface ( persona: Caracter){ // Més fàcil de mantindre
console.log(`Enviando a ${persona.nombre} a Arrakis`);
}
enviarInterface(personaInterface);
})();
Classes
(() =>{
class Recolector {
private piloto:string = 'fremen';
constructor(
public identificador: string,
public propietario: string,
public buenEstado: boolean = true,
private lugar?: string
){}
}
let rec = new Recolector('1234','cofradia',true,'desierto');
console.log(rec.piloto);
})();
Funcionarà?
(() =>{
function imprimirConsola(constructorClase: Function){
console.log(constructorClase);
}
@imprimirConsola // cal descomentar experimentaldecorators en tsconfig
class Recolector {
constructor(
public identificador: string,
public propietario: string,
public buenEstado: boolean = true,
private lugar?: string
){}
}
let rec = new Recolector('1234','cofradia',true,'desierto');
})();
Nosaltres no crearem decoradors, però Angular els utilitza.
‘Hola Mon’ en Angular
<p>Hola Mundo</p>
<ul><li>{{nombre}}</li><li>{{apellido}}</li></ul>
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'my-app';
nombre = 'Pepe';
apellido = 'Lopez';
}
Estructura del projecte
Per a fer proves
Mòduls de node instal·lats en npm per Angular. No cal enviar-la a github ni com a part del projecte. Es pot regenerar fent npm install
src Codi font de l'aplicació
El que no pujarà a Github
Configuració de l’aplicació
Es crea automàticament. Indica cóm es va a passar a producció. Utilitzat per Webpack
Configuració de TypeScript
App és el primer component que carregarà
Web estàtica
Recursos estàtics
Variables d’ambient
Primer codi que executarà.
Components
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
title = 'hola-mon';
}
Importa el decorador
<app-root>
Una classe normal decorada
Els components standalone tenen imports
Estructura d’un component
Fulla d’estils sols per al component
HTML ‘plantilla’ per al component.
Per a fer proves
Propietats del mòdul (si no és standalone)
Lògica del component. Serà el que més modificarem
Primera aplicació: Primer component
Elements del Framework
Mòduls Angular
NgModule vs Standalone
Càrrega inicial de l’aplicació
Reactivitat
{{ Interpolacions }}
[Vincular atributs]
<img [src]="product.imageUrl" alt=""> → Millor!
<img src={{product.imageUrl}} alt="">
<div [style.height.px]="imageHeight"></div>
<td [ngStyle]="{'background-color': isEven?'red':'green'}">...</td>
<td [ngStyle]="{'width.px': width}">...</td> <!--width és un atribut de TS-->
<td [ngStyle]="styleObject">...</td> <!-- Atribut de TS -->
<td [ngClass]="{'even': isEven, 'last': isLast}"></td>
<td [ngClass]="{'even': isEven, 'last active': isLast}"></td>
<td [ngClass]="classObject"></td> <!-- Atribut de TS-->
[ngStyle] i [ngClass]
<p [ngStyle]="{'font-size': tamano+'px'}">Hola Mundo</p>
<p [ngStyle]="{'font-size.px': tamano}">Hola Mundo</p>
<p [style.fontSize.px]="tamano">Hola Mundo</p>
<p [ngClass]="clase">Hola Mundo</p>
<p [ngClass]="[clase, claseParrafo ]">Hola Mundo</p>
<p [ngClass]="{'text-danger': danger, 'text-info': !danger}">Hola Mundo</p>
Vinculació bidireccional [(ngModel)]
import { FormsModule } from '@angular/forms';
<input type="text" [(ngModel)]="filterSearch" class="form-control"
name="filterDesc" id="filterDesc" placeholder="Filter...">
(Vincular esdeveniments)
<button class="btn btn-sm"
[ngClass]="{'btn-danger': showImage, 'btn-primary': !showImage}"
(click)="toggleImage()">
{{showImage?'Ocultar':'Mostrar'}} imagen
</button>
<img [src]="product.imageUrl" *ngIf="showImage" alt=""
[title]="product.desc">
Observem també el ngClass
Directives
Directives estructurals
<h2>Proves *ngIf</h2>
<div style="width: 400px;" *ngIf="mostrar">
<h3>{{frase.autor}}</h3>
<p> {{frase.mensaje}}</p></div>
<button (click) = "mostrar = !mostrar">Mostra/Ocultar</button>
<h2>Proves *ngFor</h2>
<ul>
<li *ngFor="let cita of citas; let i = index">{{ i }} {{ cita }}</li>
</ul>
index: number
first: boolean
last: boolean
even: boolean
odd: boolean
ngIf, else, ngSwitch
<div *ngIf="show; else elseBlock">La condición es verdadera</div>
<ng-template #elseBlock>La condición es falsa</ng-template>
<span [ngSwitch]="property">
<span *ngSwitchCase="'val1'">Value 1</span>
<span *ngSwitchCase="'val2'">Value 2</span>
<span *ngSwitchCase="'val3'">Value 3</span>
<span *ngSwitchDefault>Other value</span>
</span>
Combinar directives
<ng-container *ngFor="let person of persons"> <!-- Desaparece -->
<ng-container *ngIf="person.age >= 18"> <!-- Desaparece -->
<p>{{person | json}}</p> <!-- Sólo quedará este elemento -->
</ng-container>
</ng-container>
@if (names.length > 0) {
@for (name of names; track $index) {
<p>
{{$index}} {{ name }}
</p>
} } @else {
<p>No names</p>
}
@for (name of names; track $index;
let first = $first, last = $last;
let odd = $odd, even = $even;
let count = $count) {
<p>
{{$index}} {{ name }} {{ first }}: {{ last }} {{ odd }}: {{ even }} {{ count }}
</p>
} @empty {
<p>No items</p>
}
Interficies
ng g interface interfaces/i-product
Es pot utilitzar l’extensió Paste JSON as Code en Visual Studio per objectes complexos
export interface IProduct {
id: number;
description: string;
price: number;
available: Date;
imageUrl: string;
rating: number;
}
products: IProduct[] = [{
id: 1,
description: 'SSD hard drive',
available: new Date('2016-10-03'),
price: 75,
imageUrl: 'assets/ssd.jpg',
rating: 5
},{...
Components
Components
Decorador dels Components
@Component
Estils en els components
Implementar OnInit
@Component({
selector: 'app-product-detail',
templateUrl: './product-detail.component.html',
})
export class ProductDetailComponent implements OnInit {
constructor( private activatedRoute: ActivatedRoute,
private productsService: ProductsService ) { }
ngOnInit(): void {
this.activatedRoute.params.subscribe( params => {
});
}
}
OnInit és una interfície i obliga a implementar ngOnInit()
Components niuats
<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>
...
@Input() p: Product;
// podem ficar p! si falla TS
…
@Input(), diferents opcions
@Input({ required: true, }) name: string; // Requerit, si el pare no el té dona error
@Input() // Amb getters i setters� get courses() {� return this.myCourses;� }� set courses(courses: Course[]) { this.myCourses = courses; }
@Input("userName") name: string; // Amb “Alias”
Property has no initializers…
@Input() products?: IProduct[]�// o�@Input() products: IProduct[] | undefined
@Component({
selector: '[app-edifici]',
<tr app-edifici
*ngFor="let punto of datos; let parell = even"
[ngClass]="{ parell: parell }"
[punto]="punto"
></tr>
El selector està entre [ ]
No és el nom de l’etiqueta, és l’atribut.
@Output: del component fill al pare
@Output() rattingChanged = new EventEmitter<number>();
...
puntuar(i: number): void {
this.rattingChanged.emit(this.auxRatting);
}
(rattingChanged)="changeRatting($event, product)
changeRatting(stars: number, p: Product): void {
p.ratting = stars;
}
Rutes
Rutes
Rutes en #
<base href="/">
Crear les rutes
const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'planets', canActivate: [AuthGuard], component: PlanetListComponent},
{path: 'suns', canActivate: [AuthGuard], component: SunComponent},
{path: 'planet/:id', canActivate: [AuthGuard], component: PlanetDetailComponent},
{path: 'planet/edit/:id', canActivate: [AuthGuard],
resolve: {planet: PlanetResolveService},
component: PlanetEditComponent},
{path: 'login', component: LoginComponent},
{path: '**', pathMatch: 'full', redirectTo: 'home'}
];
Ruta bàsica: path i component que activa
Ruta amb Guard
Ruta amb paràmetres
Ruta amb resolve
Ruta per defecte
Cridar a les rutes
<a class="nav-link active" aria-current="page" [routerLink]="['home']">Home</a>
<a class="nav-link" aria-current="page"
[routerLink]="['home']"
[routerLinkActive]="['active']"
>Home</a>
Cridar a les rutes per codi
import { Router } from '@angular/router';
constructor( private router: Router ) {}
detailsProduct(id: number): void{
this.router.navigate(['/product', id]);
}
Typescript: Podem afegir un atribut declarant-lo com a paràmetre del constructor.
Angular: El router s’afegeix com una injecció de dependències.
Obtindre paràmetres de les rutes (Antic)
import { ActivatedRoute } from '@angular/router';
constructor( private activatedRoute: ActivatedRoute) { }
ngOnInit(): void {
this.activatedRoute.params.subscribe( params => { console.log(params)})
}
Params és un observable al que ens subscrivim.
Obtindre paràmetres de les rutes
provideRouter(routes , withComponentInputBinding()),
@Input('id') recipeID?: string;
providers: [
provideRouter(
[
{ path: '', pathMatch: 'full', redirectTo: '/0' },
{ path: ':count', component: Counter },
],
withViewTransitions(),
withComponentInputBinding()
),
],
@keyframes rotate-out {
to {
transform: rotate(90deg);
}
}
@keyframes rotate-in {
from {
transform: rotate(-90deg);
}
}
::view-transition-old(count),
::view-transition-new(count) {
animation-duration: 200ms;
animation-name: -ua-view-transition-fade-in, rotate-in;
}
::view-transition-old(count) {
animation-name: -ua-view-transition-fade-out, rotate-out;
}
Afegir withViewTransitions()
Al CSS es pot personalitzar
(2024) sols funciona completament a Chrome
onViewTransitionCreated
withViewTransitions({
onViewTransitionCreated: ({transition}) => {
const router = inject(Router);
const targetUrl = router.getCurrentNavigation()!.finalUrl!;
// Skip the transition if the only thing
// changing is the fragment and queryParams
const config = {
paths: 'exact',
matrixParams: 'exact',
fragment: 'ignored',
queryParams: 'ignored',
};
if (router.isActive(targetUrl, config)) {
transition.skipTransition();
}
},
}),
Accepta un objecte amb la funció onViewTransitionCreated
En aquest cas serveix per a cancel·lar l’animació
Servicis
Servicis
HttpClientModule
import { provideHttpClient } from '@angular/common/http';
providers: [
…
provideHttpClient()
]
constructor(private http: HttpClient) { }
getProducts(): Observable<Product[]>{
return this.http.get<{products: Product[]}>(this.productURL).pipe(
map(response => response.products)
);
}
Retornem un Observable que emet arrays de productes.
Abans de retornar l’observable es manipulen les dades.
map() és de la llibreria RxJS
Injecció de dependències
Post per HttpClient
import { HttpClient, HttpHeaders } from '@angular/common/http';�…� private httpOptions = {� headers: new HttpHeaders({� 'Content-Type': 'application/json',� })� };�…�this.http.post<{first: string, last: string}>(this.loginURL,JSON.stringify(datos),httpOptions) ...
Observables
Processant respostes dels Observables
products: Product[] = [];
ngOnInit(): void {
this.productsService.getProducts().subscribe(
{ // Observer literal
next: prods => this.products = prods,
error: (err) => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
} ); }
Mostrar dades asíncrones
constructor(){
effect(()=>{console.log(`Valor de num: ${this.num()}`); });
}
num = signal(0);
updateNum(){this.num.update((n:number)=> n+1);}
ngOnInit(): void {this.num.set(1) }
Resolver
@Injectable({ providedIn: 'root'})
export class ProductResolver implements Resolve<Product> {
constructor(private productsService: ProductsService, private router: Router) { }
resolve(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Product | Observable<Product> | Promise<Product>
{
return this.productsService.getProduct(route.params.id).pipe(
catchError(error => {this.router.navigate(['/products']);
return of(null); })
); }}
{ path: 'product/edit/:id',
canActivate: [ProductDetailGuard],
canDeactivate: [LeavePageGuard],
resolve: { product: ProductResolver},
component: ProductEditComponent}
Es crea com un servici i implementa resolve
Fa ús del servici real d’obtindre productes
Cache
Pipes
Pipes (filtres)
<tr *ngFor="let product of products">
<td>
<img [src]="product.imageUrl"
*ngIf="showImage" alt=""
[title]="product.desc | uppercase">
</td>
<td>{{ product.description }}</td>
<td>{{ product.price | currency:'EUR':'symbol'}}</td>
<td>{{ product.available | date:'dd/MM/y' }}</td>
</tr>
Fer els nostres Pipes
ng g pipe pipes/<nom>
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
}}
<tr *ngFor="let product of productos | productFilter: filtre;">
Autenticació
Autenticació
Guards
ng g guard product/guards/product-detail
{ path: 'product/:id',
canActivate: [supabaseLoginGuard],
component: ProductDetailComponent},
export const supabaseLoginGuard: CanActivateFn = (route, state) => {
const router: Router = inject(Router);
const supabaseService: SupabaseService = inject(SupabaseService);
const urlTree: UrlTree = router.parseUrl('./main');
return supabaseService.loggedSubject.getValue() ? true : urlTree;
};
Autenticació: Servicis, Tokens i Guards
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root'})
export class AuthInterceptorService {
intercept(req: HttpRequest<any>, next: HttpHandler):Observable<HttpEvent<any>> {
const token = localStorage.getItem('idToken'); // Token de locastorage
if (token) {
// Clonem la petició i afegiem el sufix
const authReq = req.clone({url: req.url.concat(`?auth=${token}`)});
// Enviem la petició en token
return next.handle(authReq); }
return next.handle(req);
// Sense tokens enviem la petició original
}}
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true,
}],
Variables com a Observable
private loguedInfo: BehaviorSubject<boolean>;
isLogued(): Observable<boolean>{
return this.loguedInfo.asObservable()
}
constructor(private http: HttpClient) {
this.loguedInfo = new BehaviorSubject<boolean>(false);
}
ngOnInit(): void {
this.logued = this.auth.isAuth();
this.auth.isLogued().subscribe(
l => this.logued = l
)
}
npm install @supabase/supabase-js
export const environment = {� production: false,� supabaseUrl: 'YOUR_SUPABASE_URL',� supabaseKey: 'YOUR_SUPABASE_KEY',�}
Directives
Directives
Crear directives d’atribut
ng g directive directives/resaltado
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appResaltado]'
})
export class ResaltadoDirective {
constructor( private el:ElementRef) {}
@Input('appResaltado') nuevoColor:string;
@HostListener('mouseenter')
entrarMouse(){this.el.nativeElement.style.backgroundColor = this.nuevoColor;}
@HostListener('mouseleave')
saleMouse(){this.el.nativeElement.style.backgroundColor = null;}}
<div class="col-md-4" [appResaltado]="color" >
ElementRef vs Renderer2
constructor(private e: ElementRef, private r: Renderer2) { }
@Input() elementMostrar!: any;
@HostListener('mouseenter')
entrarMouse(){this.r.setStyle(this.elementMostrar,'display','');}
@HostListener('mouseleave')
saleMouse(){ this.r.setStyle(this.elementMostrar,'display','none');}
Template Reference Variables
<li appMostrarMes
[elementMostrar]="ocultar">
…
<p #ocultar style="margin-bottom: 0; display:none ">
Producció: <span *appForDelay="100; let p of placa.production">{{p}}W </span>
</p>
...
Crear directives estructurals
@Directive({
selector: '[appForDelay]'
})
export class ForDelayDirective {
constructor(private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef) {}
items: Array<number> = [];
delay = 0;
@Input() set appForDelay(t: number){this.delay = t;}
@Input() set appForDelayOf(array: number[]){this.items = array;}
ngOnInit(): void {
let i = 0;
this.items.forEach(n => {
...}}
<span *appForDelay="100; let p of placa.production">{{p}}W </span>
Esdeveniments
@HostListener("window:scroll", ['$event'])
homeScroll($event:Event){
let scrollOffset = window.scrollY;
this.imgY = 100 - scrollOffset/2;
if (this.imgY < 0) { this.imgY = 20}
}
Formularis
Plantilla vs Reactius
Formularis de plantilla
<input
type="text"
class="form-control"
[(ngModel)]="product.description"
minlength="5"
maxlength="600"
required name=”description”
/>
Modificar estils al validar
A banda de la tècnica anterior + CSS personalitzat es pot fer una referència amb # i fer coses en ngClass:
<input type="text" name="description" class="form-control”
[(ngModel)]="product.description" minlength="5" maxlength="600" required #descriptionModel="ngModel"
[ngClass]="{'is-valid': descriptionModel.touched && descriptionModel.valid,
'is-invalid': descriptionModel.touched && !descriptionModel.valid}"
/>
</div>
<div>{{ product | json }}</div>
<div>Dirty: {{ descriptionModel.dirty }}</div>
<div>Valid: {{ descriptionModel.valid }}</div>
<div>Value: {{ descriptionModel.value }}</div>
<div>Errors: {{ descriptionModel.errors | json }}</div>
Sempre que tenim [(ngModel)] necessitem que tinga name
Una variable de referència a ngModel per utilitzar en ngClass
La variable de referència indica si és vàlid i pot ser utilitzada per al que necessitem
Manipular l’entrada de l’usuari en temps real
<input
type="text"
class="form-control"
[ngModel]="product.description"
(ngModelChange)="product.description = $event.toUpperCase()"
/>
ngForm
<form #productForm="ngForm" novalidate>
<input type="text" name="description" ... />
</form>
</div>
<div>Touched: {{ productForm.touched }}</div>
<div>Valid: {{ productForm.valid }}</div>
<div>Value: {{ productForm.value | json }}</div>
<div>Descripción: {{productForm.control.get('description').value | json}}</div>
Mostrar els errors
<input
type="email"
name="email"
[(ngModel)]="email"
class="form-control"
required
minlength="5"
maxlength="600"
id="exampleInputEmail1"
aria-describedby="emailHelp"
#emailInput="ngModel"
[ngClass]="{
'is-valid': emailInput.touched && emailInput.valid,
'is-invalid': emailInput.touched && !emailInput.valid
}"
/>
@if ( emailInput.touched && !emailInput.valid) {
<div
class="alert alert-danger">
Email requerit (entre 5 y 60 caracteres)</div>
}
<div id="emailHelp" class="form-text">
We'll never share your email with anyone else.
</div>
Fer els nostres validadors
@Directive({
selector: '[appMinPrice]',
providers: [{provide: NG_VALIDATORS, useExisting: MinPriceDirective,
multi: true}]
})
export class MinPriceDirective implements Validator {
@Input('appMinPrice') minPrice;
constructor() { }
validate( c: AbstractControl): { [key: string]: any}{
if(this.minPrice && c.value) { // si rebem algun valor
if(this.minPrice > c.value) {
return { minPrice: true } //tornem el error
} } return null; // sense error }}
Angular accepta el rol de validadora d’aquesta directiva al registrar-se com a tal.
Enviar el formulari
editar(productForm:NgForm){
// Les validacions que calguen
this.productService.editProduct(this.product).subscribe(
ok => this.router.navigate(['/product/',this.product.id])
)
}
<button type="submit" class="btn btn-primary" [disabled]="productForm.invalid"> Submit </button>
<form #productForm="ngForm" (ngSubmit)="editar(productForm)" novalidate #editForm>
@ViewChild('editForm', {static: true}) editForm: NgForm;
...
console.log(this.editForm.valid);
Les dades dels inputs
editar(productForm:NgForm){
// Les validacions que calguen
this.productService.editProduct(productForm.value).subscribe(
ok => this.router.navigate(['/product/',this.product.id])
)
}
Formularis reactius
Primeres configuracions en Formularis reactius
formulario: FormGroup;
crearFormulario(){
this.formulario = this.formBuilder.group({
name: [''],
price: [0],
description: [''],
}); }
constructor(private formBuilder: FormBuilder) {this.crearFormulario();}
<form [formGroup]="formulario"
(ngSubmit)="crear()">
...
<input type="text" class="form-control"
id="inputName" formControlName="name">
...
Validacions síncrones en formularis reactius
crearFormulario(){
this.formulario = this.formBuilder.group({
name: ['', [Validators.required,
Validators.minLength(5),
Validators.pattern('.*[a-zA-Z].*')]],
price: [0, Validators.min(0.01)],
description: [''],
});
}
get nameNotValid(){
return this.formulario.get('name').invalid && this.formulario.get('name').touched
}
<input type="text" class="form-control" id="inputName" formControlName="name"
[ngClass]="nameNotValid ? 'is-invalid': 'is-valid'">
Crear Validadors
import { AbstractControl, ValidatorFn } from '@angular/forms';
...
function minDateValidator(minInputDate: string): ValidatorFn {
return (c: AbstractControl): { [key: string]: any } => {
if (c.value) {
const minDate = newDate(minInputDate);
const inputDate = newDate(c.value);
console.log(minDate, inputDate);
return minDate <= inputDate ? null : { 'minDate': minDate.toLocaleDateString() }; }
return null; };
}
Funció factory que retorna una ValidatorFn
Funció fletxa tipus ValidatorFn perquè accepta un control i retorna un objecte de la validació.
El que ha escrit l’usuari
El paràmetre que li fiquem al validator
Validation Error Object. Normalment el nom del validador i el missatge a mostrar en l’error.
Cross-Field Validation
this.registerForm = this.formBuilder.group({
passsword: ['',
[Validators.required,
Validators.minLength(3),
Validators.maxLength(10)]],
confirm_passsword: ['',
[Validators.required,
Validators.minLength(3),
Validators.maxLength(10)]],
}, {
validators:
passwordValidator
});
passwordValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
const ps = control.get('password');
const ps2 = control.get('confirm_password');
return ps && ps2 && ps.value === ps2.value ? null : { passwordValidator: true };
};
Validadors de grup
Agrupar camps
this.formulario = this.formBuilder.group({
name: ['', [Validators.required,
Validators.minLength(5),
Validators.pattern('.*[a-zA-Z].*')]],
price: [0, Validators.min(0.01)],
description: [''],
address: this.formBuilder.group({
street: [''],
city: ['']
})
});
<div class="form-group row mb-3" formGroupName="address">
<label for="inputDescription" class="col-sm-2 col-form-label">Address</label>
<div class="form-row col">
<div class="col">
<input class="form-control" id="inputStreet" placeholder="Street"
rows="3" formControlName="street">
</div>
<div class="col">
<input class="form-control" id="inputCity" placeholder="City"
rows="3" formControlName="city">
</div> </div> </div>
Carregar les dades en el formulari
this.formPlaca.setValue(this.placa);
Detectar canvis
export class AppComponent implements OnInit {
...
ngOnInit() {
...
this.userForm.get('notifications').valueChanges
.subscribe(notif => this.updateNotifMethod(notif)); }
}
Formularis dinamics
ingredients: this.formBuilder.array([]),
...
getIngredientControl(): FormControl {
const control = this.formBuilder.control('');
control.setValidators(Validators.required);
return control;
}
get IngredientsArray(): FormArray {
return <FormArray>this.mealForm.get('ingredients');
}
addIngredient() {
(<FormArray>this.mealForm.get('ingredients')).push(this.getIngredientControl());
}
delIngredient(i: number) {
(<FormArray>this.mealForm.get('ingredients')).removeAt(i);
}
Retorna un AbstracControl i cal indicar que és un FormArray
FormArray en la plantilla
<div formArrayName="ingredients">
@for (ingredient of IngredientsArray.controls; track $index) {
<div class="input-group mb-3">
<input
type="text"
class="form-control"
id="ingredient{{ $index }}"
name="ingredient{{ $index }}"
[formControlName]="$index"
placeholder="ingredient"
[ngClass]="{
'is-valid': ingredient.valid && ingredient.touched,
'is-invalid': ingredient.invalid && ingredient.touched
}"
/>
<button class="btn btn-outline-danger" (click)="delIngredient($index)">Borrar</button>
</div>
Arquitectura
Mòduls
Els arrays del decorador @NgModule
Criteris per separar l’aplicació
Què ferramenta s'utilitza?
Arquitectura
Components
Directoris
Deployment
Desplegament en Apache
Biblioteques de tercers
Boostrap
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.scss"],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"]
angular.json
Angular Material
Telèfons mòbils
Angular en telèfons mòbils
Pàgina web responsive
PWA
App híbrida
App quasi nativa
Coses no vistes