diff --git a/packages/common/src/directives/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image.ts index bf3791bc94d28..bdb28bc7d58d9 100644 --- a/packages/common/src/directives/ng_optimized_image.ts +++ b/packages/common/src/directives/ng_optimized_image.ts @@ -15,8 +15,9 @@ import {RuntimeErrorCode} from '../errors'; * Config options recognized by the image loader function. */ export interface ImageLoaderConfig { + // Name of the image to be added to the image request URL src: string; - quality?: number; + // Width of the requested image (to be used when generating srcset) width?: number; } @@ -246,13 +247,10 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy { } private getRewrittenSrc(): string { - const imgConfig = { - src: this.rawSrc, - // TODO: if we're going to support responsive serving, we don't want to request the width - // based solely on the intrinsic width (e.g. if it's on mobile and the viewport is smaller). - // The width would require pre-processing before passing to the image loader function. - width: this.width, - }; + // ImageLoaderConfig supports setting a width property. However, we're not setting width here + // because if the developer uses rendered width instead of intrinsic width in the HTML width + // attribute, the image requested may be too small for 2x+ screens. + const imgConfig = {src: this.rawSrc}; return this.imageLoader(imgConfig); } diff --git a/packages/common/src/private_export.ts b/packages/common/src/private_export.ts index b80f7c2da07e2..bc56e4f07816e 100644 --- a/packages/common/src/private_export.ts +++ b/packages/common/src/private_export.ts @@ -6,6 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -export {IMAGE_LOADER as ɵIMAGE_LOADER, NgOptimizedImage as ɵNgOptimizedImage} from './directives/ng_optimized_image'; +export {IMAGE_LOADER as ɵIMAGE_LOADER, ImageLoaderConfig as ɵImageLoaderConfig, NgOptimizedImage as ɵNgOptimizedImage} from './directives/ng_optimized_image'; export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter'; export {BrowserPlatformLocation as ɵBrowserPlatformLocation} from './location/platform_location'; diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts index ab0fdad484015..142e52d08165f 100644 --- a/packages/common/test/directives/ng_optimized_image_spec.ts +++ b/packages/common/test/directives/ng_optimized_image_spec.ts @@ -13,18 +13,6 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {expect} from '@angular/platform-browser/testing/src/matchers'; describe('Image directive', () => { - it('should set `src` to `rawSrc` value if image loader is not provided', () => { - setupTestingModule(); - - const template = ''; - const fixture = createTestComponent(template); - fixture.detectChanges(); - - const nativeElement = fixture.nativeElement as HTMLElement; - const img = nativeElement.querySelector('img')!; - expect(img.src.endsWith('/path/img.png')).toBeTrue(); - }); - it('should set `loading` and `fetchpriority` attributes before `src`', () => { // Only run this test in a browser since the Node-based DOM mocks don't // allow to override `HTMLImageElement.prototype.setAttribute` easily. @@ -75,20 +63,6 @@ describe('Image directive', () => { expect(_fetchpriorityAttrId).toBeLessThan(_srcAttrId); // was set after `src` }); - - it('should use an image loader provided via `IMAGE_LOADER` token', () => { - const imageLoader = (config: ImageLoaderConfig) => `${config.src}?w=${config.width}`; - setupTestingModule({imageLoader}); - - const template = ''; - const fixture = createTestComponent(template); - fixture.detectChanges(); - - const nativeElement = fixture.nativeElement as HTMLElement; - const img = nativeElement.querySelector('img')!; - expect(img.src.endsWith('/path/img.png?w=150')).toBeTrue(); - }); - describe('setup error handling', () => { it('should throw if both `src` and `rawSrc` are present', () => { setupTestingModule(); @@ -315,6 +289,54 @@ describe('Image directive', () => { expect(img.getAttribute('fetchpriority')).toBe('auto'); }); }); + + describe('loaders', () => { + it('should set `src` to match `rawSrc` if image loader is not provided', () => { + setupTestingModule(); + + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + const nativeElement = fixture.nativeElement as HTMLElement; + const img = nativeElement.querySelector('img')!; + expect(img.src.trim()).toBe('/path/img.png'); + }); + + it('should set `src` using the image loader provided via the `IMAGE_LOADER` token to compose src URL', + () => { + const imageLoader = (config: ImageLoaderConfig) => `path/${config.src}`; + setupTestingModule({imageLoader}); + + const template = ` + + + `; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + const nativeElement = fixture.nativeElement as HTMLElement; + const imgs = nativeElement.querySelectorAll('img')!; + expect(imgs[0].src.trim()).toBe('/path/img.png'); + expect(imgs[1].src.trim()).toBe('/path/img-2.png'); + }); + + it('should set`src` to an image URL that does not include a default width parameter', () => { + const imageLoader = (config: ImageLoaderConfig) => { + const widthStr = config.width ? `?w=${config.width}` : ``; + return `path/${config.src}${widthStr}`; + }; + setupTestingModule({imageLoader}); + + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + const nativeElement = fixture.nativeElement as HTMLElement; + const img = nativeElement.querySelector('img')!; + expect(img.src.trim()).toBe('/path/img.png'); + }); + }); }); // Helpers diff --git a/packages/core/test/bundling/image-directive/BUILD.bazel b/packages/core/test/bundling/image-directive/BUILD.bazel index 53555998fc6b5..4062482879586 100644 --- a/packages/core/test/bundling/image-directive/BUILD.bazel +++ b/packages/core/test/bundling/image-directive/BUILD.bazel @@ -54,8 +54,6 @@ ts_devserver( static_files = [ "index.html", ":tslib", - "a.png", - "b.png", ], deps = [":image-directive"], ) diff --git a/packages/core/test/bundling/image-directive/a.png b/packages/core/test/bundling/image-directive/a.png deleted file mode 100644 index c510293918228..0000000000000 Binary files a/packages/core/test/bundling/image-directive/a.png and /dev/null differ diff --git a/packages/core/test/bundling/image-directive/b.png b/packages/core/test/bundling/image-directive/b.png deleted file mode 100644 index c510293918228..0000000000000 Binary files a/packages/core/test/bundling/image-directive/b.png and /dev/null differ diff --git a/packages/core/test/bundling/image-directive/basic/basic.e2e-spec.ts b/packages/core/test/bundling/image-directive/basic/basic.e2e-spec.ts index f64ea3c9753c6..727f7059ac67d 100644 --- a/packages/core/test/bundling/image-directive/basic/basic.e2e-spec.ts +++ b/packages/core/test/bundling/image-directive/basic/basic.e2e-spec.ts @@ -17,6 +17,6 @@ describe('NgOptimizedImage directive', () => { await browser.get('/'); const imgs = element.all(by.css('img')); const src = await imgs.get(0).getAttribute('src'); - expect(src.endsWith('b.png')).toBe(true); + expect(/a\.png/.test(src)).toBe(true); }); }); diff --git a/packages/core/test/bundling/image-directive/basic/basic.ts b/packages/core/test/bundling/image-directive/basic/basic.ts index 5f0eb2bd9ef8a..57296592a2075 100644 --- a/packages/core/test/bundling/image-directive/basic/basic.ts +++ b/packages/core/test/bundling/image-directive/basic/basic.ts @@ -6,15 +6,51 @@ * found in the LICENSE file at https://angular.io/license */ -import {ɵIMAGE_LOADER as IMAGE_LOADER, ɵNgOptimizedImage as NgOptimizedImage} from '@angular/common'; +import {ɵIMAGE_LOADER as IMAGE_LOADER, ɵImageLoaderConfig as ImageLoaderConfig, ɵNgOptimizedImage as NgOptimizedImage} from '@angular/common'; import {Component} from '@angular/core'; +const CUSTOM_IMGIX_LOADER = (config: ImageLoaderConfig) => { + const widthStr = config.width ? `?w=${config.width}` : ``; + return `https://aurora-project.imgix.net/${config.src}${widthStr}`; +}; + + @Component({ selector: 'basic', + styles: [` + h1 { + display: flex; + align-items: center; + } + + main { + border: 1px solid blue; + margin: 16px; + padding: 16px; + } + + .spacer { + height: 3000px; + } + + main img { + width: 100%; + height: auto; + } + `], + template: ` +

+ + Angular image app +

+
+
+ +
+ `, standalone: true, imports: [NgOptimizedImage], - template: ``, - providers: [{provide: IMAGE_LOADER, useValue: () => 'b.png'}], + providers: [{provide: IMAGE_LOADER, useValue: CUSTOM_IMGIX_LOADER}], }) export class BasicComponent { } diff --git a/packages/core/test/bundling/image-directive/index.ts b/packages/core/test/bundling/image-directive/index.ts index e7453ca475717..680d653a6a464 100644 --- a/packages/core/test/bundling/image-directive/index.ts +++ b/packages/core/test/bundling/image-directive/index.ts @@ -18,6 +18,7 @@ import {LcpCheckComponent} from './lcp-check/lcp-check'; standalone: true, imports: [RouterModule], template: '', + }) export class RootComponent { }