FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(image-loader): update sanity image urls on input changes by osnoser1 · Pull Request #75 · limitless-angular/limitless-angular · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .ts  (2) All 1 file type selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { Component, signal, viewChild } from '@angular/core';
import { render } from '@testing-library/angular';

import { SANITY_CONFIG } from '@limitless-angular/sanity/shared';
import { SanityImage } from './sanity-image.directive';

type DirectiveInputMetadata = {
inputs: Record<string, unknown>;
};

const sanityConfig = {
projectId: 'k4hg38xw',
dataset: 'demo',
};

@Component({
imports: [SanityImage],
template: `
<img
[sanityImage]="image()"
[width]="200"
[height]="300"
[quality]="75"
alt="Sanity image"
/>
`,
})
class SanityImageHost {
readonly directive = viewChild.required(SanityImage);
readonly image = signal('image-Tb9Ew8CXIwaY6R1kjMvI0uRR-2000x3000-jpg');
}

@Component({
imports: [SanityImage],
template: `
<img
[sanityImage]="image"
[loaderParams]="loaderParams"
width="200"
height="300"
alt="Sanity image"
/>
`,
})
class SanityImageLoaderParamsHost {
readonly directive = viewChild.required(SanityImage);
readonly image = 'image-Tb9Ew8CXIwaY6R1kjMvI0uRR-2000x3000-jpg';
readonly loaderParams = { fit: 'crop' };
}

@Component({
imports: [SanityImage],
template: `
<img
[sanityImage]="image"
[loaderParams]="loaderParams()"
[width]="width()"
[height]="height()"
[quality]="quality()"
alt="Sanity image"
/>
`,
})
class MutableSanityImageHost {
readonly directive = viewChild.required(SanityImage);
readonly image = 'image-Tb9Ew8CXIwaY6R1kjMvI0uRR-2000x3000-jpg';
readonly loaderParams = signal({ fit: 'crop' });
readonly width = signal(200);
readonly height = signal(300);
readonly quality = signal(75);
}

function directiveInputs() {
return (SanityImage as unknown as { ɵdir: DirectiveInputMetadata }).ɵdir
.inputs;
}

function imageSearchParams(fixture: {
componentInstance: MutableSanityImageHost;
}) {
return new URL(fixture.componentInstance.directive().ngSrc).searchParams;
}

function renderedImageSearchParams(fixture: { nativeElement: HTMLElement }) {
const image = fixture.nativeElement.querySelector('img');

expect(image).not.toBeNull();

return new URL((image as HTMLImageElement).src).searchParams;
}

describe('SanityImage', () => {
it('exposes ngSrc as a directive input', () => {
expect(directiveInputs()).toHaveProperty('ngSrc');
});

it('exposes loaderParams as a directive input', () => {
expect(directiveInputs()).toHaveProperty('loaderParams');
});

it('sets ngSrc from the bound Sanity image before initialization', async () => {
const { fixture } = await render(SanityImageHost, {
providers: [{ provide: SANITY_CONFIG, useValue: sanityConfig }],
});

await fixture.whenStable();

const directive = fixture.componentInstance.directive();

expect(directive.ngSrc).toBe(
'https://cdn.sanity.io/images/k4hg38xw/demo/Tb9Ew8CXIwaY6R1kjMvI0uRR-2000x3000.jpg?w=200&h=300&q=75',
);
});

it('updates ngSrc when the bound Sanity image changes', async () => {
const { fixture } = await render(SanityImageHost, {
providers: [{ provide: SANITY_CONFIG, useValue: sanityConfig }],
});

await fixture.whenStable();

fixture.componentInstance.image.set(
'image-G3i4emG6B8JnTmGoN0UjgAp8-3000x2000-jpg',
);
await fixture.whenStable();

const directive = fixture.componentInstance.directive();

expect(directive.ngSrc).toBe(
'https://cdn.sanity.io/images/k4hg38xw/demo/G3i4emG6B8JnTmGoN0UjgAp8-3000x2000.jpg?w=200&h=300&q=75',
);
});

it('uses bound loaderParams when generating ngSrc', async () => {
const { fixture } = await render(SanityImageLoaderParamsHost, {
providers: [{ provide: SANITY_CONFIG, useValue: sanityConfig }],
});

await fixture.whenStable();

const directive = fixture.componentInstance.directive();
const url = new URL(directive.ngSrc);

expect(directive.loaderParams).toEqual({ fit: 'crop' });
expect(url.searchParams.get('fit')).toBe('crop');
});

it('updates ngSrc when quality changes', async () => {
const { fixture } = await render(MutableSanityImageHost, {
providers: [{ provide: SANITY_CONFIG, useValue: sanityConfig }],
});

await fixture.whenStable();

fixture.componentInstance.quality.set(85);
await fixture.whenStable();

expect(imageSearchParams(fixture).get('q')).toBe('85');
expect(renderedImageSearchParams(fixture).get('q')).toBe('85');
});

it('updates ngSrc when loaderParams changes', async () => {
const { fixture } = await render(MutableSanityImageHost, {
providers: [{ provide: SANITY_CONFIG, useValue: sanityConfig }],
});

await fixture.whenStable();

fixture.componentInstance.loaderParams.set({ fit: 'clip' });
await fixture.whenStable();

expect(imageSearchParams(fixture).get('fit')).toBe('clip');
});

it('updates ngSrc when dimensions change', async () => {
const { fixture } = await render(MutableSanityImageHost, {
providers: [{ provide: SANITY_CONFIG, useValue: sanityConfig }],
});

await fixture.whenStable();

fixture.componentInstance.width.set(400);
fixture.componentInstance.height.set(500);
await fixture.whenStable();

const searchParams = imageSearchParams(fixture);
const renderedSearchParams = renderedImageSearchParams(fixture);

expect(searchParams.get('w')).toBe('400');
expect(searchParams.get('h')).toBe('500');
expect(renderedSearchParams.get('w')).toBe('400');
expect(renderedSearchParams.get('h')).toBe('500');
});
});
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
computed,
Directive,
inject,
Input,
input,
OnChanges,
OnInit,
Expand All @@ -29,6 +27,16 @@ import { sanityImageLoader } from './loader';

