Monolit-IT - Blog

17.01.2018

Angular. Dynamiczny atrybut src w elemencie img z użyciem rxjs

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 do imgSrc.

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 elemencie img. 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 filtru async, aby Angular wiedział, że podana wartość jest pobierana asynchronicznie.:

<img [src]="imgSrc | async" />


Filtr ten wymaga, aby podane wyrażenie było typu Promise, albo Observable. Ponieważ chcemy być nowocześni, w naszym przypadku skorzystamy z Observable:

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 pola imgSrc. Niestety powyższy kod generuje pewien problem. Zanim metoda getImageUrl 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 typu Observable 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 obiekt Observable, 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 metodzie subscribe - 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 typu Observable mogą generować wiele wartości w odróżnieniu od Promise, które zawsze generuje tylko jedną wartość. Podobne zachowanie do BehaviorSubject ma obiekt Subject. Jak można wnioskować na podstawie nazw, klasa BehaviorSubject 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 obiekt BehaviorSubject 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.

Michał Gierwatowski

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 >>

Blog technologiczny Monolit IT

W Monolit IT chętnie dzielimy się wiedzą z zakresu technologii, rozwiązań IT, programowania, projektów czy budowy infrastruktury informatycznej. Naszego bloga piszemy sami. Macie wiedzę z pierwszej ręki od naszych inżynierów i wdrożeniowców z wieloletnim doświadczeniem. Zapraszamy!

czytaj więcej

kontakt

Z naszą wiedzą i doświadczeniem tworzymy profesjonalne i niezawodne rozwiązania IT.

Monolit IT Sp z o.o. Sąd Rejonowy Gdańsk-Północ w Gdańsku VIII Wydział Gospodarczy Krajowego Rejestru Sądowego, KRS: 0000280291 NIP: 958-155-93-85 REGON: 220431534 Wysokość kapitału zakładowego: 50 500 PLN





Monolit IT Sp. z o.o.
81-341 Gdynia
ul. Warsztatowa 12

tel: +48 58 763 30 00
fax: +48 58 763 30 10
e-mail: biuro@monolit-it.pl

Mapa Serwisu
Polityka Cookie
Projekty Unijne



Wszelkie Prawa zastrzeżone ©2018