Professional Documents
Culture Documents
Frontend - 4 - Primer
Frontend - 4 - Primer
Dashboard i Heroes tasteri predstavlјaju linkove ka dva različita prikaza, prikaz za Dashboard
i prikaz za listu heroja.
Ispod naslova Top Heroes dati su najbolјi heroji, a klikom na nekog od njih dobija se prikaz
detalјa o tom heroju, kao na Slici 2, gde se može promeniti ime heroja ili se klikom na taster
„Back“ vratiti na Dashboard.
Ako se odabere taster Heroes, prikazuje se lista heroja, kao na Slici 3, a klikom na neko
drugo ime prikazuju se detalјi za tog heroja ispod liste, analogno kao na Slici 4, na kojoj je
prikazan dijagram svih opcija za navigaciju.
U prozoru pretraživača prikazuje se tzv. lјuska aplikacije (application shell), tj. glavni prozor
aplikacije, koju kontroliše Angular komponenta pod nazivom AppComponent, koja je root
komponenta.
Ova aplikacija je u ovom stadijumu praktično ista kao aplikacija data na prethodnim
vežbama.
Prvo menjamo naslov aplikacije, tako što u app.component.ts fajlu dodamo unutar klase
atribut:
title = 'Tour of Heros';
dok u app.component.html fajlu unutar <h1> elementa upisujemo Angular markap:
<h1>{{title}}</h1>
Sada ćemo dodati još jednu komponentu, koja će u glavnom prozoru aplikacije prikazivati
detalјe o heroju.
constructor() { }
ngOnInit() {
}
}
@Component predstavlјa dekorator klase, unutar koga su navedeni podaci o CSS fajlu i
HTML šablonu za prikaz komponente, kao i tzv. CSS selektor za prikaz komponente unutar
HTML šablona.
Postavlјa se pitanje unutar kog HTML dokumenta ćemo prikazati heroes komponente?
Jednostavno, pošto je u pitanju komponenta koja je sadržana u glavnom modulu aplikacije,
sadržaj heroes komponente ćemo prikazati unutar HTML šablona glavne komponente
(app.component.html). Ispod naslova dodamo element:
<app-heroes></app-heroes>
Unutar klase HeroesComponent dodamo atribut:
hero = 'Windstorm';
i potom taj atribut prikažemo unutar heroes.component.html šablon fajla (obrisati sadržaj
koji je generisao Angular):
{{hero}}
Kako će u aplikaciji postojati više heroja, logično je da napravimo posebnu klasu za heroja,
pa ćemo unutar „src/app“ direktorijuma kreirati hero.ts dokument sa klasom:
export class Hero {
id: number;
name: string;
}
Koristimo klјučnu reč „export“ kako bi klasa bila dostupna drugim klasama (komponentama,
modulima).
Sada unutar HeroesComponenti klase uvezemo (import) Hero klasu:
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
hero: Hero = {
id: 1,
name: 'Windstorm'
};
constructor() { }
ngOnInit() {
}
Strana aplikacije se neće prikazati pravilno, jer hero više nije string, već objekat, pa je u
heroes.component.html potrebno da sadrži sledeći kod umesto prethodno datog:
<h2>{{ hero.name }} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><span>name: </span>{{hero.name}}</div>
nakon čega Angular ponovo rebilduje aplikaciju i pretraživač prikazuje osvežen sadržaj.
Ime heroja možemo prikazati velikim slovima, tako što ćemo koristiti pipe za konverziju u
velika slova, tj. uppercase, odnosno:
<h2>{{ hero.name | uppercase }} Details</h2>
Reč uppercase posle karaktera „|“ aktivira ugrađen UppercasePipe. Angular ima ugrađeno
nekoliko ovakvih mehanizama za formatiranje stringova, oznaka valuta, datuma i slično, a
moguće je definisati i dodatne mehanizme. Više o pipes može se pročitati ovde.
Prema specifikaciji, potrebno je da korisnik može izmeniti ime heroja, tj. potrebno nam je
<input> polјe. Ovo polјe mora sadržati ime heroja i takođe omogućiti da se u njega unesu
izmene, nakon čega se izmena imena heroja u instanci klase. To znači da podaci moraju teći
od komponent klase ka ekranu (tj. prikazu) i nazad sa prikaza do klase. Da bi se ovaj proces
automatizovao, Angular omogućava two-way data binding, u konkretnom slučaju između
<input> elementa i hero.name atributa, tako da će HeroesComponent šablon umesto ranije
datog sadržati sledeći kod:
<h2>{{ hero.name | uppercase }} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label>name:
<input [(ngModel)]="hero.name" placeholder="name">
</label>
</div>
[(ngModel)] je Angular sintaksa za two-way data binding, tako da u ovom slučaju povezuje
hero.name atribut i HTML textbox, tako da podaci mogu da teku u oba smera.
Ovo nije dovolјno da bi aplikacija nastavila normalno da radi, jer se na konzoli (Inspector
panel) može videti greška:
Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'.
Naime, ngModel nije dostupan podrazumevano, jer pripada FormsModule, koji se ne učitava
podrazumevano.
Zbog toga je u glavnom modulu (app.module.ts) potrebno importovati FormsModule na
sledeći način:
import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
a takođe, ne treba zaboraviti FormsModule u „imports“ polјu @NgModule dekoratera:
imports: [
BrowserModule,
FormsModule
]
Sada aplikacija radi kako je predviđeno, i možemo menjati ime prikazanog heroja.
Na početku je rečeno da svaka komponenta mora biti deklarisana samo u jednom modulu,
tj. pripada jednom modulu. Mi u prethodnom postupku nigde nismo deklarisali
HeroesComponent, a aplikacija ipak radi. Kako?
Jednostavno, kada smo koristili Angular CLI za kreiranje nove komponente, Angular je
automatski dodao deklaraciju nove komponente unutar glavnog modula app.module.ts, gde
se može videti da je Angular dodao:
import { HeroesComponent } from './heroes/heroes.component';
i izmenio:
declarations: [
AppComponent,
HeroesComponent
]
Na ovaj način je definisan i eksportovan konstantan niz HEROES, pri čemu smo naravno prvo
uvezli klasu Hero, a HEROES definisali kao niz tipa Hero, pa je u pitanju niz koji sadrži
elemente tipa Hero.
Sada je potrebno importovati mock-heroes.ts unutar HeroesComponent klase:
import { HEROES } from '../mock-heroes';
U istoj komponenti umesto koda:
hero: Hero = {
id: 1,
name: 'Windstorm'
};
stavlјamo:
heroes = HEROES;
jer sada umesto jedne klase imamo niz objekata HEROES.
Sada u šalbonu komponente HeroComponent (heroes.component.html) dodamo iznad
postojećeg koda sledeći kod za prikaz jednog heroja u okviru HTML liste:
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
Da bismo mogli prikazivati dinamičke podatke u listi (npr. broj elemenata u listi se menja u
zavisnosti od neke promenlјive ili događaja), ne kreiramo celu listu ručno, već koristimo
Angular repeater direktivu *ngFor unutar početnog taga za element liste:
<li *ngFor="let hero of heroes">
gde je:
<li> host element
heroes je lista iz HeroesComponent klase
hero je tekući objekat liste pri jednoj iteraciji kroz listu
Kada korisnik klikne na ime nekog od heroja, potrebno je za tog heroja prikazati detalјe, što
znači da moramo na događaj klika mišem pozvati metodu koja će prikazati detalјe za tog
heroja.
Prvo je potrebno dodati click event binding unutar elementa liste:
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">
gde je (click)="onSelect(hero)" Angular event binding mehanizam za povezivanje
događaja u DOM-u metode koja se poziva pri tom događaju (tzv. template statement). Kod
Angular-a se povezivanje sa događajima vrši tako što se sa leve strane znaka jednakosti
navede naziv događaja u zagradama, a sa desne pod znacima navoda template statement.
Više o ovoj temi možete pogledati ovde.
Prvo kreiramo novu promenlјivu selectedHero kojoj će biti dodelјen objekat heroja na kog je
korisnik kliknuo, a potom definišemo metodu koja vrši tu dodelu nakon klika:
selectedHero: Hero;
constructor() { }
ngOnInit() {
}
Za heroja na koga je korisnik kliknuo možemo izmeniti prikaz, npr. promeniti boju teksta i
pozadine, tako što unutar početnog <li> elementa za heroja dodamo:
[class.selected]="hero === selectedHero"
čime se elementu dodaje klasa ako je tekući objekat u iteraciji prikaza elemenata liste
jednak odabranom heroju po vrednosti i tipu. Na ovaj način se može uslovno dodeliti
određena CSS klasa nekom elementu.
Za ovu fazu primera možete pogledati (neznatno drugačiju) live aplikaciju ovde, a kod
možete preuzeti ovde.
Međutim, heroes komponenta prikazuje i listu heroja i detalјe o njima, što u budućnosti
može praviti problem pri dalјem razvoju. Cilј je da svaka komponenta ima specifične
zadatke, ne samo zbog lakšeg održavanja koda i timskog rada, već i zbog mogućnosti
ponovnog korišćenja bez potrebe za kopiranjem koda i sl.
Dakle, kreiraćemo komponentu hero-detail, i premestiti HTML kod za prikaz detalјa o heroju
iz HTML šablona komponente hero u šablon komponente hero-detail.
Potrebno je i da se uveze atribut Hero:
import { Hero } from '../hero';
U šablonu komponente heroes na mesto detalјa stavimo selektor heroe-detail komponente.
Novi šablon za hero-detail komponentu ne mora da prikazuje samo selectedHero, može da
prikazuje bilo kog heroja, pa ćemo svugde umesto selectedHero staviti hero.
Sada je potrebno povezati selectedHero iz heroes komponente i hero atribut koji se mapira
na hero atribut hero-detail komponente, što se postiže Angular property binding sintaksom
unutar selektora u kome se prikazuje šablon hero-detail komponente, tj. unutar app-hero-
detail kojoj dodajemo sintaksu za povezivanje:
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
Za Angular property binding možete pogledati primere sintakse ovde.
U klasi HeroDetailComponent je potrebno deklarisati atribut hero tipa Hero, međutim,
potrebno je ispred deklaracije dodati dekorator @Input(), jer se sa ovim atributom povezuje
eksterna komponenta HeroesComponent (kod iznad sa <app-hero-detail>). Angular
kompajler će odbiti povezivanje sa atributom druge komponente osim ako je taj atribut
Input ili Output tipa, tj. ako ima odgovarajući dekorator. Takođe, ovde je potrebno dodati
importovati Input modul, ručno ili pomoću VSC, tako što se klikne na Input dekorator, potom
na „sijalicu) i odabere ponuđena opcija (kao na sledećoj slici):
Na ovaj način se dobija aplikacija koja vizuelno isto izgleda i ima iste funkcionalnosti, ali smo
dodatno pojednostavili komponente, tako da npr. možemo dalјe unapređivati
HeroDetailComponent bez ikakvih izmena u HeroesComponent.
Finalan kod aplikacije je dat na strani predmeta pod nazivom Tour-of-heros (v2).zip
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor() { }
}
Injectable u import-u i kao dekorator označava da klasa učestvuje u DI, tj. da će HeroService
klasa pružiti injectable service, ali ćemo pokazati kako može imati i sopstvene injektovane
zavisnosti (injected dependency).
@Injectable dekorator prihvata objekat sa metapodacima za servis, kao @Component
dekorator sa klasama komponenti.
Kako bi HeroService servis bio dostupan DI pre nego što Angular može da injektuje servis u
HeroesComponent, potrebno je da se registruje provider, koji kreira i isporučuje servis, a u
ovom slučaju praktično instancira HeroService klasu kako bi omogućio servis. Zbog toga
HeroService mora da bude registrovan kao provajder servisa, za šta se koristi @Injectable
dekorator.
Angular podrazumevano registruje provajdera sa root injectior, što znači da je servis
omogućen na root nivou, pa se kreira jedna delјena instanca HeroService i injektuje u bilo
koju klasu gde se to traži. Registracijom provajdera preko @Injectable metapodataka
omogućava i da se aplikacija optimizuje izbacivanjem onih servisa koji se uopšte ne koriste.
Ako želimo da se provajder servisa registruje na drugom nivou, npr. ako želimo da servis
bude dostupan na nivou modula (npr. app), koristimo komandu:
ng generate service hero --module=app
HeroService može podatke preuzimati bilo odakle, sa veb servisa, iz lokalnog skladišta
podataka ili ih jednostavno može lažirati (mock). Izmeštanje manipulacije podacima iz
komponenti znači da bilo kada možemo promeniti implementaciju pristupa podacima bez
ikakvih izmena na komponentama. U dalјem ćemo i dalјe koristiti istu listu heroja.
Prvo u novi HeroService importujemo Hero i HEROES klase, a potom dodamo metodu
@Injectable({
providedIn: 'root'
})
export class HeroService {
getHeroes(): Hero[]{
return HEROES;
}
constructor() { }
}
Aplikacija funkcioniše, a na strani predmeta se može preuzeti kod za ovu fazu aplikacije pod
nazivom Tour-of-heros (v3).
@Injectable({
providedIn: 'root'
})
export class MessageService {
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}}</div>
</div>
Kod za ovu fazu aplikacije dostupan je na strani predmeta kao Tour-of-heros (v4).zip