Skip to content

Commit d5ab91f

Browse files
MitkoTschimevedoparearyee
authored andcommitted
feat(image-loader): Set image asynchronously and placeholder immediately
1 parent d03a9d5 commit d5ab91f

File tree

4 files changed

+141
-11
lines changed

4 files changed

+141
-11
lines changed

src/app/app.component.html

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,22 @@ <h3>Bottom image</h3>
2323
#imgEl>
2424
</sn-image-loader>
2525
<h3>Bottom image imageLoaded event count: <span class="image-loaded-count">{{ imageLoadedEventCount }}</span></h3>
26+
27+
<h1>Scroll down for async image</h1>
28+
<h3>Async image placeholder loaded: <span class="placeholder-loaded">{{ placeholderAsyncImageLoaded }}</span></h3>
29+
<div class="spacer"></div>
30+
31+
<h3>Async image data after 3 seconds (placeholder immediately)</h3><button (click)="startAsyncLoading()">Start timer</button>
32+
<sn-image-loader
33+
[image]="asyncImage"
34+
[sizes]="sizes"
35+
imgClass="img img--bottom"
36+
alt="lorem ipsum"
37+
(placeholderLoaded)="onAsyncImagePlaceholderLoad($event)"
38+
(imageLoaded)="onAsyncImageLoad($event)"
39+
class="sn-image-loader sn-image-loader--bottom">
40+
</sn-image-loader>
41+
<h3>Async image imageLoaded event count: <span class="image-loaded-count">{{ asyncImageLoadedEventCount }}</span></h3>
2642
<div class="spacer"></div>
2743

2844
<sn-video-loader

src/app/app.component.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component } from '@angular/core';
1+
import { Component, OnInit } from '@angular/core';
22

