mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
refactor(common): allow string urls in NgOptimizedImage input (#54901)
With this commit it is now possible to pass an url as placeholder for the image directive. PR Close #54901
This commit is contained in:
committed by
Jessica Janiuk
parent
341a116d61
commit
f407e3ce6f
@@ -31,6 +31,8 @@ export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
PARENT_NG_SWITCH_NOT_FOUND = 2000,
|
||||
// (undocumented)
|
||||
PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED = 2967,
|
||||
// (undocumented)
|
||||
PRIORITY_IMG_MISSING_PRECONNECT_TAG = 2956,
|
||||
// (undocumented)
|
||||
REQUIRED_INPUT_MISSING = 2954,
|
||||
|
||||
@@ -117,6 +117,12 @@ const FIXED_SRCSET_HEIGHT_LIMIT = 1080;
|
||||
*/
|
||||
export const PLACEHOLDER_BLUR_AMOUNT = 15;
|
||||
|
||||
/**
|
||||
* Placeholder dimension (height or width) limit in pixels. Angular produces a warning
|
||||
* when this limit is crossed.
|
||||
*/
|
||||
const PLACEHOLDER_DIMENSION_LIMIT = 1000;
|
||||
|
||||
/**
|
||||
* Used to warn or error when the user provides an overly large dataURL for the placeholder
|
||||
* attribute.
|
||||
@@ -364,7 +370,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
|
||||
/**
|
||||
* A URL or data URL for an image to be used as a placeholder while this image loads.
|
||||
*/
|
||||
@Input({transform: booleanOrDataUrlAttribute}) placeholder?: string | boolean;
|
||||
@Input({transform: booleanOrUrlAttribute}) placeholder?: string | boolean;
|
||||
|
||||
/**
|
||||
* Configuration object for placeholder settings. Options:
|
||||
@@ -518,6 +524,10 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ngDevMode && changes['placeholder']?.currentValue && !this.isServer) {
|
||||
assertPlaceholderDimensions(this, this.imgElement);
|
||||
}
|
||||
}
|
||||
|
||||
private callImageLoader(
|
||||
@@ -651,7 +661,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
|
||||
width: placeholderResolution,
|
||||
isPlaceholder: true,
|
||||
})})`;
|
||||
} else if (typeof placeholderInput === 'string' && placeholderInput.startsWith('data:')) {
|
||||
} else if (typeof placeholderInput === 'string') {
|
||||
return `url(${placeholderInput})`;
|
||||
}
|
||||
return null;
|
||||
@@ -1286,6 +1296,28 @@ function assetPriorityCountBelowThreshold() {
|
||||
IMGS_WITH_PRIORITY_ATTR_COUNT++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Warns if placeholder's dimension are over a threshold.
|
||||
*
|
||||
* This assert function is meant to only run on the browser.
|
||||
*/
|
||||
function assertPlaceholderDimensions(dir: NgOptimizedImage, imgElement: HTMLImageElement) {
|
||||
const computedStyle = window.getComputedStyle(imgElement);
|
||||
let renderedWidth = parseFloat(computedStyle.getPropertyValue('width'));
|
||||
let renderedHeight = parseFloat(computedStyle.getPropertyValue('height'));
|
||||
|
||||
if (renderedWidth > PLACEHOLDER_DIMENSION_LIMIT || renderedHeight > PLACEHOLDER_DIMENSION_LIMIT) {
|
||||
console.warn(
|
||||
formatRuntimeError(
|
||||
RuntimeErrorCode.PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED,
|
||||
`${imgDirectiveDetails(dir.ngSrc)} it uses a placeholder image, but at least one of ` +
|
||||
`of the dimensions attribute (height or width) exceeds the limit of ${PLACEHOLDER_DIMENSION_LIMIT}px. ` +
|
||||
`To fix this, use a smaller image as a placeholder.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function round(input: number): number | string {
|
||||
return Number.isInteger(input) ? input : input.toFixed(2);
|
||||
}
|
||||
@@ -1301,8 +1333,8 @@ function unwrapSafeUrl(value: string | SafeValue): string {
|
||||
|
||||
// Transform function to handle inputs which may be booleans, strings, or string representations
|
||||
// of boolean values. Used for the placeholder attribute.
|
||||
export function booleanOrDataUrlAttribute(value: boolean | string): boolean | string {
|
||||
if (typeof value === 'string' && value.startsWith(`data:`)) {
|
||||
export function booleanOrUrlAttribute(value: boolean | string): boolean | string {
|
||||
if (typeof value === 'string' && value !== 'true' && value !== 'false' && value !== '') {
|
||||
return value;
|
||||
}
|
||||
return booleanAttribute(value);
|
||||
|
||||
@@ -40,4 +40,5 @@ export const enum RuntimeErrorCode {
|
||||
LCP_IMG_NGSRC_MODIFIED = 2964,
|
||||
OVERSIZED_PLACEHOLDER = 2965,
|
||||
TOO_MANY_PRIORITY_ATTRIBUTES = 2966,
|
||||
PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED = 2967,
|
||||
}
|
||||
|
||||
@@ -1129,6 +1129,38 @@ describe('Image directive', () => {
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
it('should add a background-image tag when placeholder is provided as a URL', () => {
|
||||
setupTestingModule({imageLoader});
|
||||
const template =
|
||||
'<img ngSrc="path/img.png" width="400" height="300" placeholder="https://mysite.com/assets/my-image.png" />';
|
||||
|
||||
const fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
const nativeElement = fixture.nativeElement as HTMLElement;
|
||||
const img = nativeElement.querySelector('img')!;
|
||||
// Double quotes removed to account for different browser behavior.
|
||||
expect(img.getAttribute('style')?.replace(/"/g, '').replace(/\s/g, '')).toBe(
|
||||
`background-size:cover;background-position:50%50%;background-repeat:no-repeat;background-image:url(https://mysite.com/assets/my-image.png);filter:blur(${PLACEHOLDER_BLUR_AMOUNT}px);`,
|
||||
);
|
||||
});
|
||||
|
||||
// DataURLs get stripped from background-image attribute in Node, but not browsers.
|
||||
it('should add a background-image tag when placeholder is provided as relative URL', () => {
|
||||
setupTestingModule({imageLoader});
|
||||
const template =
|
||||
'<img ngSrc="path/img.png" width="400" height="300" placeholder="../../assets/my-image.png" />';
|
||||
|
||||
const fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
const nativeElement = fixture.nativeElement as HTMLElement;
|
||||
const img = nativeElement.querySelector('img')!;
|
||||
// Double quotes removed to account for different browser behavior.
|
||||
expect(img.getAttribute('style')?.replace(/"/g, '').replace(/\s/g, '')).toBe(
|
||||
`background-size:cover;background-position:50%50%;background-repeat:no-repeat;background-image:url(../../assets/my-image.png);filter:blur(${PLACEHOLDER_BLUR_AMOUNT}px);`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a background-image tag when placeholder is provided without value', () => {
|
||||
setupTestingModule({imageLoader});
|
||||
const template = '<img ngSrc="path/img.png" width="400" height="300" placeholder />';
|
||||
@@ -1276,6 +1308,40 @@ describe('Image directive', () => {
|
||||
`performance, generate a smaller data URL placeholder.`,
|
||||
);
|
||||
});
|
||||
|
||||
if (isBrowser) {
|
||||
it('should throw if the placeholder height exceeds the threshold', () => {
|
||||
setUpModuleNoLoader();
|
||||
|
||||
const template = `<img ngSrc="path/img.png" width="100" height="100" style="width:1001px; height: 300px" placeholder="data:image/png;base64,${'a'.repeat(
|
||||
100,
|
||||
)}">`;
|
||||
|
||||
const consoleWarnSpy = spyOn(console, 'warn');
|
||||
const fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
expect(consoleWarnSpy.calls.count()).toBe(1);
|
||||
expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch(
|
||||
new RegExp(`NG0${RuntimeErrorCode.PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED}:`),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the placeholder width exceeds the threshold', () => {
|
||||
setUpModuleNoLoader();
|
||||
|
||||
const template = `<img ngSrc="path/img.png" width="100" height="100" style="height:1001px; width: 300px" placeholder="data:image/png;base64,${'a'.repeat(
|
||||
100,
|
||||
)}">`;
|
||||
|
||||
const consoleWarnSpy = spyOn(console, 'warn');
|
||||
const fixture = createTestComponent(template);
|
||||
fixture.detectChanges();
|
||||
expect(consoleWarnSpy.calls.count()).toBe(1);
|
||||
expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch(
|
||||
new RegExp(`NG0${RuntimeErrorCode.PLACEHOLDER_DIMENSION_LIMIT_EXCEEDED}:`),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('preconnect detector', () => {
|
||||
|
||||
Reference in New Issue
Block a user