diff --git a/goldens/public-api/common/errors.md b/goldens/public-api/common/errors.md index 92b257c65a9..f34aa7d1f34 100644 --- a/goldens/public-api/common/errors.md +++ b/goldens/public-api/common/errors.md @@ -19,6 +19,8 @@ export const enum RuntimeErrorCode { // (undocumented) NG_FOR_MISSING_DIFFER = -2200, // (undocumented) + NGSRCSET_WITHOUT_LOADER = 2963, + // (undocumented) OVERSIZED_IMAGE = 2960, // (undocumented) PARENT_NG_SWITCH_NOT_FOUND = 2000, diff --git a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts index aa760ec9f8e..78fd7ab23bb 100644 --- a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts +++ b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts @@ -385,6 +385,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy { assertNoComplexSizes(this); } assertNotMissingBuiltInLoader(this.ngSrc, this.imageLoader); + assertNoNgSrcsetWithoutLoader(this, this.imageLoader); if (this.priority) { const checker = this.injector.get(PreconnectLinkChecker); checker.assertPreconnect(this.getRewrittenSrc(), this.ngSrc); @@ -953,3 +954,17 @@ function assertNotMissingBuiltInLoader(ngSrc: string, imageLoader: ImageLoader) } } } + +/** + * Warns if ngSrcset is present and no loader is configured (i.e. the default one is being used). + */ +function assertNoNgSrcsetWithoutLoader(dir: NgOptimizedImage, imageLoader: ImageLoader) { + if (dir.ngSrcset && imageLoader === noopImageLoader) { + console.warn(formatRuntimeError( + RuntimeErrorCode.NGSRCSET_WITHOUT_LOADER, + `${imgDirectiveDetails(dir.ngSrc)} the \`ngSrcset\` attribute is present but ` + + `no image loader is configured (i.e. the default one is being used), ` + + `which would result in the same image being used for all configured sizes. ` + + `To fix this, provide a loader or remove the \`ngSrcset\` attribute from the image.`)); + } +} diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts index d088c0bc160..a886c284f2d 100644 --- a/packages/common/src/errors.ts +++ b/packages/common/src/errors.ts @@ -33,4 +33,5 @@ export const enum RuntimeErrorCode { OVERSIZED_IMAGE = 2960, TOO_MANY_PRELOADED_IMAGES = 2961, MISSING_BUILTIN_LOADER = 2962, + NGSRCSET_WITHOUT_LOADER = 2963, } diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts index cd066fa4fae..5b21e14d6fa 100644 --- a/packages/common/test/directives/ng_optimized_image_spec.ts +++ b/packages/common/test/directives/ng_optimized_image_spec.ts @@ -1183,6 +1183,24 @@ describe('Image directive', () => { expect(consoleWarnSpy.calls.count()).toBe(0); }); + it('should warn if there is no image loader but `ngSrcset` is present', () => { + setUpModuleNoLoader(); + + const template = ``; + const fixture = createTestComponent(template); + const consoleWarnSpy = spyOn(console, 'warn'); + fixture.detectChanges(); + + expect(consoleWarnSpy.calls.count()).toBe(1); + expect(consoleWarnSpy.calls.argsFor(0)[0]) + .toBe( + 'NG02963: The NgOptimizedImage directive (activated on an element ' + + 'with the `ngSrc="img.png"`) has detected that the `ngSrcset` attribute is ' + + 'present but no image loader is configured (i.e. the default one is being used), ' + + `which would result in the same image being used for all configured sizes. ` + + 'To fix this, provide a loader or remove the `ngSrcset` attribute from the image.'); + }); + it('should set `src` using the image loader provided via the `IMAGE_LOADER` token to compose src URL', () => { const imageLoader = (config: ImageLoaderConfig) => `${IMG_BASE_URL}/${config.src}`; diff --git a/packages/core/test/bundling/image-directive/e2e/oversized-image/oversized-image.ts b/packages/core/test/bundling/image-directive/e2e/oversized-image/oversized-image.ts index 920758bc58c..fa03e4416a5 100644 --- a/packages/core/test/bundling/image-directive/e2e/oversized-image/oversized-image.ts +++ b/packages/core/test/bundling/image-directive/e2e/oversized-image/oversized-image.ts @@ -6,13 +6,19 @@ * found in the LICENSE file at https://angular.io/license */ -import {NgOptimizedImage} from '@angular/common'; +import {IMAGE_LOADER, ImageLoaderConfig, NgOptimizedImage} from '@angular/common'; import {Component} from '@angular/core'; +const imageLoader = { + provide: IMAGE_LOADER, + useFactory: () => (config: ImageLoaderConfig) => config.src +}; + @Component({ selector: 'oversized-image-passing', standalone: true, imports: [NgOptimizedImage], + providers: [imageLoader], template: `
@@ -33,6 +39,7 @@ export class OversizedImageComponentPassing { selector: 'oversized-image-failing', standalone: true, imports: [NgOptimizedImage], + providers: [imageLoader], template: `