33
import { Breakpoint } from './image-loader/shared/breakpoint.model';
44
import { ResponsiveImage, Size } from './image-loader/shared/image.model';
@@ -11,10 +11,11 @@ import { video, image, sizes } from './app-data';
1111
templateUrl: './app.component.html',
1212
styleUrls: ['./app.component.scss'],
1313
})
14-
export class AppComponent {
14+
export class AppComponent implements OnInit {
1515
sizes: Breakpoint[] = sizes;
1616
image: ResponsiveImage = image;
1717
video: ResponsiveVideo = video;
18+
asyncImage: ResponsiveImage;
1819

1920
/**
2021
* Set to true on placeholder loaded event.
@@ -24,6 +25,14 @@ export class AppComponent {
2425
*/
2526
placeholderLoaded = false;
2627

28+
/**
29+
* Set to true on placeholder loaded event.
30+
*
31+
* @type {boolean}
32+
* @memberof AppComponent
33+
*/
34+
placeholderAsyncImageLoaded = false;
35+
2736
/**
2837
* Incremented on each image load event.
2938
*
@@ -32,6 +41,14 @@ export class AppComponent {
3241
*/
3342
imageLoadedEventCount = 0;
3443

44+
/**
45+
* Incremented on each async image load event.
46+
*
47+
* @type {number}
48+
* @memberof AppComponent
49+
*/
50+
asyncImageLoadedEventCount = 0;
51+
3552
/**
3653
* Increments event count on each image loaded event.
3754
* Counter displayed in component template.
@@ -42,6 +59,16 @@ export class AppComponent {
4259
this.placeholderLoaded = true;
4360
}
4461

62+
/**
63+
* Increments event count on each image loaded event.
64+
* Counter displayed in component template.
65+
*
66+
* @memberof AppComponent
67+
*/
68+
public onAsyncImagePlaceholderLoad(imageLoadedEvent: ImageLoadedEvent) {
69+
this.placeholderAsyncImageLoaded = true;
70+
}
71+
4572
/**
4673
* Increments event count on each image loaded event.
4774
* Counter displayed in component template.
@@ -51,4 +78,48 @@ export class AppComponent {
5178
public onImageLoad(imageLoadedEvent: ImageLoadedEvent) {
5279
this.imageLoadedEventCount++;
5380
}
81+
82+
/**
83+
* Increments event count on each image loaded event.
84+
* Counter displayed in component template.
85+
*
86+
* @memberof AppComponent
87+
*/
88+
public onAsyncImageLoad(imageLoadedEvent: ImageLoadedEvent) {
89+
this.asyncImageLoadedEventCount++;
90+
}
91+
92+
/**
93+
* Resets async image data and simulates loading
94+
*
95+
* @memberof AppComponent
96+
*/
97+
public startAsyncLoading() {
98+
this.asyncImageLoadedEventCount = 0;
99+
this.placeholderAsyncImageLoaded = false;
100+
this.loadAsyncImageData();
101+
}
102+
103+
ngOnInit() {
104+
this.asyncImage = {
105+
placeholder: image.placeholder,
106+
images: [],
107+
fallback: image.fallback,
108+
};
109+
}
110+
111+
/**
112+
* Sets a placeholder for the image and simulates loading of image data (typically from some API endpoint)
113+
* After 3 seconds the image data is set
114+
*/
115+
private loadAsyncImageData() {
116+
this.asyncImage = {
117+
placeholder: image.placeholder,
118+
images: [],
119+
fallback: image.fallback,
120+
};
121+
setTimeout(() => {
122+
this.asyncImage = image;
123+
}, 3000);
124+
}
54125
}

src/app/image-loader/image-loader/image-loader.component.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,45 @@ describe('ImageLoaderComponent', () => {
219219
component.ngOnChanges();
220220
expect(spy).toHaveBeenCalled();
221221
});
222+
223+
describe('async image', () => {
224+
let spyPreloadImage, spySetPlaceholder;
225+
226+
beforeEach(() => {
227+
spyPreloadImage = spyOn(component, 'preloadImage').and.callThrough();
228+
spySetPlaceholder = spyOn(component, 'setPlaceholder').and.callThrough();
229+
230+
component.image = {
231+
placeholder: 'someplaceholder',
232+
images: null,
233+
fallback: null,
234+
};
235+
component.inViewport = true;
236+
component.loaded = false;
237+
});
238+
239+
it('should not preload image if no images are set and is in viewport', () => {
240+
component.ngOnChanges();
241+
expect(spyPreloadImage).toHaveBeenCalled();
242+
expect(spySetPlaceholder).toHaveBeenCalled();
243+
expect(component.preloadSrc).toEqual('');
244+
expect(component.preloadSrcset).toEqual('');
245+
});
246+
247+
it('should preload image after already scrolled in and set later on', () => {
248+
component.ngOnChanges();
249+
expect(component.preloadSrc).toEqual('');
250+
expect(component.preloadSrcset).toEqual('');
251+
expect(component.src).toEqual('someplaceholder');
252+
253+
component.image = image;
254+
component.ngOnChanges();
255+
expect(component.preloadSrc).toEqual(
256+
'http://via.placeholder.com/150x350?text=xs+1x',
257+
);
258+
expect(component.preloadSrcset).toEqual(
259+
'http://via.placeholder.com/150x350?text=xs+1x 1x, http://via.placeholder.com/300x700?text=xs+2x 2x',
260+
);
261+
});
262+
});
222263
});

src/app/image-loader/image-loader/image-loader.component.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -250,16 +250,18 @@ export class ImageLoaderComponent
250250
*/
251251
public preloadImage(): void {
252252
if (this.inViewport && this.notLoaded) {
253-
const retinaImg = this.image.images.find(
254-
retinaImage => retinaImage.size === this.size,
255-
);
256-
const imageNormal = retinaImg.x1;
257-
const imageRetina = retinaImg.x2;
258-
if ('srcset' in this.img.nativeElement) {
259-
this.supportsSrcSet = true;
253+
const retinaImg =
254+
this.image.images &&
255+
this.image.images.find(retinaImage => retinaImage.size === this.size);
256+
if (retinaImg) {
257+
const imageNormal = retinaImg.x1;
258+
const imageRetina = retinaImg.x2;
259+
if ('srcset' in this.img.nativeElement) {
260+
this.supportsSrcSet = true;
261+
}
262+
this.preloadSrcset = `${imageNormal} 1x, ${imageRetina} 2x`;
263+
this.preloadSrc = imageNormal;
260264
}
261-
this.preloadSrcset = `${imageNormal} 1x, ${imageRetina} 2x`;
262-
this.preloadSrc = imageNormal;
263265
}
264266
}
265267
/**

0 commit comments

Comments
 (0)