Skip to content

Commit

Permalink
feat(common): Allow ngSrc to be changed post-init
Browse files Browse the repository at this point in the history
Remove thrown error when ngSrc is modified after an NgOptimizedImage image is initialized
  • Loading branch information
atcastle committed Jun 12, 2023
1 parent a163d35 commit 58a0ddb
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 15 deletions.
Expand Up @@ -247,8 +247,21 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
* Image name will be processed by the image loader and the final URL will be applied as the `src`
* property of the image.
*/
@Input() ngSrc!: string;

@Input()
set ngSrc(value: string) {
this._ngSrc = value;
if (this._ngSrcInitialized) {
const rewrittenSrc = this.callImageLoader({src: value});
this.setHostAttribute('src', rewrittenSrc);
this.updateSrcset();
}
this._ngSrcInitialized = true;
}
get ngSrc(): string {
return this._ngSrc;
}
private _ngSrc = '';
private _ngSrcInitialized = false;
/**
* A comma separated list of width or density descriptors.
* The image name will be taken from `ngSrc` and combined with the list of width or density
Expand Down Expand Up @@ -440,15 +453,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
this.setHostAttribute('sizes', this.sizes);
}

if (this.ngSrcset) {
rewrittenSrcset = this.getRewrittenSrcset();
} else if (this.shouldGenerateAutomaticSrcset()) {
rewrittenSrcset = this.getAutomaticSrcset();
}

if (rewrittenSrcset) {
this.setHostAttribute('srcset', rewrittenSrcset);
}
this.updateSrcset();

if (this.isServer && this.priority) {
this.preloadLinkCreator.createPreloadLinkTag(
Expand All @@ -460,7 +465,6 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges) {
if (ngDevMode) {
assertNoPostInitInputChange(this, changes, [
'ngSrc',
'ngSrcset',
'width',
'height',
Expand Down Expand Up @@ -539,6 +543,19 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
return finalSrcs.join(', ');
}

private updateSrcset(): void {
let rewrittenSrcset;
if (this.ngSrcset) {
rewrittenSrcset = this.getRewrittenSrcset();
} else if (this.shouldGenerateAutomaticSrcset()) {
rewrittenSrcset = this.getAutomaticSrcset();
}

if (rewrittenSrcset) {
this.setHostAttribute('srcset', rewrittenSrcset);
}
}

private getFixedSrcset(): string {
const finalSrcs = DENSITY_SRCSET_MULTIPLIERS.map(multiplier => `${this.callImageLoader({
src: this.ngSrc,
Expand Down
103 changes: 100 additions & 3 deletions packages/common/test/directives/ng_optimized_image_spec.ts
Expand Up @@ -642,9 +642,8 @@ describe('Image directive', () => {
});

const inputs = [
['ngSrc', 'new-img.png'], ['width', 10], ['height', 20], ['priority', true], ['fill', true],
['loading', true], ['sizes', '90vw'], ['disableOptimizedSrcset', true],
['loaderParams', '{foo: "test1"}']
['width', 10], ['height', 20], ['priority', true], ['fill', true], ['loading', true],
['sizes', '90vw'], ['disableOptimizedSrcset', true], ['loaderParams', '{foo: "test1"}']
];
inputs.forEach(([inputName, value]) => {
it(`should throw if the \`${inputName}\` input changed after directive initialized the input`,
Expand Down Expand Up @@ -692,6 +691,35 @@ describe('Image directive', () => {
}).toThrowError(new RegExp(expectedErrorMessage));
});
});
it(`should not throw if ngSrc changed after directive is initialized`, () => {
@Component({
selector: 'test-cmp',
template: `<img
[ngSrc]="ngSrc"
[width]="width"
[height]="height"
[loading]="loading"
[sizes]="sizes"
>`
})
class TestComponent {
width = 100;
height = 50;
ngSrc = 'img.png';
loading = false;
sizes = '100vw';
}

setupTestingModule({component: TestComponent});

// Initial render
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
expect(() => {
fixture.componentInstance.ngSrc = 'newImg.png';
fixture.detectChanges();
}).not.toThrowError(new RegExp('was updated after initialization'));
});
});

describe('lazy loading', () => {
Expand Down Expand Up @@ -1259,6 +1287,75 @@ describe('Image directive', () => {
expect(imgs[1].src.trim()).toBe(`${IMG_BASE_URL}/img-2.png`);
});

it('should use the image loader to update `src` if `ngSrc` updated', () => {
@Component({
selector: 'test-cmp',
template: `<img
[ngSrc]="ngSrc"
width="300"
height="300"
>`
})
class TestComponent {
ngSrc = `img.png`;
}
const imageLoader = (config: ImageLoaderConfig) => `${IMG_BASE_URL}/${config.src}`;
setupTestingModule({imageLoader, component: TestComponent});
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

let nativeElement = fixture.nativeElement as HTMLElement;
let imgs = nativeElement.querySelectorAll('img')!;
expect(imgs[0].src).toBe(`${IMG_BASE_URL}/img.png`);

fixture.componentInstance.ngSrc = 'updatedImg.png';
fixture.detectChanges();
expect(imgs[0].src).toBe(`${IMG_BASE_URL}/updatedImg.png`);
});

it('should use the image loader to update `srcset` if `ngSrc` updated', () => {
@Component({
selector: 'test-cmp',
template: `<img
[ngSrc]="ngSrc"
width="300"
height="300"
sizes="100vw"
>`
})
class TestComponent {
ngSrc = `img.png`;
}
const imageLoader = (config: ImageLoaderConfig) => {
const width = config.width ? `?w=${config.width}` : ``;
return `${IMG_BASE_URL}/${config.src}${width}`;
};
setupTestingModule({imageLoader, component: TestComponent});
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

let nativeElement = fixture.nativeElement as HTMLElement;
let imgs = nativeElement.querySelectorAll('img')!;
expect(imgs[0].getAttribute('srcset'))
.toBe(`${IMG_BASE_URL}/img.png?w=640 640w, ${IMG_BASE_URL}/img.png?w=750 750w, ${
IMG_BASE_URL}/img.png?w=828 828w, ${IMG_BASE_URL}/img.png?w=1080 1080w, ${
IMG_BASE_URL}/img.png?w=1200 1200w, ${IMG_BASE_URL}/img.png?w=1920 1920w, ${
IMG_BASE_URL}/img.png?w=2048 2048w, ${IMG_BASE_URL}/img.png?w=3840 3840w`);

fixture.componentInstance.ngSrc = 'updatedImg.png';
nativeElement = fixture.nativeElement as HTMLElement;
imgs = nativeElement.querySelectorAll('img')!;
fixture.detectChanges();
expect(imgs[0].getAttribute('srcset'))
.toBe(`${IMG_BASE_URL}/updatedImg.png?w=640 640w, ${
IMG_BASE_URL}/updatedImg.png?w=750 750w, ${IMG_BASE_URL}/updatedImg.png?w=828 828w, ${
IMG_BASE_URL}/updatedImg.png?w=1080 1080w, ${
IMG_BASE_URL}/updatedImg.png?w=1200 1200w, ${
IMG_BASE_URL}/updatedImg.png?w=1920 1920w, ${
IMG_BASE_URL}/updatedImg.png?w=2048 2048w, ${
IMG_BASE_URL}/updatedImg.png?w=3840 3840w`);
});

it('should pass absolute URLs defined in the `ngSrc` to custom image loaders provided via the `IMAGE_LOADER` token',
() => {
const imageLoader = (config: ImageLoaderConfig) => `${config.src}?rewritten=true`;
Expand Down

0 comments on commit 58a0ddb

Please sign in to comment.