Skip to content

Commit

Permalink
feat: use native lazy loading instead of custom one
Browse files Browse the repository at this point in the history
The custom loading worked more or less fine but it was only a temporary
solution before loading="lazy" got better browser support. That moment
has finally come and switching to it allows for removing a lot of code!

The downside is that write now I don't have a replacement for loading
the prev/next item. I think I could make it work by somehow exposing the
items on the sides in the scrollport, but I will deal with that once the
need arises.
  • Loading branch information
daelmaak committed Aug 6, 2023
1 parent d210c66 commit c26e75a
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 309 deletions.
101 changes: 51 additions & 50 deletions projects/gallery/src/lib/components/viewer/viewer.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,64 +23,65 @@
(keydown.Tab)="onTab(i + 1)"
(keydown.shift.Tab)="onTab(i - 1)"
>
<ng-container *ngIf="!lazyLoading || isInScrollportProximity(i)">
<ng-container *ngIf="!itemTemplate; else customTemplate">
<picture *ngIf="!item.video" @mediaAnimate>
<source
*ngFor="let source of item.pictureSources"
[srcset]="source.srcset"
[attr.media]="source.media"
[attr.sizes]="source.sizes"
[attr.type]="source.type"
/>
<img
[src]="item.src"
[alt]="item.alt"
[class.viewer-media-loading]="!itemLoaded(item)"
[style.objectFit]="objectFit"
/>
</picture>
<!-- Using loadedmetadata instead of loadeddata because iOS loads data lazily upon user's interaction -->
<video
*ngIf="!isYoutube(item) && item.video"
@mediaAnimate
<ng-container *ngIf="!itemTemplate; else customTemplate">
<picture *ngIf="!item.video" @mediaAnimate>
<source
*ngFor="let source of item.pictureSources"
[srcset]="source.srcset"
[attr.media]="source.media"
[attr.sizes]="source.sizes"
[attr.type]="source.type"
/>
<img
[src]="item.src"
[poster]="item.thumbSrc || ''"
[alt]="item.alt"
[attr.loading]="loading"
[class.viewer-media-loading]="!itemLoaded(item)"
[style.objectFit]="objectFit"
controls
playsinline
></video>
/>
</picture>
<!-- Using loadedmetadata instead of loadeddata because iOS loads data lazily upon user's interaction -->
<video
*ngIf="!isYoutube(item) && item.video"
@mediaAnimate
[src]="item.src"
[poster]="item.thumbSrc || ''"
[attr.preload]="loading === 'lazy' ? 'metadata' : 'auto'"
[class.viewer-media-loading]="!itemLoaded(item)"
[style.objectFit]="objectFit"
controls
playsinline
></video>

<iframe
*ngIf="isYoutube(item)"
@mediaAnimate
[src]="item.src | safe"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</ng-container>

<ng-container *ngIf="!itemLoaded(item) && !itemFailedToLoad(item)">
<ng-container *ngTemplateOutlet="loadingTemplate"></ng-container>
</ng-container>
<iframe
*ngIf="isYoutube(item)"
@mediaAnimate
[attr.loading]="loading"
[src]="item.src | safe"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
></iframe>
</ng-container>

<ng-container *ngIf="itemFailedToLoad(item)">
<div *ngIf="!errorTemplate; else errorTemplate" class="viewer-error">
<div class="viewer-error-icon"></div>
<p class="viewer-error-text">
{{ errorText || 'Loading of this media failed' }}
</p>
</div>
</ng-container>
<ng-container *ngIf="!itemLoaded(item) && !itemFailedToLoad(item)">
<ng-container *ngTemplateOutlet="loadingTemplate"></ng-container>
</ng-container>

<span
[id]="'viewer-aria-description-' + i"
class="sr-only"
[innerHTML]="item.description"
></span>
<ng-container *ngIf="itemFailedToLoad(item)">
<div *ngIf="!errorTemplate; else errorTemplate" class="viewer-error">
<div class="viewer-error-icon"></div>
<p class="viewer-error-text">
{{ errorText || 'Loading of this media failed' }}
</p>
</div>
</ng-container>

<span
[id]="'viewer-aria-description-' + i"
class="sr-only"
[innerHTML]="item.description"
></span>

<ng-template #customTemplate>
<ng-container
*ngTemplateOutlet="
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,125 +17,6 @@ describe('ViewerComponent', () => {
viewer.selectedIndex = 0;
});