type LoaderParams = Omit<ImageUrlBuilderOptionsWithAliases, 'quality'>;

const imageUrlInputs = [
'sanityImage',
'loaderParams',
'quality',
'width',
'height',
];

const staticNgOptimizedImageInputs = ['loaderParams', 'width', 'height'];

function getNoopImageLoader() {
return (
IMAGE_LOADER.ɵprov as {
Expand All @@ -41,7 +49,7 @@ function getNoopImageLoader() {
// eslint-disable-next-line @angular-eslint/directive-selector
selector: 'img[sanityImage]',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['ngSrc'],
inputs: ['ngSrc', 'loaderParams'],
providers: [
{
provide: IMAGE_LOADER,
Expand All @@ -57,27 +65,15 @@ function getNoopImageLoader() {
],
})
export class SanityImage extends NgOptimizedImage implements OnInit, OnChanges {
private _loaderParams: LoaderParams = {};

sanityImage = input.required<SanityImageSource>();

@Input()
// @ts-expect-error we want to add some internal properties to loaderParams input
override set loaderParams(loaderParams: LoaderParams) {
this._loaderParams = loaderParams;
}

override get loaderParams(): LoaderParams {
return this._loaderParams;
}

quality = input<number>();

private imageUrl = computed(() => {
private imageUrl() {
const url = new URL(
createImageUrlBuilder(this.sanityConfig)
.image(this.sanityImage())
.withOptions(this._loaderParams)
.withOptions((this.loaderParams ?? {}) as LoaderParams)
.url(),
);
if (this.width) {
Expand All @@ -94,7 +90,7 @@ export class SanityImage extends NgOptimizedImage implements OnInit, OnChanges {
}

return url.toString();
});
}

private sanityConfig = inject<SanityConfig>(SANITY_CONFIG);

Expand All @@ -105,16 +101,28 @@ export class SanityImage extends NgOptimizedImage implements OnInit, OnChanges {

// ngSrc is not being updated by NgOptimizedImage, so we need to do it manually
override ngOnChanges(changes: SimpleChanges) {
if (changes['sanityImage']) {
const ngOptimizedImageChanges: SimpleChanges = { ...changes };

if (imageUrlInputs.some((inputName) => changes[inputName])) {
const previousNgSrc = this.ngSrc;
const ngSrc = this.imageUrl();
changes['ngSrc'] = new SimpleChange(
this.ngSrc,
ngOptimizedImageChanges['ngSrc'] = new SimpleChange(
previousNgSrc,
ngSrc,
changes['sanityImage'].isFirstChange(),
!previousNgSrc,
);
this.ngSrc = ngSrc;
}

super.ngOnChanges(changes);
for (const inputName of staticNgOptimizedImageInputs) {
if (
ngOptimizedImageChanges[inputName] &&
!ngOptimizedImageChanges[inputName].isFirstChange()
) {
delete ngOptimizedImageChanges[inputName];
}
}

super.ngOnChanges(ngOptimizedImageChanges);
}
}
Loading

Back | FazBrowse Home | New Git URL