diff --git a/aio/content/guide/image-directive.md b/aio/content/guide/image-directive.md
index c074279e1fe8d..e20864e5702a9 100644
--- a/aio/content/guide/image-directive.md
+++ b/aio/content/guide/image-directive.md
@@ -81,6 +81,18 @@ providers: [
+### Using `fill` mode
+
+In cases where you want to have an image fill a containing element, you can use the `fill` attribute. This is often useful when you want to achieve a "background image" behavior, or when you don't know the exact width and height of your image.
+
+When you add the `fill` attribute to your image, you do not need and should not include a `width` and `height`, as in this example:
+
+
+
+<img ngSrc="cat.jpg" fill>
+
+
+
### Adjusting image styling
Depending on the image's styling, adding `width` and `height` attributes may cause the image to render differently. `NgOptimizedImage` warns you if your image styling renders the image at a distorted aspect ratio.
diff --git a/goldens/public-api/common/index.md b/goldens/public-api/common/index.md
index ab18be6cb5f87..904604de8757d 100644
--- a/goldens/public-api/common/index.md
+++ b/goldens/public-api/common/index.md
@@ -549,6 +549,9 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
set disableOptimizedSrcset(value: string | boolean | undefined);
// (undocumented)
get disableOptimizedSrcset(): boolean;
+ set fill(value: string | boolean | undefined);
+ // (undocumented)
+ get fill(): boolean;
set height(value: string | number | undefined);
// (undocumented)
get height(): number | undefined;
@@ -571,7 +574,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
// (undocumented)
get width(): number | undefined;
// (undocumented)
- static ɵdir: i0.ɵɵDirectiveDeclaration;
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration;
}
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 9d24eea2ffb3c..d645a97e9fd7e 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
@@ -202,6 +202,12 @@ export const IMAGE_CONFIG = new InjectionToken(
@Directive({
standalone: true,
selector: 'img[ngSrc],img[rawSrc]',
+ host: {
+ '[style.position]': 'fill ? "absolute" : null',
+ '[style.width]': 'fill ? "100%" : null',
+ '[style.height]': 'fill ? "100%" : null',
+ '[style.inset]': 'fill ? "0px" : null'
+ }
})
export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
private imageLoader = inject(IMAGE_LOADER);
@@ -330,6 +336,19 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
}
private _disableOptimizedSrcset = false;
+ /**
+ * Sets the image to "fill mode," which eliminates the height/width requirement and adds
+ * styles such that the image fills its containing element.
+ */
+ @Input()
+ set fill(value: string|boolean|undefined) {
+ this._fill = inputToBoolean(value);
+ }
+ get fill(): boolean {
+ return this._fill;
+ }
+ private _fill = false;
+
/**
* Value of the `src` attribute if set on the host `` element.
* This input is exclusively read to assert that `src` is not set in conflict
@@ -356,7 +375,11 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
}
assertNotBase64Image(this);
assertNotBlobUrl(this);
- assertNonEmptyWidthAndHeight(this);
+ if (this.fill) {
+ assertEmptyWidthAndHeight(this);
+ } else {
+ assertNonEmptyWidthAndHeight(this);
+ }
assertValidLoadingInput(this);
assertNoImageDistortion(this, this.imgElement, this.renderer);
if (!this.ngSrcset) {
@@ -383,8 +406,14 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
private setHostAttributes() {
// Must set width/height explicitly in case they are bound (in which case they will
// only be reflected and not found by the browser)
- this.setHostAttribute('width', this.width!.toString());
- this.setHostAttribute('height', this.height!.toString());
+ if (this.fill) {
+ if (!this.sizes) {
+ this.sizes = '100vw';
+ }
+ } else {
+ this.setHostAttribute('width', this.width!.toString());
+ this.setHostAttribute('height', this.height!.toString());
+ }
this.setHostAttribute('loading', this.getLoadingBehavior());
this.setHostAttribute('fetchpriority', this.getFetchPriority());
@@ -805,6 +834,22 @@ function assertNonEmptyWidthAndHeight(dir: NgOptimizedImage) {
}
}
+/**
+ * Verifies that width and height are not set. Used in fill mode, where those attributes don't make
+ * sense.
+ */
+function assertEmptyWidthAndHeight(dir: NgOptimizedImage) {
+ if (dir.width || dir.height) {
+ throw new RuntimeError(
+ RuntimeErrorCode.INVALID_INPUT,
+ `${
+ imgDirectiveDetails(
+ dir.ngSrc)} the attributes \`height\` and/or \`width\` are present ` +
+ `along with the \`fill\` attribute. Because \`fill\` mode causes an image to fill its containing ` +
+ `element, the size attributes have no effect and should be removed.`);
+ }
+}
+
/**
* Verifies that the `loading` attribute is set to a valid input &
* is not used on priority images.
diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts
index 8483bf6d5a3d8..8870960cee715 100644
--- a/packages/common/test/directives/ng_optimized_image_spec.ts
+++ b/packages/common/test/directives/ng_optimized_image_spec.ts
@@ -794,6 +794,123 @@ describe('Image directive', () => {
});
});
+ describe('fill mode', () => {
+ it('should allow unsized images in fill mode', () => {
+ setupTestingModule();
+
+ const template = '';
+ expect(() => {
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ }).not.toThrow();
+ });
+ it('should throw if width is provided for fill mode image', () => {
+ setupTestingModule();
+
+ const template = '';
+ expect(() => {
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ })
+ .toThrowError(
+ 'NG02952: The NgOptimizedImage directive (activated on an element with the ' +
+ '`ngSrc="path/img.png"`) has detected that the attributes `height` and/or `width` ' +
+ 'are present along with the `fill` attribute. Because `fill` mode causes an image ' +
+ 'to fill its containing element, the size attributes have no effect and should be removed.');
+ });
+ it('should throw if height is provided for fill mode image', () => {
+ setupTestingModule();
+
+ const template = '';
+ expect(() => {
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ })
+ .toThrowError(
+ 'NG02952: The NgOptimizedImage directive (activated on an element with the ' +
+ '`ngSrc="path/img.png"`) has detected that the attributes `height` and/or `width` ' +
+ 'are present along with the `fill` attribute. Because `fill` mode causes an image ' +
+ 'to fill its containing element, the size attributes have no effect and should be removed.');
+ });
+ it('should apply appropriate styles in fill mode', () => {
+ setupTestingModule();
+
+ const template = '';
+
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ const nativeElement = fixture.nativeElement as HTMLElement;
+ const img = nativeElement.querySelector('img')!;
+ expect(img.getAttribute('style')?.replace(/\s/g, ''))
+ .toBe('position:absolute;width:100%;height:100%;inset:0px;');
+ });
+ it('should augment existing styles in fill mode', () => {
+ setupTestingModule();
+
+ const template = '';
+
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ const nativeElement = fixture.nativeElement as HTMLElement;
+ const img = nativeElement.querySelector('img')!;
+ expect(img.getAttribute('style')?.replace(/\s/g, ''))
+ .toBe(
+ 'border-radius:5px;padding:10px;position:absolute;width:100%;height:100%;inset:0px;');
+ });
+ it('should not add fill styles if not in fill mode', () => {
+ setupTestingModule();
+
+ const template =
+ '';
+
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ const nativeElement = fixture.nativeElement as HTMLElement;
+ const img = nativeElement.querySelector('img')!;
+ expect(img.getAttribute('style')?.replace(/\s/g, ''))
+ .toBe('position:relative;border-radius:5px;');
+ });
+ it('should add default sizes value in fill mode', () => {
+ setupTestingModule();
+
+ const template = '';
+
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ const nativeElement = fixture.nativeElement as HTMLElement;
+ const img = nativeElement.querySelector('img')!;
+ expect(img.getAttribute('sizes')).toBe('100vw');
+ });
+ it('should not overwrite sizes value in fill mode', () => {
+ setupTestingModule();
+
+ const template = '';
+
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ const nativeElement = fixture.nativeElement as HTMLElement;
+ const img = nativeElement.querySelector('img')!;
+ expect(img.getAttribute('sizes')).toBe('50vw');
+ });
+ it('should cause responsive srcset to be generated in fill mode', () => {
+ setupTestingModule();
+
+ const template = '';
+
+ const fixture = createTestComponent(template);
+ fixture.detectChanges();
+ const nativeElement = fixture.nativeElement as HTMLElement;
+ const img = nativeElement.querySelector('img')!;
+ expect(img.getAttribute('srcset'))
+ .toBe(
+ `${IMG_BASE_URL}/path/img.png 640w, ${IMG_BASE_URL}/path/img.png 750w, ${
+ IMG_BASE_URL}/path/img.png 828w, ` +
+ `${IMG_BASE_URL}/path/img.png 1080w, ${IMG_BASE_URL}/path/img.png 1200w, ${
+ IMG_BASE_URL}/path/img.png 1920w, ` +
+ `${IMG_BASE_URL}/path/img.png 2048w, ${IMG_BASE_URL}/path/img.png 3840w`);
+ });
+ });
+
describe('preconnect detector', () => {
const imageLoader = () => {
// We need something different from the `localhost` (as we don't want to produce