describe('scroll proximity', () => {
describe('in loop mode', () => {
beforeEach(() => {
viewer.touched = true;
viewer.loop = true;
viewer['_viewerWidth'] = 600;
viewer['_itemWidth'] = 600;
viewer['_fringeCount'] = 1;
});

it('should consider selected item in proximity', () => {
expect(viewer.isInScrollportProximity(1)).toBeTruthy();
});

it('should consider fringe item in proximity if selected item next to fringe', () => {
expect(viewer.isInScrollportProximity(0)).toBeTruthy();
});

it('should consider regular item in proximity if selected item is just next to it', () => {
expect(viewer.isInScrollportProximity(2)).toBeTruthy();
});

it('should consider fringe item on the opposite side of slider in proximity if first item selected', () => {
expect(viewer.isInScrollportProximity(5)).toBeTruthy();
});

it('should consider fringe item on the opposite side of slider in proximity if last item selected', () => {
viewer.selectedIndex = 3;
expect(viewer.isInScrollportProximity(0)).toBeTruthy();
});

it('should consider regular item on the opposite side of slider in proximity if first item selected', () => {
expect(viewer.isInScrollportProximity(4)).toBeTruthy();
});

it('should consider regular items in the middle, which are not fringe nor next to selected item, as out of proximity', () => {
expect(viewer.isInScrollportProximity(3)).toBeFalsy();
});

it(`should display number of items adjacent to the selected, the number being ceiled amount of displayed items.
If 2.2 items are displayed, then 3 adjacent items are displayed next to the selected item`, () => {
viewer.items.push({ src: 'src5' });
viewer.items.push({ src: 'src6' });
viewer.items.push({ src: 'src7' });
viewer.items.push({ src: 'src8' });
viewer.items.push({ src: 'src9' });
viewer['_itemWidth'] = 600 / 2.5;
viewer['_fringeCount'] = 3;

expect(viewer.isInScrollportProximity(4)).toBeTruthy();
expect(viewer.isInScrollportProximity(5)).toBeTruthy();
expect(viewer.isInScrollportProximity(6)).toBeTruthy();
expect(viewer.isInScrollportProximity(7)).toBeFalsy();
expect(viewer.isInScrollportProximity(8)).toBeFalsy();
});

it('should load just 1 item on each side if the item is only by a fraction of a pixel smaller than viewer', () => {
viewer.selectedIndex = 2;
viewer['_itemWidth'] = 599.5;
viewer['_fringeCount'] = 1;

// fringe item representing the last item, should be loaded
expect(viewer.isInScrollportProximity(0)).toBeTruthy();
// first item, too far from selected, shouldn't be loaded
expect(viewer.isInScrollportProximity(1)).toBeFalsy();
// items around selected item, should be loaded
expect(viewer.isInScrollportProximity(2)).toBeTruthy();
expect(viewer.isInScrollportProximity(3)).toBeTruthy();
expect(viewer.isInScrollportProximity(4)).toBeTruthy();
// fringe item representing the first item, shouldn't be loaded
expect(viewer.isInScrollportProximity(5)).toBeFalsy();
});
});

describe('in non-loop mode', () => {
beforeEach(() => {
viewer.loop = false;
viewer['_viewerWidth'] = 600;
viewer['_itemWidth'] = 600;
viewer['_fringeCount'] = 0;
});

it('should not display last items like in loop mode when first item selected', () => {
viewer['_itemWidth'] = 600 / 2.5;
viewer.items.push({ src: 'src5' });
viewer.items.push({ src: 'src6' });
expect(viewer.isInScrollportProximity(4)).toBeFalsy();
});
});

describe('untouched', () => {
beforeEach(() => {
viewer.loop = true;
viewer.touched = false;
viewer['_viewerWidth'] = 600;
viewer['_itemWidth'] = 600;
viewer['_fringeCount'] = 1;
});

it(`should load only 1 item
if only 1 item is visible`, () => {
expect(viewer.isInScrollportProximity(0)).toBeFalsy();
expect(viewer.isInScrollportProximity(1)).toBeTruthy();
expect(viewer.isInScrollportProximity(2)).toBeFalsy();
});

it(`should load only 3 items
if 3 items are visible, although partly`, () => {
viewer['_itemWidth'] = 600 / 1.5;
viewer['_fringeCount'] = 2;
expect(viewer.isInScrollportProximity(0)).toBeFalsy();
expect(viewer.isInScrollportProximity(1)).toBeTruthy();
expect(viewer.isInScrollportProximity(2)).toBeTruthy();
expect(viewer.isInScrollportProximity(3)).toBeTruthy();
expect(viewer.isInScrollportProximity(4)).toBeFalsy();
});
});
});

