Wykład 5: Tworzenie aplikacji SPA w Angularze
mgr inż. Maciej Małecki
Przeglądarka
Serwer
Przykłady: PHP + Smarty, JSP, Ruby on Rails, ASP.NET Web Forms
Zalety
Wyzwania
@Component({
standalone: true,
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
imports: [ NgFor, NgIf, HeroDetailComponent ],
providers: [ HeroService ]
})
export class HeroListComponent implements OnInit {
/* . . . */
}
Hero List
Select a hero from the list to see details.
-
Node - środowisko uruchomieniowe dla JavaScriptNPM - ang. Node Package Manager - zarządzanie zależnościamiNPM registry - repozytorium bibliotekTypeScript - podstawowy język programowania dla Angular 2.x+Angular CLI - narzędzie linii poleceń wspierające proces tworzenia i budowaniaKarma, Jasmine, JEST, Protractor, Cypress - środowisko tworzenia testówInstalacja node ⇝ nvm
Instalacja Angular CLI
npm install -g @angular/cli
Wygenerowanie aplikacji Angular
$ ng new
? What name would you like to use for the new workspace and initial project? foo
? Do you want to enforce stricter type checking and stricter bundle budgets in the workspace?
This setting helps improve maintainability and catch bugs ahead of time.
For more information, see https://angular.io/strict Yes
? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS [ https://sass-lang.com/documentation/syntax#scss ]
Zbudowanie aplikacji
ng build
Uruchomienie aplikacji
ng serve -o
NgModule)Moduł ECMA Script oraz Moduł Angulara to dwie różne rzeczy!
@NgModule({
imports: [CommonModule],
declarations: [
MyComponent1,
MyComponent2,
MyExternalComponent3
],
exports: [MyExternalComponent3]
})
export class MyModule {
}
@NgModule - dekorator TypeScript
standalone.
ng generate component user-mgmt/user-list
html - szablon - część wizualna, ang. template (opcja)scss - prywatny arkusz styli (opcja)component.ts - kod/kontroler komponentuspec.ts - testy jednostkowe komponentu (opcja)Definicja wyglądu komponentu w HTML:
lub z użyciem zewnętrznego pliku:
user-list.component.html
DOM (Document Object Model) jest to model obiektowy reprezentujący dokument HTML wyświetlany przez przeglądarkę.
Przeglądarka buduje DOM w pamięci za każdym razem, gdy wyświetla HTML.
Każdy skrypt JavaScript (a więc także i każda aplikacja Angular) wchodzi w interakcję z DOM.
Postawowe API elementów DOM:
| atrybut | dodatkowa wartość definiująca tag (w dokumencie HTML) |
| właściwość | (property), właściwość obiektu DOM (zwykła właściwość obiektu w JS) |
| metoda | funkcja obiektu DOM, którą można wywołać |
| zdarzenie | (event) - informacja o zaistnieniu pewnego faktu, np click, focus, blur |
Data binding są to mechanizmy pozwalające na integrację modelu z widokiem.
{{interpolation}} |
wyliczy wyrażenie wewnątrz {{}} oraz wstawi wartość |
[property_binding] |
Angular będzie aktualizował wartość właściwości na podstawie wyrażenia |
(event_binding) |
wywołana zostanie funkcja w reakcji na zaistnienie zdarzenia |
[(two_way_binding)] |
synchronizuje model z widokiem "w obie strony" |
Detekcja zmian
Cykl życia komponentu
Dyrektywy to elementy rozszerzające funkcjonalność HTML, które można deklarować za pomocą Angular.
Istnieją trzy rodzaje dyrektyw:
ngModel - wiążą pola edycyjne z modelemngClass - pozwalają dynamicznie modyfikować klasy CSSngStyle - pozwalają dynamicznie modyfikować style CSS
divClasses = {};
...
this.divClasses: {
'valid': this.isValid(),
'highlighted': this.isHighlighted()
}
W prostszych przypadkach...
*ngIf - dodaje/usuwa element z
DOM:
*ngFor - iteruje po liście i
dodaje element DOM dla każdego elementu listy:
ngSwitch, *ngSwitchCase,
*ngSwitchDefault - odpowiednik instrukcji switch/case/default z innych języków
programowania.
Pipes)Pipe transformuje wartość wyświetlaną w szablonie.
Pipes można łączyć w łańcuch: {{ value | pipe1 | pipe2 }}
date | formatowanie daty i czasu |
currency | formatowanie waluty |
number | formatowanie liczb (miejsca dziesiętne) |
uppercase / lowercase | zmiana wielkości liter |
json | serializacja do JSON (do debugowania) |
async | subskrypcja Observable lub Promise |
AsyncPipePipe async automatycznie subskrybuje Observable i anuluje subskrypcję przy niszczeniu komponentu.
// komponent
users$ = this.userService.getAllUsers();
Brak ręcznego subscribe() i unsubscribe() — eliminuje wycieki pamięci.
Angular pozwala na stosowanie data bindings we własnych komponentach.
Dla danych wejściowych:
export class UserDetailsComponent
{
...
@Input() user: User;
...
}
Dla danych wyjściowych:
export class UserDetailsComponent
{
...
@Output() onChanged = new EventEmitter<User>();
...
this.onChanged.emit(user);
...
}
@Input({required=true})@Input({transform: booleanAttribute})Signals to nowy prymityw reaktywny wprowadzający precyzyjną, opartą na grafie zależności detekcję zmian — krok ku odejściu od Zone.js.
signal(value) — reaktywna wartość powiadamiająca o zmianiecomputed(() => ...) — wartość wyliczana automatycznie z innych sygnałóweffect(() => ...) — efekt uboczny reagujący na zmiany sygnałów
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() { this.count.update(v => v + 1); }
constructor() {
effect(() => console.log('count:', this.count()));
}
}
Zone.js (tradycyjny)
setTimeout, fetch, Promise…)Signals (nowy)
Signals to nie gotowe zastąpienie Zone.js — Angular 18 wprowadza tryb eksperymentalny:
provideExperimentalZonelessChangeDetection(). Zone.js nadal jest domyślny.
| Framework | Sygnał / stan | Wartość wyliczana | Efekt uboczny |
|---|---|---|---|
| Angular | signal() |
computed() |
effect() |
| Vue 3 | ref() / reactive() |
computed() |
watchEffect() |
| React | useState() |
useMemo() |
useEffect() |
| Solid.js | createSignal() |
createMemo() |
createEffect() |
Signals to nie wynalazek Angular — Vue 3 i Solid.js stosują je od lat. Kluczowa różnica: React re-renderuje cały komponent przy każdej zmianie stanu; frameworki sygnałowe aktualizują tylko zależne fragmenty DOM.
export class UserCardComponent {
user = input.required<User>(); // zamiast @Input({required: true})
label = input('', {alias: 'title'}); // alias i wartość domyślna
changed = output<User>(); // zamiast @Output() + EventEmitter
}
Pełna integracja z computed() — reaktywność bez dekoratorów.
Jak działa przeglądarka?
Router jest modułem Angulara, który implementuje podstawowe funkcje przeglądarki (URL, historia) w aplikacjach typu Single-Page.
router-outlet określa miejsce, w którym Router będzie
"wklejał" nasz komponent.
Więcej informacji na https://angular.io/guide/router.
| Template-driven | Reactive | |
|---|---|---|
| Definicja struktury | Szablon HTML | Kod TypeScript |
| Testowanie | Wymaga DOM | Łatwe (czyste TS) |
| Walidacja | Dyrektywy HTML | Funkcje Validators |
| Dynamiczne pola | Trudne | Naturalne |
Preferuj Reactive Forms dla złożonych i testowalnych formularzy.
export class UserFormComponent {
private fb = inject(FormBuilder);
form = this.fb.group({
name: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
age: [null, Validators.min(0)]
});
onSubmit() {
if (this.form.valid) {
this.userService.saveUser(this.form.value as User);
}
}
}
Serwis to współdzielony kod aplikacji:
@Injectable()
export class UserService {
users: User[] = [];
constructor() {
this.users = [
{id: 1, name: 'John Doe', email: 'john.doe@email.com'},
{id: 2, name: 'Max Rockatansky', email: 'mad.max@email.com'},
{id: 3, name: 'Chuck Peddle', email: 'chuck@mos.com'}
];
}
getAllUsers() {
return this.users;
}
saveUser(user: User) {
const found = this.findUser(user.id);
if (found) {
Object.assign(found, user);
} else {
this.users.push(user);
}
}
findUser(id: number) {
return this.users.find((user: User) => user.id === id);
}
getNextId() {
return this.users
.map((elem: User) => elem.id)
.reduce((prev, curr) => prev > curr ? prev : curr, 0) + 1;
}
}
HttpClient (i wiele innych części Angulara) zwraca Observable z biblioteki RxJS.
Observable — strumień danych (zero lub więcej wartości w czasie)subscribe()Subscription — aktywna subskrypcja; należy ją anulować (unsubscribe()), aby uniknąć wycieków pamięcimap, filter, switchMap…) transformują strumień
const users$ = this.userService.getAllUsers(); // Observable — nic się nie dzieje
const sub = users$.subscribe({
next: users => this.users = users,
error: err => console.error(err),
complete: () => console.log('done')
});
ngOnDestroy() {
sub.unsubscribe(); // obowiązkowe!
}
Preferuj AsyncPipe w szablonie — zarządza subskrypcją automatycznie.
map | transformacja emitowanej wartości |
filter | pomijanie wartości niespełniających warunku |
switchMap | zamiana strumienia na inny (np. kolejne żądanie HTTP po kliknięciu) |
catchError | przechwycenie i obsługa błędu w strumieniu |
takeUntilDestroyed | automatyczne unsubscribe() przy niszczeniu komponentu (Angular 16+) |