Skip to content

Commit

Permalink
feat(common): add loading attr to NgOptimizedImage (#47082)
Browse files Browse the repository at this point in the history
Add loading attribute to NgOptimizedImage.

PR Close #47082
  • Loading branch information
khempenius authored and Pawel Kozlowski committed Aug 16, 2022
1 parent 57f3386 commit e854a8c
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 0 deletions.
39 changes: 39 additions & 0 deletions packages/common/src/directives/ng_optimized_image.ts
Expand Up @@ -210,6 +210,14 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
return this._height;
}

/**
* The desired loading behavior (lazy, eager, or auto).
* The primary use case for this input is opting-out non-priority images
* from lazy loading by marking them loading='eager' or loading='auto'.
* This input should not be used with priority images.
*/
@Input() loading?: string;

/**
* Indicates whether this image should have a high priority.
*/
Expand Down Expand Up @@ -242,6 +250,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
assertNotBlobURL(this);
assertRequiredNumberInput(this, this.width, 'width');
assertRequiredNumberInput(this, this.height, 'height');
assertValidLoadingInput(this);
if (!this.priority) {
// Monitor whether an image is an LCP element only in case
// the `priority` attribute is missing. Otherwise, an image
Expand Down Expand Up @@ -270,6 +279,9 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
}

private getLoadingBehavior(): string {
if (!this.priority && this.loading !== undefined && isNonEmptyString(this.loading)) {
return this.loading;
}
return this.priority ? 'eager' : 'lazy';
}

Expand Down Expand Up @@ -337,6 +349,12 @@ function inputToBoolean(value: unknown): boolean {
return value != null && `${value}` !== 'false';
}

function isNonEmptyString(value: unknown): boolean {
const isString = typeof value === 'string';
const isEmptyString = isString && value.trim() === '';
return isString && !isEmptyString;
}

/**
* Invokes a function, passing an instance of the `LCPImageObserver` as an argument.
*
Expand Down Expand Up @@ -500,3 +518,24 @@ function assertRequiredNumberInput(dir: NgOptimizedImage, inputValue: unknown, i
`on the mentioned element.`);
}
}

// Verifies that the `loading` attribute is set to a valid input &
// is not used on priority images.
function assertValidLoadingInput(dir: NgOptimizedImage) {
if (dir.loading && dir.priority) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_INPUT,
`The NgOptimizedImage directive has detected that the \`loading\` attribute ` +
`was used on an image that was marked "priority". Images marked "priority" ` +
`are always eagerly loaded and this behavior cannot be overwritten by using ` +
`the "loading" attribute.`);
}
const validInputs = ['auto', 'eager', 'lazy'];
if (typeof dir.loading === 'string' && !validInputs.includes(dir.loading)) {
throw new RuntimeError(
RuntimeErrorCode.INVALID_INPUT,
`The NgOptimizedImage directive has detected that the \`loading\` attribute ` +
`has an invalid value: expecting "lazy", "eager", or "auto" but got: ` +
`\`${dir.loading}\`.`);
}
}
56 changes: 56 additions & 0 deletions packages/common/test/directives/ng_optimized_image_spec.ts
Expand Up @@ -412,6 +412,62 @@ describe('Image directive', () => {
});
});

describe('loading attribute', () => {
it('should override the default loading behavior for non-priority images', () => {
setupTestingModule();

const template = '<img rawSrc="path/img.png" width="150" height="50" loading="eager">';
const fixture = createTestComponent(template);
fixture.detectChanges();

const nativeElement = fixture.nativeElement as HTMLElement;
const img = nativeElement.querySelector('img')!;
expect(img.getAttribute('loading')).toBe('eager');
});

it('should throw if used with priority images', () => {
setupTestingModule();

const template =
'<img rawSrc="path/img.png" width="150" height="50" loading="eager" priority>';
expect(() => {
const fixture = createTestComponent(template);
fixture.detectChanges();
})
.toThrowError(
'NG02951: The NgOptimizedImage directive has detected that the `loading` ' +
'attribute was used on an image that was marked "priority". ' +
'Images marked "priority" are always eagerly loaded and this behavior ' +
'cannot be overwritten by using the "loading" attribute.');
});

it('should support setting loading priority to "auto"', () => {
setupTestingModule();

const template = '<img rawSrc="path/img.png" width="150" height="50" loading="auto">';
const fixture = createTestComponent(template);
fixture.detectChanges();

const nativeElement = fixture.nativeElement as HTMLElement;
const img = nativeElement.querySelector('img')!;
expect(img.getAttribute('loading')).toBe('auto');
});

it('should throw for invalid loading inputs', () => {
setupTestingModule();

const template = '<img rawSrc="path/img.png" width="150" height="150" loading="fast">';
expect(() => {
const fixture = createTestComponent(template);
fixture.detectChanges();
})
.toThrowError(
'NG02951: The NgOptimizedImage directive has detected that the `loading` ' +
'attribute has an invalid value: expecting "lazy", "eager", or "auto"' +
' but got: `fast`.');
});
});

describe('fetch priority', () => {
it('should be "high" for priority images', () => {
setupTestingModule();
Expand Down

0 comments on commit e854a8c

Please sign in to comment.