describe('fringe items count in loop mode', () => {
beforeEach(() => {
viewer.loop = true;
Expand Down
118 changes: 0 additions & 118 deletions projects/gallery/src/lib/components/viewer/viewer.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,124 +123,6 @@ describe('ViewerComponent', () => {
});
});

describe('item loading', () => {
let changes: SimpleChanges;

beforeEach(() => {
component.touched = true;
component.items = generateGalleryImages(4);
component.loading = 'lazy';
component.selectedIndex = 0;
changes = {
items: new SimpleChange(null, component.items, true),
};
});

it('should display all items if default loading on', fakeAsync(() => {
component.loading = 'auto';
component.ngOnChanges(changes);
fixture.detectChanges();
tick();

const items = viewerDe.queryAll(By.css('li picture'));

expect(items.length).toBe(4);
}));

it('should not display item outside scroll proximity if lazy loading on', fakeAsync(() => {
component.ngOnChanges(changes);
fixture.detectChanges();
tick();

const items = viewerDe.queryAll(By.css('li picture'));

expect(items.length).toBe(2);
}));

it('should display items if lazy loading on and items are in scroll proximity', fakeAsync(() => {
component.ngOnChanges(changes);
fixture.detectChanges();
tick();

const itemList = viewerDe.query(By.css('ul'));

expect(itemList.query(By.css('img[src=src1]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src2]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src3]'))).toBeFalsy();
expect(itemList.query(By.css('img[src=src4]'))).toBeFalsy();
}));

it('should display last item if first is selected and loop and lazy loading is on', fakeAsync(() => {
component.loop = true;
component.ngOnChanges(changes);
fixture.detectChanges();
tick();

const itemList = viewerDe.query(By.css('ul'));

expect(itemList.query(By.css('img[src=src1]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src2]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src3]'))).toBeFalsy();
expect(itemList.query(By.css('img[src=src4]'))).toBeTruthy();
}));

it('should load 5 items if 3 are visible', fakeAsync(() => {
component.items.push({ src: 'src5' }, { src: 'src6' });
component.selectedIndex = 3;
component.ngOnChanges(changes);
fixture.detectChanges();
tick();
component['_viewerWidth'] = 600;
component['_itemWidth'] = 400;
fixture.detectChanges();

const itemList = viewerDe.query(By.css('ul'));

expect(itemList.query(By.css('img[src=src1]'))).toBeFalsy();
expect(itemList.query(By.css('img[src=src2]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src3]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src4]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src5]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src6]'))).toBeTruthy();
}));

describe('before touched', () => {
beforeEach(() => {
component.touched = false;
});

it('should load 3 items if 1.5 items are visible', fakeAsync(() => {
component.ngOnChanges(changes);
fixture.detectChanges();
tick();
component['_viewerWidth'] = 600;
component['_itemWidth'] = 400;
fixture.detectChanges();

const itemList = viewerDe.query(By.css('ul'));

expect(itemList.query(By.css('img[src=src1]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src2]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src3]'))).toBeFalsy();
}));

it('should load just 1 item if just 1 item is visible', fakeAsync(() => {
component.ngOnChanges(changes);
fixture.detectChanges();
tick();
component['_viewerWidth'] = 600;
component['_itemWidth'] = 600;
fixture.detectChanges();

const itemList = viewerDe.query(By.css('ul'));

expect(itemList.query(By.css('img[src=src1]'))).toBeTruthy();
expect(itemList.query(By.css('img[src=src2]'))).toBeFalsy();
expect(itemList.query(By.css('img[src=src3]'))).toBeFalsy();
}));
});
});

describe('error handling', () => {
let templateContainer: TestCustomTemplatesComponent;
let templateContainerFixture: ComponentFixture<TestCustomTemplatesComponent>;
Expand Down
Loading

0 comments on commit c26e75a

Please sign in to comment.