Monolit-IT - Blog - szczegóły

Baza wiedzy

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

17.01.2018

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śćimgSrcnie 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ęgetImageUrldo polaimgSrc. Niestety powyższy kod generuje pewien problem. Zanim metodagetImageUrlzwróci wartość, "obserwabla"imgSrcnie 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 typuObservablez 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ąsubscribei 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 typuObservablemogą generować wiele wartości w odróżnieniu odPromise, które zawsze generuje tylko jedną wartość. Podobne zachowanie doBehaviorSubjectma obiektSubject. Jak można wnioskować na podstawie nazw, klasaBehaviorSubjectrozszerza klasęSubjectwłaśnie poprzez możliwość zdefiniowania inicjalnej wartości. To nam załatwia w/w problem z null-em. W tym celu stworzymy sobie obiektBehaviorSubjectz 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.nextw innym miejscu.

Zainteresował Cię ten wpis?
Chcesz dowiedzieć się więcej?

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 »