1. Dlaczego obrazki w aplikacji?
Obrazki to podstawowy element każdej nowoczesnej aplikacji webowej:
- Plakaty filmów w aplikacji kinowej
- Avatary użytkowników w social media
- Zdjęcia produktów w sklepie internetowym
- Ikony i logo aplikacji
- Tła i dekoracje
Gdzie mogą być przechowywane obrazki:
- 📁 Lokalnie – w folderze projektu Angular
- 🌐 Zewnętrzne URL – na innych serwerach
- ☁️ Chmura – Google Drive, Cloudinary, AWS
- 📊 Bazy danych – jako linki URL
2. Struktura folderów dla obrazków w Angular
Standardowa struktura projektu:
src/
├── app/
├── assets/ 👈 TUTAJ TRZYMAMY OBRAZKI!
│ ├── images/
│ │ ├── logos/
│ │ │ ├── logo.png
│ │ │ └── favicon.ico
│ │ ├── films/
│ │ │ ├── avengers.jpg
│ │ │ ├── batman.jpg
│ │ │ └── spider-man.jpg
│ │ ├── users/
│ │ │ ├── default-avatar.png
│ │ │ └── admin-avatar.jpg
│ │ └── backgrounds/
│ │ ├── hero-bg.jpg
│ │ └── pattern.png
│ └── icons/
│ ├── star.svg
│ ├── heart.svg
│ └── delete.svg
├── index.html
└── ...
public/ 👈 NIE UŻYWAMY DO OBRAZKÓW!
├── favicon.ico (tylko podstawowe pliki)
└── ...❓ Dlaczego NIE używamy folderu public/?
Folder public/ (jeśli istnieje) vs assets/:
| Cecha | assets/ ✅ | public/ ❌ |
|---|---|---|
| Optymalizacja | Angular kompresuje i optymalizuje | Kopiowane bez zmian |
| Cache busting | Angular dodaje hash do nazw plików | Brak automatycznego cache busting |
| Bundling | Włączone w proces budowania | Pomijane podczas budowania |
| TypeScript | Sprawdzanie ścieżek | Brak sprawdzania |
| Production ready | Automatycznie gotowe na produkcję | Wymaga ręcznej konfiguracji |
Przykład problemu z public/:
// ❌ PROBLEM z public/ - brak optymalizacji
<img src="public/images/film.jpg" alt="film">
// - Obrazek nie będzie skompresowany
// - Brak cache busting (problemy z aktualizacjami)
// - Angular nie sprawdzi czy plik istnieje
// ✅ ROZWIĄZANIE z assets/ - pełna optymalizacja
<img src="assets/images/film.jpg" alt="film">
// - Angular automatycznie optymalizuje
// - Dodaje hash do nazwy pliku (film.abc123.jpg)
// - Sprawdza czy plik istnieje podczas budowaniaDlaczego folder assets?
- Angular automatycznie kopiuje zawartość
assets/do wersji produkcyjnej - Bezpieczny dostęp – obrazki są zawsze dostępne
- Optymalizacja – Angular może kompresować obrazki podczas budowania
- Cache busting – Angular dodaje hash do nazw plików w produkcji
- Sprawdzanie błędów – Angular może wykryć brakujące obrazki podczas budowania
3. Podstawowe sposoby wyświetlania obrazków
🔧 Ważne: Różnica między src a [src]
Przed przykładami – kluczowe pojęcie:
<!-- src = STATYCZNY (zawsze ten sam) -->
<img src="assets/images/logo.jpg" alt="logo">
<!-- [src] = DYNAMICZNY (może się zmieniać) -->
<img [src]="nazwaZmiennej" alt="dynamiczny obrazek">Analogia z życia:
src= adres na kopercie – zawsze ten sam[src]= zmienna z adresem – może się zmieniać w zależności od sytuacji
Kiedy używać którego:
src– gdy obrazek nigdy się nie zmienia (logo, ikony)[src]– gdy obrazek zależy od danych (zdjęcia użytkowników, plakaty filmów)
Sposób 1: Statyczne obrazki (zawsze te same)
// film-poster.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-film-poster',
standalone: true,
template: `
<div class="poster-container">
<h3>Plakat filmu</h3>
<!-- Podstawowy sposób - statyczny obrazek -->
<!-- src = zawsze ten sam obrazek -->
<img
src="assets/images/films/avengers.jpg"
alt="Plakat Avengers"
class="film-poster">
<!-- Z opisem -->
<p>Avengers: Endgame</p>
</div>
`,
styles: [`
.poster-container {
text-align: center;
padding: 20px;
}
.film-poster {
width: 300px;
height: 450px;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
`]
})
export class FilmPosterComponent { }Sposób 2: Dynamiczne obrazki (zmieniają się)
// dynamic-poster.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-dynamic-poster',
standalone: true,
template: `
<div class="poster-container">
<h3>{{ currentFilm.title }}</h3>
<!-- Dynamiczny obrazek - zmienia się z danymi -->
<!-- [src] = może się zmieniać, zależy od zmiennej currentFilm -->
<img
[src]="currentFilm.posterUrl"
[alt]="currentFilm.title + ' plakat'"
class="film-poster"
(error)="onImageError($event)">
<!-- Przyciski do zmiany filmu -->
<div class="controls">
@for (film of films; track film.id) {
<button
(click)="selectFilm(film)"
[class.active]="currentFilm.id === film.id"
class="film-btn">
{{ film.title }}
</button>
}
</div>
</div>
`,
styles: [`
.poster-container {
text-align: center;
padding: 20px;
}
.film-poster {
width: 300px;
height: 450px;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
transition: transform 0.3s ease;
}
.film-poster:hover {
transform: scale(1.05);
}
.controls {
margin-top: 20px;
display: flex;
gap: 10px;
justify-content: center;
flex-wrap: wrap;
}
.film-btn {
padding: 8px 16px;
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.film-btn:hover {
background: #e9ecef;
}
.film-btn.active {
background: #007bff;
color: white;
}
`]
})
export class DynamicPosterComponent {
films = [
{
id: 1,
title: 'Avengers: Endgame',
posterUrl: 'assets/images/films/avengers.jpg'
},
{
id: 2,
title: 'Batman',
posterUrl: 'assets/images/films/batman.jpg'
},
{
id: 3,
title: 'Spider-Man',
posterUrl: 'assets/images/films/spider-man.jpg'
}
];
currentFilm = this.films[0]; // Domyślnie pierwszy film
selectFilm(film: any) {
this.currentFilm = film;
console.log('Wybrano film:', film.title);
}
// Obsługa błędów ładowania obrazka
onImageError(event: any) {
console.log('Błąd ładowania obrazka:', event);
// Ustaw domyślny obrazek
event.target.src = 'assets/images/films/default-poster.jpg';
}
}4. Obrazki z zewnętrznych źródeł (URL)
Przykład z zewnętrznymi URL
// external-images.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-external-images',
standalone: true,
template: `
<div class="gallery">
<h3>Obrazki z zewnętrznych źródeł</h3>
<div class="images-grid">
@for (image of externalImages; track image.id) {
<div class="image-card">
<img
[src]="image.url"
[alt]="image.title"
class="gallery-image"
(load)="onImageLoad(image)"
(error)="onImageError($event, image)">
<div class="image-info">
<h4>{{ image.title }}</h4>
<p>{{ image.description }}</p>
@if (image.loading) {
<span class="loading">Ładowanie...</span>
}
</div>
</div>
}
</div>
</div>
`,
styles: [`
.gallery {
padding: 20px;
}
.images-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.image-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s;
}
.image-card:hover {
transform: translateY(-4px);
}
.gallery-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.image-info {
padding: 15px;
}
.loading {
color: #007bff;
font-style: italic;
}
`]
})
export class ExternalImagesComponent {
externalImages = [
{
id: 1,
title: 'Krajobraz górski',
description: 'Piękne góry w słońcu',
url: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400',
loading: true
},
{
id: 2,
title: 'Zachód słońca',
description: 'Romantyczny zachód słońca',
url: 'https://images.unsplash.com/photo-1495616811223-4d98c6e9c869?w=400',
loading: true
},
{
id: 3,
title: 'Las jesienią',
description: 'Kolorowy las jesienią',
url: 'https://images.unsplash.com/photo-1441974231531-c6227db76b6e?w=400',
loading: true
}
];
onImageLoad(image: any) {
console.log('Obrazek załadowany:', image.title);
image.loading = false;
}
onImageError(event: any, image: any) {
console.log('Błąd ładowania:', image.title);
image.loading = false;
// Ustaw domyślny obrazek
event.target.src = 'assets/images/default-image.jpg';
}
}5. Obrazki z danymi dynamicznymi
Przykład: Lista filmów z plakatami
// films-gallery.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-films-gallery',
standalone: true,
template: `
<div class="films-gallery">
<h2>Galeria filmów</h2>
<!-- Pasek wyszukiwania -->
<div class="search-bar">
<input
type="text"
#searchInput
(input)="searchTerm = searchInput.value; filterFilms()"
placeholder="Wyszukaj film..."
class="search-input">
<span class="results-count">Znaleziono: {{ filteredFilms.length }} filmów</span>
</div>
<!-- Galeria filmów -->
<div class="films-grid">
@if (filteredFilms.length === 0) {
<div class="no-results">
<img
src="assets/images/icons/search-empty.svg"
alt="Brak wyników"
class="empty-icon">
<h3>Brak filmów</h3>
<p>Nie znaleziono filmów pasujących do "{{ searchTerm }}"</p>
</div>
} @else {
@for (film of filteredFilms; track film.id) {
<div class="film-card" (click)="selectFilm(film)">
<!-- Obrazek filmu -->
<div class="image-container">
<img
[src]="film.posterUrl"
[alt]="film.title + ' plakat'"
class="film-image"
(error)="setDefaultPoster($event)">
<!-- Nakładka z oceną -->
<div class="rating-overlay">
<span class="rating">{{ film.rating }}</span>
<img src="assets/images/icons/star.svg" alt="gwiazdka" class="star-icon">
</div>
<!-- Znacznik "Nowość" -->
@if (film.isNew) {
<div class="new-badge">
NOWOŚĆ!
</div>
}
</div>
<!-- Informacje o filmie -->
<div class="film-info">
<h3>{{ film.title }}</h3>
<p class="year">{{ film.year }}</p>
<p class="genre">{{ film.genre }}</p>
</div>
</div>
}
}
</div>
<!-- Szczegóły wybranego filmu -->
@if (selectedFilm) {
<div class="film-details">
<button (click)="closeDetails()" class="close-btn">×</button>
<div class="details-content">
<img
[src]="selectedFilm.posterUrl"
[alt]="selectedFilm.title"
class="details-poster">
<div class="details-info">
<h2>{{ selectedFilm.title }}</h2>
<p><strong>Rok:</strong> {{ selectedFilm.year }}</p>
<p><strong>Gatunek:</strong> {{ selectedFilm.genre }}</p>
<p><strong>Ocena:</strong> {{ selectedFilm.rating }}/10</p>
<p><strong>Opis:</strong> {{ selectedFilm.description }}</p>
</div>
</div>
</div>
}
</div>
`,
styles: [`
.films-gallery {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.search-bar {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.search-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.results-count {
color: #6c757d;
font-size: 14px;
}
.films-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 25px;
}
.film-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: all 0.3s ease;
cursor: pointer;
}
.film-card:hover {
transform: translateY(-8px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.image-container {
position: relative;
height: 300px;
overflow: hidden;
}
.film-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.film-card:hover .film-image {
transform: scale(1.1);
}
.rating-overlay {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.8);
color: white;
padding: 5px 10px;
border-radius: 20px;
display: flex;
align-items: center;
gap: 5px;
}
.star-icon {
width: 16px;
height: 16px;
filter: brightness(0) invert(1);
}
.new-badge {
position: absolute;
top: 10px;
left: 10px;
background: #ff4757;
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.film-info {
padding: 20px;
}
.film-info h3 {
margin: 0 0 8px 0;
color: #333;
}
.year {
color: #6c757d;
margin: 4px 0;
}
.genre {
color: #007bff;
font-weight: 500;
margin: 4px 0;
}
.no-results {
grid-column: 1 / -1;
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.empty-icon {
width: 80px;
height: 80px;
opacity: 0.5;
margin-bottom: 20px;
}
.film-details {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.details-content {
background: white;
border-radius: 12px;
max-width: 600px;
max-height: 80vh;
overflow: auto;
position: relative;
display: flex;
gap: 20px;
padding: 30px;
}
.close-btn {
position: absolute;
top: 10px;
right: 15px;
background: none;
border: none;
font-size: 30px;
cursor: pointer;
color: #666;
}
.details-poster {
width: 200px;
height: 300px;
object-fit: cover;
border-radius: 8px;
}
.details-info {
flex: 1;
}
@media (max-width: 768px) {
.details-content {
flex-direction: column;
margin: 20px;
padding: 20px;
}
.details-poster {
width: 100%;
height: 250px;
}
}
`]
})
export class FilmsGalleryComponent {
allFilms = [
{
id: 1,
title: 'Avengers: Endgame',
year: 2019,
genre: 'Akcja',
rating: 8.4,
posterUrl: 'assets/images/films/avengers.jpg',
isNew: false,
description: 'Finałowa walka z Thanosem. Bohaterowie próbują odwrócić skutki Blip i przywrócić połowę wszechświata.'
},
{
id: 2,
title: 'Spider-Man: No Way Home',
year: 2021,
genre: 'Akcja',
rating: 8.2,
posterUrl: 'assets/images/films/spider-man.jpg',
isNew: true,
description: 'Peter Parker zmaga się z ujawnieniem swojej tożsamości i prosi o pomoc Doctor Strange.'
},
{
id: 3,
title: 'The Batman',
year: 2022,
genre: 'Kryminał',
rating: 7.8,
posterUrl: 'assets/images/films/batman.jpg',
isNew: true,
description: 'Młody Bruce Wayne ściga seryjnego mordercę Riddlera w mrocznym Gotham.'
}
];
filteredFilms = [...this.allFilms];
searchTerm = '';
selectedFilm: any = null;
filterFilms() {
if (this.searchTerm.trim() === '') {
this.filteredFilms = [...this.allFilms];
} else {
this.filteredFilms = this.allFilms.filter(film =>
film.title.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
film.genre.toLowerCase().includes(this.searchTerm.toLowerCase())
);
}
}
selectFilm(film: any) {
this.selectedFilm = film;
console.log('Wybrano film:', film.title);
}
closeDetails() {
this.selectedFilm = null;
}
setDefaultPoster(event: any) {
console.log('Błąd ładowania plakatu');
event.target.src = 'assets/images/films/default-poster.jpg';
}
}6. Optymalizacja obrazków
Lazy Loading – ładowanie na żądanie
// lazy-images.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-lazy-images',
standalone: true,
template: `
<div class="lazy-gallery">
<h3>Lazy Loading - obrazki ładują się gdy są potrzebne</h3>
<div class="images-list">
@for (image of images; track image.id) {
<div class="image-item">
<!-- loading="lazy" = ładuje tylko gdy użytkownik przewija do obrazka -->
<img
[src]="image.url"
[alt]="image.title"
loading="lazy"
class="lazy-image">
<p>{{ image.title }}</p>
</div>
}
</div>
</div>
`,
styles: [`
.lazy-gallery {
padding: 20px;
}
.images-list {
display: flex;
flex-direction: column;
gap: 30px;
}
.image-item {
text-align: center;
}
.lazy-image {
width: 100%;
max-width: 600px;
height: 400px;
object-fit: cover;
border-radius: 8px;
}
`]
})
export class LazyImagesComponent {
images = [
{ id: 1, title: 'Obrazek 1', url: 'assets/images/gallery/image1.jpg' },
{ id: 2, title: 'Obrazek 2', url: 'assets/images/gallery/image2.jpg' },
{ id: 3, title: 'Obrazek 3', url: 'assets/images/gallery/image3.jpg' },
// ... więcej obrazków
];
}7. Najlepsze praktyki
✅ Co robić:
- Używaj folderu assets/ – Angular automatycznie kopiuje do produkcji
- Zawsze dodawaj alt – dla dostępności
- Obsługuj błędy – (error) z fallback obrazkiem
- Używaj object-fit: cover – dla spójnych rozmiarów
- Kompresuj obrazki – mniejsze pliki = szybsze ładowanie
- Używaj loading=”lazy” – dla lepszej wydajności
❌ Czego unikać:
- Za duże pliki – kompresuj obrazki przed dodaniem
- Brak alt – utrudnia dostępność
- Twarde kodowanie ścieżek – używaj zmiennych
- Brak obsługi błędów – zawsze przewiduj że obrazek może się nie załadować
Przykład dobrej praktyki:
// ✅ Dobra praktyka
export class GoodPracticeComponent {
readonly DEFAULT_POSTER = 'assets/images/default-poster.jpg';
readonly POSTER_PATH = 'assets/images/films/';
getImageUrl(filename: string): string {
return this.POSTER_PATH + filename;
}
onImageError(event: any) {
event.target.src = this.DEFAULT_POSTER;
}
}9. Ćwiczenia do wykonania
- Galeria zdjęć – stwórz galerię z możliwością powiększania
- Avatar użytkownika – domyślny avatar + możliwość zmiany
- Karta produktu – główne zdjęcie + miniaturki
- Slider obrazków – przełączanie między obrazkami