Creat per en Ramon Vilar d'Ymbra
AngularJS és un framework JS per a crear interfícies d'aplicaciones riques creat per Google.
Es basa en una arquitectura MVC i seguint OOP.
ES6 introdueix el conceptes de classe típic de qualsevol llenguatge de programació OO.
class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
ES6 introdueix també el concepte de mòdul nadiu (ja fa anys que es venen usant a través de tècniques JS).
La diferència principal és que usen de forma automàtica el mode estricte ("use strict";
) i que tot el que es declari en un mòdul és local en aquest.
Cal importar i exportar tot.
// foo.js
export function detectFoo(zone, options) {
// My code.
}
// bar.js
import {detectFoo} from "foo.js";
function go() {
var foo = detectFoo(canvas);
// ...
}
AngularJS està escrit (i usa) TypeScript
És un llenguatge de programació desenvolupat per Microsoft.
És un superconjunt de JavaScript que afegeix, entre d'altres, tipatge estàtic i objectes basats en classes.
Una anotació és una "funció" que afegeix informació a una funció, una variable o una classe.
class Example {
a() {}
@readonly
b() {}
}
És una interfície per línia de comandes per Angular.
Ens facilita moltes operaciones diàries amb Angular tot estalviant-nos temps.
Per instal·lar és tan fàcil com:
npm install -g @angular/cli
Podem crear una aplicació Angular i servir-la des del nostre ordinador fent.
ng new my-app
cd my-app
ng serve
Angular CLI ens proporciona una comanda per a poder crear fàcilment diversos elements.
// Generates a component.
ng generate component MyComponent
// Generates a service.
ng generate service MyService
Anem a crear la nostra primera aplicació Angular
Anem a un directori de treball i executem el següent:
ng new my-app
cd my-app
ng serve --open
Anem al navegador que se'ns ha obert a http://localhost:4200/
Accedim a /src/app/app.component.ts
i podem provar de canviar el títol.
Proveu de canviar també els estils a /src/app/app.component.css
Anem a revisar tots els fitxers que formen part de l'aplicació.
Una aplicació Angular és modular per definició i està formada per 1 o N mòduls.
Cada aplicació té com a mínim un mòdul NgModule, el mòdul root (sovint AppModule
).
@NgModule({
declarations: [
...
],
imports: [
...
],
bootstrap: [AppComponent]
})
export class AppModule { }
Angular ens proporciona una sèrie de biblioteques que podem fer servir a les nostres aplicacions.
Podem identificar una biblioteca d'angular perquè ve precedida del prefix @angular
import { Component } from '@angular/core';
Un component controla una porció de la pantlla que anomenarem vista.
La lògica d'un component la definirem sempre dins d'una classe.
Angular crea, actualitza i destrueix components a mesura que l'usuari es mou per l'aplicació
export class ShoppingListComponent implements OnInit {
lineItems: LineItem[];
constructor(private service: LineItemService) { }
ngOnInit() {
this.lineItems = this.service.getLineItems();
}
}
Per definir la vista d'un component usem una plantilla.
Les plantilles estan desenvolupades amb HTML i usen bindings d'Angular per a poder interactuar amb la informació.
<h2>Hero List</h2>
<p><i>Pick a hero from the list</i></p>
<ul>
<li *ngFor="let hero of heroes" (click)="selectHero(hero)">
{{hero.name}}
</li>
</ul>
Les metadades ajuden a Angular a saber com ha de processar una classe.
Les metadades es passen a través d'anotacions com per exemple: @Component
, @Input
o @Output
.
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})
export class HeroListComponent implements OnInit {
/* . . . */
}
Un servei és qualsevol classe que proporciona una funcionalitat o dades, de forma concisa i precisa.
El concepte de servei no és propi d'Angular, però s'usa molt sovvint per a organitzar les aplicacions per tal de que siguin fàcilment mantenibles.
Angular es basa en el patró d'injecció de dependències.
constructor(private service: HeroService) { }
Les plantilles es construeixen amb HTML normal i corrent.
Per motius de seguretat l'element <script> no està permés en una plantilla. En cas de trobar-lo, s'ignorarà.
Per a mostrar un valor en una plantilla s'usa {{ }}
<a href="{{url}}">Hi {{user.name}}. How are you?.</a>
Les expressions usades poden referir-se a propietats del context de la plantilla.
El resultat d'una expressió es pot transformar abans de mostrar-se en una plantilla amb una pipe.
<h2>{{title | uppercase}}</h2>
I fins i tot podent tenir paràmetres
<p>Birthdate: {{birthdate | date:'longDate'}}</p>
Serveix per a presentar una llista de valors en una plantilla
<div *ngFor="let hero of heroes; let i=index;">
({{i}}) {{hero.name}}
</div>
A més a més de l'índex, permet obtenir altres valors del bucle com booleans indicant si és el primer o el darrer valor, o si és un valor parell o senar.
Es pot afegir o eliminar un element al DOM aplicant aquesta directiva a un element.
<div *ngIf="heroes.length > 3">
There is a lot of heroes!
</div>
No confonguem eliminar amb amagar!
Modifiqueu la vostra aplicació de prova per tal de tenir la següent estructura
LineItem
amb dues propietats: id, nameShoppingList: LineItem[]
És el mecanisme per a coordinar el que veu l'usuari amb els valors de l'aplicació.
Nosaltres declarem la unió de valors i el framework fa la feina per a comunicar aquesta informació entre ells mateixos.
N'hi ha principalment de tres tipus.
Amb aquests mètodes no establim valors als atributs de l'HTML, si no que treballem amb propietats.
<button [disabled]="isUnchanged">Save</button>
No estem treballant amb l'atribut disabled
si no que amb la seva propietat!
Enllaça una propietat d'un element a una propietat d'un component.
<img [src]="myImageUrl">
<img bind-src="myImageUrl">
Ara si canvia la propietat del component myImageUrl
canviarà de forma automàtica a l'HTML.
Per poder establir valors a atributs (aria, svgs i span de taules) s'ha d'usar
<button [attr.aria-label]="actionName">{{actionName}} with Aria</button>
Per a establir valors de classes:
<div [class.special]="isSpecial">The class binding is special</div>
Per a més opcions, podeu mirar la directiva NgClass
Per a poder establir valors d'estils:
<button [style.color]="isSpecial ? 'red': 'green'">Red</button>
Per a establir valors amb unitats:
<button [style.font-size.em]="isSpecial ? 3 : 1" >Big</button>
Per a més opcions, podeu mirar la directiva NgStyle
S'utilitza per a establir valors des d'un element a un component.
<button (click)="onSave()">Save</button>
<button on-click="onSave()">Save</button>
En fer clic al botó, es crida al mètode del component.
Aquest és el darrer dels casos: quan volem mostrar un valor a la vegada que l'estem modificant la propietat.
Un cas especial és l'ús en formularis amb NgModel
(cal importar FormsModule
).
<input [(ngModel)]="currentHero.name">
Agafem la llista de la compra d'abans i afegim les següents opcions.
No es pot accedir a propietats o mètodes d'un altre component (només té accés al seu context públic).
I què fem si jo vull passar un element d'un component a un altre de diferent? Per exemple des d'un llistat a un detall? I si vull reaccionar a un esdeveniment?
Entren en joc les annotacions @Input
i @Output
@Input
En cas de voler "passar" un valor d'un component pare a un componet fill podem fer una cosa del tipus:
<app-list-detail [listItem]="selectedListItem"></app-list-detail>
Però si fem directament això el compilador ens donarà un missatge de violació d'accés
@Input
Cal definir la variable com a element d'entrada.
@Input() listItem: ListItem;
Amb això fem que el valor de selectedListItem
es vinculi amb la propietat listItem
del component ListDetailComponent
.
@Output
En canvi, si el que volem és que un valor d'un component fill pugi cap al pare, farem servir una cosa així.
<app-list-detail (deleteExecuted)="deleteItem($event)"></app-list-detail>
Igualment si fem directament això el compilador ens donarà un missatge de violació.
@Output
Cal definir el mètode com a element de sortida.
@Output() deleteExecuted = new EventEmitter<ListItem>();
Amb això fem que la propietat deleteExecuted
pugui emetre un esdeveniment informant del LineItem
eliminat i que un altre component pugui reaccionar-hi.
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-voter',
template: `
<h4>{{name}}</h4>
<button (click)="vote(true)" [disabled]="voted">Agree</button>
<button (click)="vote(false)" [disabled]="voted">Disagree</button>
`
})
export class VoterComponent {
@Input() name: string;
@Output() onVoted = new EventEmitter<boolean>();
voted = false;
vote(agreed: boolean) {
this.onVoted.emit(agreed);
this.voted = true;
}
}
import { Component } from '@angular/core';
@Component({
selector: 'app-vote-taker',
template: `
<h2>Should mankind colonize the Universe?</h2>
<h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
<app-voter *ngFor="let voter of voters"
[name]="voter"
(onVoted)="onVoted($event)">
</app-voter>
`
})
export class VoteTakerComponent {
agreed = 0;
disagreed = 0;
voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
onVoted(agreed: boolean) {
agreed ? this.agreed++ : this.disagreed++;
}
}
Podem crear una referència a un element fent servir la sintaxis #var
<input #phone placeholder="phone number">
I usar-la més endavant allà on ens interessi.
<button (click)="callPhone(phone.value)">Call</button>
Les variables poden referenciar a un component, i per tant, en una plantilla podem fer una cosa com aquesta.
<h3>Countdown</h3>
<button (click)="timer.start()">Start</button>
<button (click)="timer.stop()">Stop</button>
<div class="seconds">{{timer.seconds}}</div>
<app-countdown-timer #timer></app-countdown-timer>
Seguint amb l'exemple de la ShoppingList, dividim l'aplicació en dos components, de tal forma que tinguem un component per al llistat i un altre de diferent per al detall/formulari de modificació de valors.
Sovint a les nostres aplicacions voldrem que les vistes siguin accessibles a través de certs URLs.
A Angular treballarem amb el concepte Routes
.
Tot i que ho podem declarar a l'aplicació general, es recomana la creació d'un mòdul específic.
Podem usar una classe base com la següent:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@NgModule({
exports: [ RouterModule ]
})
export class AppRoutingModule {}
Per definir rutes hem de fer:
const routes: Routes = [
{ path: 'heroes', component: HeroesComponent }
];
I afegir-ho a @NgModule.imports
:
imports: [ RouterModule.forRoot(routes) ],
Ara cal obrir la nostra AppComponent
per exemple i afegir:
<h1>{{title}}</h1>
<nav>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
Ara dins de <router-outlet>
es mostrarà el component definit per la ruta
Podem definir una ruta per defecte perquè hi navegui sempre quan anem a l'arrel de l'aplicació.
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' }
Ens pot interessar interceptar qualsevol ruta que no estigui definida per, per exemple, enviar-la a la típica pàgina 404.
{ path: '**', component: PageNotFoundComponent }
Hi ha certes pàgines que ens pot interessar accedir-hi via una adreça amb paràmetres, del tipus /detail/23
. Per fer-ho podem declarar:
{ path: 'detail/:id', component: HeroDetailComponent }
Si volem a més a més, obtenir el valor de l'URL per a poder mostrar aquell valor específic, caldrà injectar un servei al constructor del component amb el que treballem.
constructor(private route: ActivatedRoute) {}
Allà on vulguem usar el valor, el podem obtenir del paràmetre de l'URL fent:
const id = this.route.snapshot.paramMap.get('id');
Cal tenir present que tots els paràmetres obtinguts de l'URL són strings. Si volem usar com a nombre, caldrà transformar-lo.
Anem a transformarla nostra aplicació de la llista de la compra per tal de poder ser accessible via /shopping-list
i el seu detall mitjançant /list-item/<id>
Extra: afegiu opcions per a poder tenir una pàgina d'error.
Existeixen dues formes diferents de poder crear formularis a Angular:
Es basa en la directiva ngModel
que ja coneixem.
Tota la lògica del formulari es defineix a la plantilla (mapping, validacions, etc.).
Addient per a petits formularis però ràpidament es queda curt.
<form #form="ngForm" (ngSubmit)="onSubmit()">
<p>
<label>First Name:</label>
<input type="text" [(ngModel)]="user.firstName" name="firstName" required>
</p>
<p>
<label>Last Name:</label>
<input type="text" [(ngModel)]="user.LastName" name="lastName" required>
</p>
<p>
<button type="submit" [disabled]="!f.valid">Submit</button>
</p>
</form>
En aquest cas, gràcies al two-way binding la nostra funció de submit pot quedar ben senzilla.
onSubmit() {
console.log(this.user);
this.user.save();
}
En cas de no voler two-way binding, podem només usar en una direcció [ngModel]
i fer:
<form #form="ngForm" (ngSubmit)="onSubmit(form)">
onSubmit({value}) {
this.user.firstName = value.firstName;
this.user.lastName = value.lastName;
this.user.save();
}
Si anem afegint lògica al formulari, validacions, etc. aquests es torna difícil de llegir i de mantenir. A més, no podem fer proves unitàries del formularis!
Imaginem-nos que necessitem fer validacions dependents d'un altre camp, com per exemple, comprovar que dos camps tenen el mateix valor.
Es necessita una versió més avançada de declaració de formularis.
També anomenats Model driven és una forma de definir formularis des de codi, tot fent independent la seva modulació HTML de la seva modulació en codi.
Es basa en el mòdul ReactiveFormsModule
import {ReactiveFormsModule} from "@angular/forms";
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<p>
<label>First Name:</label>
<input type="text" formControlName="firstName">
</p>
<p>
<label>Last Name:</label>
<input type="text" formControlName="lastName">
</p>
<p>
<button type="submit" [disabled]="!f.valid">Submit</button>
</p>
</form>
Si ens fixem, ara tenim una directiva formGroup
associada a una variable anomenada userForm
Ara la nostra plantilla no té informació sobre validacions. Aquestes residiran al controlador, el qual pot ser provat unitàriament.
El nostre controlador tindrà la següent pinta.
export class UserFormComponent {
userForm: FormGroup;
constructor(private fb: FormBuilder) {
this.createForm();
}
createForm() {
this.userForm = this.fb.group({
firstName: ['', Validators.required ],
lastName: ['', Validators.required ]
});
}
onSubmit() {
// Show formModel.
console.log(this.userForm.value);
}
}
Això ens permet poder tenir el model completament independent del formulari (formularis parcials).
A més, se'ns obre la possibilitat de poder aplicar programació reactiva.
Podem establir tots els valors d'un formulari amb setValue
.
this.form.setValue({
firstName: this.user.firstName,
lastName: this.user.lastName
});
this.form.setValue(user);
Però també podem especificar només valors per part dels elements amb patchValue
.
this.form.patchValue({
firstName: this.user.firstName
});
this.form.setValue(user);
Podem esborrar tots els camps (i estat) d'un formulari fent servir la funció reset()
this.form.reset();
Afegim un formulari al shoppingList per tal de poder afegir nous elements a la llista
Ampliem el model de dades per a especificar nom de l'element (obligatori) i quantitat, per defecte 1.
Usar reactive forms.
Per a fer-ho farem servir un servei anomenat HttpClient
disponible mitjançant HttpClientModule
.
Hem d'injectar el servei al nostre component per tal de poder-lo fer servir.
constructor(private http: HttpClient) {}
HttpClient
Usa l'objecte XMLHttpRequest
propi dels navegadors per a poder fer peticions dels següents tipus:
Per a llançar una petició de consulta, només cal cridar al mètode get()
de l'objecte i subscriure'ns-hi.
this.http.get(url).subscribe(data => {
console.log(data);
});
Tècnicament, qui executa la petició és la crida a subscribe()
. Sense això, no s'executaria.
Aquesta crida retornaria a data
un anònim Object
, imaginem-nos, amb diverses propietats, entre les quals, name
i surname
.
Només podríem accedir als seus valors fent: data['name']
o data['surname']
.
Però el que de debò ens agradaria és poder accedir mitjançant la notació del punt data.name
o data.surname
.
El que podem fer és, per exemple, fer cast de la resposta a un tipus que disposi de les propietats corresponents.
Per això, podem definir una interface amb les propietats que ens interessi:
interface UserResponse {
name: string,
surname: string
}
Ara ja podem fer cast en la resposta de la petició:
this.http.get<UserResponse>(url).subscribe(data => {
console.log('User name: ' + data.name);
console.log('User surname: ' + data.surname);
});
this.http.get<UserResponse>(url).subscribe(
data => {
console.log('User name: ' + data.name);
console.log('User surname: ' + data.surname);
},
err => {
console.log('Error occurred.');
}
);
Si volem accedir a més informació de l'error, podem definir el paràmetre com de tipus:
(err: HttpErrorResponse)
const req = this.http.post(url, data)
.subscribe(
res => {
console.log(res);
},
err => {
console.log("Error occured");
}
);
Usant https://jsonplaceholder.typicode.com/, anem a construir una aplicació que consulti l'endpoint /posts i llisti la seva resposta.
Una vegada fet això, fem un formulari amb dos camps, title i body, i al fer clic a un botó, envïi els valors a /posts amb userId = 2.