Baza wiedzy
Jednokierunkowe wiązanie danych z atrybutem komponentu wydaje się w Angular proste. Są jednak sytuacje, jak w przypadku komponentu img, do którego chcemy załadować obrazek dynamicznie i asynchronicznie, że wymaga to użycia kilku dodatkowych mechanizmów.
Problem postawiony w temacie wydaje się oczywisty. Wystarczy w szablonie napisać tak:
<img [src]="imgSrc" />
a w klasie komponentu:
private imgSrc = '/obrazek.jpg';
A co jeżeli wartośćimgSrc
nie jest od razu znana - np. pobieramy ją
z serwera? Wtedy sprawa też wydaje się prosta - otrzymujemy z serwera url obrazka i tą wartość podstawiamy doimgSrc
.
Wyglądałoby to mniej więcej tak, że piszemy w klasie serwisowej metodę do pobierania urla obrazka:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class ImgUrlService {
constructor(private http: HttpClient) { }
getImageUrl(): Observable<string> {
return this.http.get<string>('url_do_metody_restowej');
}
}
A w komponencie:
export class TestComponent implements AfterViewInit {
private imgSrc = '/obrazek.jpg';
constructor(private imgUrlService: ImgUrlService) {}
ngAfterViewInit(): void {
this.imgUrlService.getImageUrl().subscribe(imgUrl => {
this.imgSrc = imgUrl;
});
}
}
Powyższe będzie działać pod warunkiem, że na serwerze znajduje się dodatkowo metoda, która na podstawie przysłanego wcześniej url-a jest w stanie odesłać zawartość obrazka.
Co jeśli takiej metody nie mamy, a zamiast tego istnieje metoda, która przysyła obrazek w postaci zakodowanej, np. base64? Wówczas po stronie przeglądarki musimy wykonać pewne czynności, zanim taki obrazek będzie mógł być użyty przez przeglądarkę w elemencieimg
. W tym celu musimy zmodyfikować przede wszystkim metodę usługową:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { DomSanitizer } from '@angular/platform-browser';
import { SafeUrl } from '@angular/platform-browser/src/security/dom_sanitization_service';
@Injectable()
export class ImgUrlService {
constructor(private http: HttpClient, private sanitizer: DomSanitizer) { }
getImageUrl(): Observable<SafeUrl> {
return this.http.get('url_do_metody_restowej', {
responseType: 'blob'
}).map(response => {
return this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(response));
});
}
}
Na podstawie przesłanego ciągu bajtów tworzymy specjalny url, który wskazuje na obiekt w pamięci. Dodatkowo, aby Angular nie protestował, że utworzony w ten sposób url jest niebezpieczny, musimy wywołać metodębypassSecurityTrustUrl
.
Teraz czas na zmianę w szablonie komponentu, w którym przy okazji skorzystamy z interesującego filtruasync
, aby Angular wiedział, że podana wartość jest pobierana asynchronicznie.:
<img [src]="imgSrc | async" />
Filtr ten wymaga, aby podane wyrażenie było typuPromise
, alboObservable
. Ponieważ chcemy być nowocześni, w naszym przypadku skorzystamy zObservable
:
export class TestComponent implements AfterViewInit {
private imgSrc: Observable<SafeUrl>;
constructor(private imgUrlService: ImgUrlService) { }
ngAfterViewInit(): void {
this.imgSrc = this.imgUrlService.getImageUrl();
}
}
Widać, że kod komponentu zrobił się przyjemniejszy, mamy proste przypisanie wyniku zwracanego przez metodęgetImageUrl
do polaimgSrc
. Niestety powyższy kod generuje pewien problem. Zanim metodagetImageUrl
zwróci wartość, "obserwabla"imgSrc
nie będzie zawierała wartości, a co za tym idzie przeglądarka spróbuje pobrać obrazek z adresu http://localhost:4200/null. Raczej nie mamy zasobu o takim adresie, więc w konsoli przeglądarki pojawi się błąd. Dla użytkownika będzie on niewidoczny, niemniej nie wygląda to najlepiej. Źródłowym problemem jest tutaj brak możliwości utworzenia obiektu typuObservable
z inicjalną wartością.
Na szczęście biblioteka rxjs, z której korzysta Angular, zawiera szereg definicji klas, z których jedna jest dla nas interesująca -BehaviorSubject
. Jest to o tyle ciekawy obiekt, że po pierwsze działa jak obiektObservable
, czyli taki, do którego możemy się podłączyć metodąsubscribe
i oczekiwać na wynik. Po drugie działa jak "zlew", do którego możemy sekwencyjnie wlewać pewne wartości, które będą odbierane w metodziesubscribe
- również sekwencyjnie. Jeżeli do tej pory nie miałeś(aś) wiele wspólnego z biblioteką rxjs, ale wiesz jak działają "obietnice" (Promise
), to najważniejsza różnica o której musisz wiedzieć to fakt, że obiekty typuObservable
mogą generować wiele wartości w odróżnieniu odPromise
, które zawsze generuje tylko jedną wartość. Podobne zachowanie doBehaviorSubject
ma obiektSubject
. Jak można wnioskować na podstawie nazw, klasaBehaviorSubject
rozszerza klasęSubject
właśnie poprzez możliwość zdefiniowania inicjalnej wartości. To nam załatwia w/w problem z null-em. W tym celu stworzymy sobie obiektBehaviorSubject
z jakimś inicjalnym obrazkiem, najlepiej tzw. pustym obrazkiem, którego reprezentację można sobie wygenerować w wielu dostępnych w internecie generatorach.
Ostatecznie nasz komponent będzie wyglądać tak:
export class TestComponent implements OnInit, AfterViewInit {
private imgSrc: BehaviorSubject<SafeUrl>;
constructor(private imgUrlService: ImgUrlService) { }
ngOnInit(): void {
this.imgSrc = new BehaviorSubject('data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=');
}
ngAfterViewInit(): void {
this.imgUrlService.getImageUrl().subscribe(imgUrl => {
this.imgSrc.next(imgUrl);
});
}
}
Jak widać używamy metody next, za pomocą której "wlewamy" do naszego "zlewu" kolejną wartość, która zostanie użyta przez Angular-a do zasilenia atrybutu src.
W powyższym przykładzie pobieramy obrazek tylko jednokrotnie, po zainicjowaniu naszego komponentu, ale nic nie stoi na przeszkodzie, aby wywołaćthis.imgSrc.next
w innym miejscu.
Zainteresował Cię ten wpis?
Chcesz dowiedzieć się więcej?
Michał Gierwatowski
Programista wszechstronny, od języka Progress4GL począwszy, przez Javę, na TypeScripcie kończąc. Ponad piętnastoletnie doświadczenie w wytwarzaniu różnego rodzaju systemów informatycznych. Ostatnio interesuje się nowinkami w ekosystemie JavaScript/node.js
Michał.Gierwatowski(at)monolit-it.pl
Zobacz wszystkie artykuły danego autora »