1. Czym jest serwis w Angularze?
Serwis to specjalna klasa w Angularze, która:
- Przechowuje dane i logikę biznesową
- Udostępnia funkcjonalności dla komponentów
- Łączy komponenty między sobą
- Żyje przez cały czas działania aplikacji
Analogie z życia:
- Serwis = Poczta 📮 – przekazuje wiadomości między ludźmi
- Serwis = Bank 🏦 – przechowuje pieniądze, udostępnia operacje
- Serwis = Biblioteka 📚 – przechowuje książki, wypożycza je różnym osobom
- Serwis = Recepcja w hotelu 🏨 – komunikuje między gośćmi a różnymi działami
W czym serwis pomaga?
// ❌ BEZ SERWISU - kod powtarzany w każdym komponencie
export class FilmComponent1 {
films = [...]; // Ta sama lista
addFilm() { /* ta sama logika */ }
}
export class FilmComponent2 {
films = [...]; // Ta sama lista - DUPLIKACJA!
addFilm() { /* ta sama logika - DUPLIKACJA! */ }
}
// ✅ Z SERWISEM - kod w jednym miejscu
export class FilmService {
films = [...]; // Jedna lista dla wszystkich
addFilm() { /* logika w jednym miejscu */ }
}Kiedy tworzymy serwis?
- Współdzielenie danych – kilka komponentów potrzebuje tych samych danych
- Komunikacja – komponenty muszą się ze sobą „rozmawiać”
- Logika biznesowa – obliczenia, walidacja, API calls
- Unikanie duplikacji – ten sam kod w wielu miejscach
2. Czym jest komunikacja rodzeństwa?
Komponenty rodzeństwa to komponenty, które są na tym samym poziomie w hierarchii – mają tego samego rodzica, ale nie są swoimi rodzicami ani dziećmi.
Przykład z życia:
Wyobraź sobie dwoje rodzeństwa w domu:
- Ania (komponent 1) chce przekazać wiadomość Tomkowi (komponent 2)
- Nie mogą się komunikować bezpośrednio
- Potrzebują pośrednika – np. kartka na lodówce (serwis)
W aplikacji filmowej:
App Component (rodzic)
├── Film Search (dziecko 1)
├── Film List (dziecko 2)
└── Film Counter (dziecko 3)Problem: Jak Film Search może przekazać wyniki wyszukiwania do Film List?
Rozwiązanie: Serwis – „kartka na lodówce” dla komponentów!
2. Jak działa serwis jako pośrednik?
Dlaczego potrzebujemy serwisu?
Problem bez serwisu:
// Komponenty nie mogą się komunikować bezpośrednio!
Component A ❌ Component B // Nie da się!
Component A ❌ Component C // Nie da się!Rozwiązanie z serwisem:
Component A → Service → Component B ✅
Component A → Service → Component C ✅
Component B → Service → Component A ✅Serwis to „Poczta” między komponentami:
- Serwis = Skrzynka pocztowa 📮
- Komponent A = Osoba wysyłająca list 📝
- Komponent B = Osoba odbierająca list 📬
- Subject/Observable = System powiadomień 🔔
Schemat działania:
Komponent A → wysyła → Serwis → powiadamia → Komponent BDlaczego to jest lepsze?
- Centralne miejsce – wszystkie dane w jednym serwisie
- Automatyczne powiadomienia – gdy dane się zmienią, wszyscy się dowiadują
- Łatwość dodawania – nowy komponent łatwo się podłącza
- Brak duplikacji – logika w jednym miejscu
3. Podstawowe pojęcia
Subject vs Observable – prosta analogia:
Subject = Nadajnik radiowy 📻
- Może wysyłać sygnały (next())
- Może odbierać sygnały (subscribe())
Observable = Odbiornik radiowy 📟
- Może tylko odbierać sygnały (subscribe())
// W serwisie
private messageSubject = new Subject<string>(); // Nadajnik
message$ = this.messageSubject.asObservable(); // Odbiornik
// Wysyłanie
this.messageSubject.next("Wiadomość");
// Odbieranie
this.message$.subscribe(wiadomość => console.log(wiadomość));4. Przykład krok po kroku – Wyszukiwarka filmów
Krok 1: Tworzymy serwis
// film-search.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root' // Dostępny w całej aplikacji
})
export class FilmSearchService {
// PRYWATNY nadajnik - tylko serwis może wysyłać
private searchResultsSubject = new Subject<any[]>();
// PUBLICZNY odbiornik - komponenty mogą nasłuchiwać
searchResults$ = this.searchResultsSubject.asObservable();
// Metoda do wysyłania wyników wyszukiwania
updateSearchResults(films: any[]) {
console.log('🔔 Serwis wysyła nowe wyniki:', films);
this.searchResultsSubject.next(films);
}
// Metoda do czyszczenia wyników
clearResults() {
console.log('🧹 Serwis czyści wyniki');
this.searchResultsSubject.next([]);
}
}Krok 2: Komponent wysyłający (Film Search)
// film-search.component.ts
import { Component } from '@angular/core';
import { FilmSearchService } from './film-search.service';
@Component({
selector: 'app-film-search',
standalone: true,
template: `
<div class="search-container">
<h3>🔍 Wyszukiwarka filmów</h3>
<input
type="text"
#searchInput
(input)="searchTerm = searchInput.value"
placeholder="Wpisz tytuł filmu..."
class="search-input">
<button (click)="search()" class="search-btn">
Szukaj
</button>
<button (click)="clearSearch()" class="clear-btn">
Wyczyść
</button>
<p>Szukasz: "{{ searchTerm }}"</p>
</div>
`,
styles: [`
.search-container {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin: 10px;
}
.search-input {
padding: 8px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
width: 200px;
}
.search-btn {
background: #007bff;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.clear-btn {
background: #6c757d;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
}
`]
})
export class FilmSearchComponent {
searchTerm = '';
// Baza filmów do przeszukiwania
allFilms = [
{ id: 1, title: 'Avengers: Endgame', year: 2019, genre: 'Akcja' },
{ id: 2, title: 'Spider-Man', year: 2021, genre: 'Akcja' },
{ id: 3, title: 'Batman', year: 2022, genre: 'Akcja' },
{ id: 4, title: 'Titanic', year: 1997, genre: 'Romans' },
{ id: 5, title: 'Joker', year: 2019, genre: 'Dramat' }
];
constructor(private filmSearchService: FilmSearchService) {}
search() {
if (this.searchTerm.trim() === '') {
this.clearSearch();
return;
}
// Filtruj filmy na podstawie wyszukiwanego tekstu
const results = this.allFilms.filter(film =>
film.title.toLowerCase().includes(this.searchTerm.toLowerCase())
);
console.log('📤 Wysyłam wyniki wyszukiwania:', results);
// WYSYŁAM wyniki przez serwis
this.filmSearchService.updateSearchResults(results);
}
clearSearch() {
this.searchTerm = '';
console.log('📤 Wysyłam puste wyniki');
// WYSYŁAM pustą listę przez serwis
this.filmSearchService.clearResults();
}
}Krok 3: Komponent odbierający (Film List)
// film-list.component.ts
import { Component, OnDestroy } from '@angular/core';
import { FilmSearchService } from './film-search.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-film-list',
standalone: true,
template: `
<div class="list-container">
<h3>📋 Lista filmów</h3>
<p>Wyników: {{ films.length }}</p>
@if (films.length === 0) {
<div class="no-results">
<p>🔍 Brak wyników wyszukiwania</p>
<p>Użyj wyszukiwarki powyżej, aby znaleźć filmy</p>
</div>
} @else {
<div class="films-grid">
@for (film of films; track film.id) {
<div class="film-card">
<h4>{{ film.title }}</h4>
<p>Rok: {{ film.year }}</p>
<p>Gatunek: {{ film.genre }}</p>
</div>
}
</div>
}
</div>
`,
styles: [`
.list-container {
background: #e9ecef;
padding: 20px;
border-radius: 8px;
margin: 10px;
}
.films-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.film-card {
background: white;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.no-results {
text-align: center;
color: #6c757d;
padding: 20px;
}
`]
})
export class FilmListComponent implements OnDestroy {
films: any[] = [];
private subscription: Subscription;
constructor(private filmSearchService: FilmSearchService) {
console.log('📥 Film List nasłuchuje wyników wyszukiwania...');
// SUBSKRYBUJĘ się na wyniki wyszukiwania
this.subscription = this.filmSearchService.searchResults$.subscribe(
results => {
console.log('📥 Film List otrzymał nowe wyniki:', results);
this.films = results;
}
);
}
// WAŻNE: Muszę się wysubskrybować gdy komponent jest niszczony
ngOnDestroy() {
console.log('🚫 Film List przestaje nasłuchiwać');
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}Krok 4: Drugi odbierający komponent (Film Counter)
// film-counter.component.ts
import { Component, OnDestroy } from '@angular/core';
import { FilmSearchService } from './film-search.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-film-counter',
standalone: true,
template: `
<div class="counter-container">
<h3>📊 Statystyki</h3>
<div class="stats">
<div class="stat-item">
<span class="stat-number">{{ filmCount }}</span>
<span class="stat-label">Znalezione filmy</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ genreCount }}</span>
<span class="stat-label">Różne gatunki</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ avgYear }}</span>
<span class="stat-label">Średni rok</span>
</div>
</div>
@if (filmCount > 0) {
<div class="genres-list">
<h4>Znalezione gatunki:</h4>
@for (genre of uniqueGenres; track genre) {
<span class="genre-badge">{{ genre }}</span>
}
</div>
}
</div>
`,
styles: [`
.counter-container {
background: #fff3cd;
padding: 20px;
border-radius: 8px;
margin: 10px;
}
.stats {
display: flex;
gap: 20px;
margin: 15px 0;
}
.stat-item {
text-align: center;
}
.stat-number {
display: block;
font-size: 2rem;
font-weight: bold;
color: #856404;
}
.stat-label {
font-size: 0.9rem;
color: #6c757d;
}
.genres-list {
margin-top: 15px;
}
.genre-badge {
background: #ffc107;
color: #212529;
padding: 4px 8px;
border-radius: 12px;
margin-right: 8px;
font-size: 0.8rem;
}
`]
})
export class FilmCounterComponent implements OnDestroy {
filmCount = 0;
genreCount = 0;
avgYear = 0;
uniqueGenres: string[] = [];
private subscription: Subscription;
constructor(private filmSearchService: FilmSearchService) {
console.log('📥 Film Counter nasłuchuje wyników wyszukiwania...');
// SUBSKRYBUJĘ się na te same wyniki co Film List
this.subscription = this.filmSearchService.searchResults$.subscribe(
results => {
console.log('📥 Film Counter otrzymał nowe wyniki:', results);
this.updateStats(results);
}
);
}
private updateStats(films: any[]) {
this.filmCount = films.length;
if (films.length > 0) {
// Oblicz unikalne gatunki
this.uniqueGenres = [...new Set(films.map(f => f.genre))];
this.genreCount = this.uniqueGenres.length;
// Oblicz średni rok
const totalYears = films.reduce((sum, film) => sum + film.year, 0);
this.avgYear = Math.round(totalYears / films.length);
} else {
this.uniqueGenres = [];
this.genreCount = 0;
this.avgYear = 0;
}
}
ngOnDestroy() {
console.log('🚫 Film Counter przestaje nasłuchiwać');
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}Krok 5: Główny komponent łączący wszystko
// app.component.ts
import { Component } from '@angular/core';
import { FilmSearchComponent } from './film-search.component';
import { FilmListComponent } from './film-list.component';
import { FilmCounterComponent } from './film-counter.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [FilmSearchComponent, FilmListComponent, FilmCounterComponent],
template: `
<div class="app-container">
<h1>🎬 Komunikacja przez Serwis</h1>
<p class="description">
Komponenty rodzeństwa komunikują się przez serwis.
Spróbuj wyszukać filmy!
</p>
<!-- Komponent wysyłający -->
<app-film-search></app-film-search>
<div class="results-container">
<!-- Komponenty odbierające -->
<app-film-list></app-film-list>
<app-film-counter></app-film-counter>
</div>
</div>
`,
styles: [`
.app-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.description {
text-align: center;
color: #6c757d;
margin-bottom: 30px;
}
.results-container {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.results-container {
grid-template-columns: 1fr;
}
}
`]
})
export class AppComponent {
// Główny komponent nie musi nic robić!
// Serwis załatwia całą komunikację
}5.Jak to działa – krok po kroku
1. Użytkownik wpisuje „Spider” w wyszukiwarce
Film Search → wyszukuje filmy → znajduje "Spider-Man"2. Film Search wysyła wyniki przez serwis
this.filmSearchService.updateSearchResults([{id: 2, title: 'Spider-Man', ...}]);3. Serwis powiadamia wszystkich subskrybentów
this.searchResultsSubject.next([{id: 2, title: 'Spider-Man', ...}]);4. Film List otrzymuje wyniki i wyświetla filmy
this.films = results; // Aktualizuje listę5. Film Counter otrzymuje te same wyniki i aktualizuje statystyki
this.filmCount = results.length; // Aktualizuje licznik6. Dlaczego to działa?
Kluczowe punkty:
- Jeden serwis, wiele subskrybentów
- Serwis to „stacja radiowa”
- Komponenty to „odbiorniki radiowe”
- Jeden sygnał dociera do wszystkich odbiorników
- Automatyczne powiadomienia
- Gdy serwis wysyła nowe dane (next())
- Wszyscy subskrybenci automatycznie dostają aktualizację
- Loose coupling (luźne powiązanie)
- Komponenty nie wiedzą o sobie nawzajem
- Znają tylko serwis
- Można dodawać/usuwać komponenty bez zmian w innych
7. Najważniejsze zasady
✅ Co robić:
1.Zawsze unsubscribe w ngOnDestroy
ngOnDestroy() {
this.subscription.unsubscribe();
}2.Używaj opisowych nazw
searchResults$ // $ oznacza Observable
updateSearchResults() // jasna nazwa metody3.Loguj dla debugowania
console.log('📤 Wysyłam:', data);
console.log('📥 Otrzymuję:', data);❌ Czego unikać:
- Zapominania o unsubscribe – wyciek pamięci!
- Bezpośredniego dostępu do Subject – używaj Observable
- Modyfikowania danych w serwisie – serwis tylko przekazuje
8. Proste ćwiczenie krok po kroku
Zadanie: Stwórz komunikację „Dodaj do ulubionych”
- Serwis – dodaj metodę
addToFavorites(film) - Film Card – przycisk „Polub” wysyła film do serwisu
- Favorites List – nasłuchuje i wyświetla polubione filmy
- Favorites Counter – pokazuje liczbę polubionych
// favorites.service.ts
export class FavoritesService {
private favoritesSubject = new Subject<Film[]>();
favorites$ = this.favoritesSubject.asObservable();
private favoritesList: Film[] = [];
addToFavorites(film: Film) {
// TODO: Dodaj film do listy
// TODO: Wyślij aktualizację
}
}9. Najczęstsze błędy i rozwiązania
Błąd 1: Subscription nie działa
// ❌ Źle
this.service.data$.subscribe();
// ✅ Dobrze
this.subscription = this.service.data$.subscribe(data => {
console.log('Otrzymano:', data);
this.myData = data;
});Błąd 2: Zapomnienie o unsubscribe
// ❌ Brak unsubscribe = wyciek pamięci
// ✅ Zawsze pamiętaj
ngOnDestroy() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}Błąd 3: Modyfikowanie danych w subscribe
// ❌ Nie modyfikuj oryginalnych danych
this.service.data$.subscribe(data => {
data.push(newItem); // Źle!
});
// ✅ Stwórz kopię
this.service.data$.subscribe(data => {
this.myData = [...data, newItem]; // Dobrze!
});10. Podsumowanie
Komunikacja przez serwis to:
- 📮 Poczta między komponentami rodzeństwa
- 🔔 System powiadomień – jeden wysyła, wszyscy otrzymują
- 🔗 Luźne powiązanie – komponenty nie znają się nawzajem
Kluczowe elementy:
- Subject – nadajnik w serwisie
- Observable – odbiornik dla komponentów
- subscribe() – słuchanie zmian
- unsubscribe() – przestawanie słuchania
Pamiętaj:
- Jeden serwis może obsługiwać wielu subskrybentów
- Zawsze unsubscribe w ngOnDestroy
- Używaj console.log do debugowania
- Serwis tylko przekazuje dane, nie je modyfikuje