Skip to content

Commit 54b24eb

Browse files
atcastleatscott
authored andcommitted
feat(common): Add loaderParams attribute to NgOptimizedImage (#48907)
Add a new loaderParams attribute, which can be used to send arbitrary data to a custom loader, allowing for greater control of image CDN features. PR Close #48907
1 parent 759db12 commit 54b24eb

File tree

7 files changed

+212
-24
lines changed

7 files changed

+212
-24
lines changed

aio/content/guide/image-directive.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,41 @@ providers: [
276276
],
277277
</code-example>
278278

279-
A loader function for the `NgOptimizedImage` directive takes an object with the `ImageLoaderConfig` type (from `@angular/common`) as its argument and returns the absolute URL of the image asset. The `ImageLoaderConfig` object contains the `src` and `width` properties.
279+
A loader function for the `NgOptimizedImage` directive takes an object with the `ImageLoaderConfig` type (from `@angular/common`) as its argument and returns the absolute URL of the image asset. The `ImageLoaderConfig` object contains the `src` property, and optional `width` and `loaderParams` properties.
280280

281-
Note: a custom loader must support requesting images at various widths in order for `ngSrcset` to work properly.
281+
Note: even though the `width` property may not always be present, a custom loader must use it to support requesting images at various widths in order for `ngSrcset` to work properly.
282+
283+
### The `loaderParams` Property
284+
285+
There is an additional attribute supported by the `NgOptimizedImage` directive, called `loaderParams`, which is specifically designed to support the use of custom loaders. The `loaderParams` attribute take an object with any properties as a value, and does not do anything on its own. The data in `loaderParams` is added to the `ImageLoaderConfig` object passed to your custom loader, and can be used to control the behavior of the loader.
286+
287+
A common use for `loaderParams` is controlling advanced image CDN features.
288+
289+
### Example custom loader
290+
291+
The following shows an example of a custom loader function. This example function concatenates `src` and `width`, and uses `loaderParams` to control a custom CDN feature for rounded corners:
292+
293+
<code-example format="typescript" language="typescript">
294+
const myCustomLoader = (config: ImageLoaderConfig) => {
295+
let url = `https://example.com/images/${config.src}?`;
296+
let queryParams = [];
297+
if (config.width) {
298+
queryParams.push(`w=${config.width}`);
299+
}
300+
if (config.loaderParams?.roundedCorners) {
301+
queryParams.push('mask=corners&corner-radius=5');
302+
}
303+
return url + queryParams.join('&');
304+
};
305+
</code-example>
306+
307+
Note that in the above example, we've invented the 'roundedCorners' property name to control a feature of our custom loader. We could then use this feature when creating an image, as follows:
308+
309+
<code-example format="html" language="html">
310+
311+
&lt;img ngSrc="profile.jpg" width="300" height="300" [loaderParams]="{roundedCorners: true}"&gt;
312+
313+
</code-example>
282314

283315
<!-- links -->
284316

goldens/public-api/common/errors.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ export const enum RuntimeErrorCode {
1717
// (undocumented)
1818
MISSING_BUILTIN_LOADER = 2962,
1919
// (undocumented)
20-
NG_FOR_MISSING_DIFFER = -2200,
20+
MISSING_NECESSARY_LOADER = 2963,
2121
// (undocumented)
22-
NGSRCSET_WITHOUT_LOADER = 2963,
22+
NG_FOR_MISSING_DIFFER = -2200,
2323
// (undocumented)
2424
OVERSIZED_IMAGE = 2960,
2525
// (undocumented)

goldens/public-api/common/index.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ export type ImageLoader = (config: ImageLoaderConfig) => string;
327327

328328
// @public
329329
export interface ImageLoaderConfig {
330+
loaderParams?: {
331+
[key: string]: any;
332+
};
330333
src: string;
331334
width?: number;
332335
}
@@ -605,6 +608,9 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
605608
set height(value: string | number | undefined);
606609
// (undocumented)
607610
get height(): number | undefined;
611+
loaderParams?: {
612+
[key: string]: any;
613+
};
608614
loading?: 'lazy' | 'eager' | 'auto';
609615
// (undocumented)
610616
ngOnChanges(changes: SimpleChanges): void;
@@ -622,7 +628,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
622628
// (undocumented)
623629
get width(): number | undefined;
624630
// (undocumented)
625-
static ɵdir: i0.ɵɵDirectiveDeclaration<NgOptimizedImage, "img[ngSrc]", never, { "ngSrc": "ngSrc"; "ngSrcset": "ngSrcset"; "sizes": "sizes"; "width": "width"; "height": "height"; "loading": "loading"; "priority": "priority"; "disableOptimizedSrcset": "disableOptimizedSrcset"; "fill": "fill"; "src": "src"; "srcset": "srcset"; }, {}, never, never, true, never>;
631+
static ɵdir: i0.ɵɵDirectiveDeclaration<NgOptimizedImage, "img[ngSrc]", never, { "ngSrc": "ngSrc"; "ngSrcset": "ngSrcset"; "sizes": "sizes"; "width": "width"; "height": "height"; "loading": "loading"; "priority": "priority"; "loaderParams": "loaderParams"; "disableOptimizedSrcset": "disableOptimizedSrcset"; "fill": "fill"; "src": "src"; "srcset": "srcset"; }, {}, never, never, true, never>;
626632
// (undocumented)
627633
static ɵfac: i0.ɵɵFactoryDeclaration<NgOptimizedImage, never>;
628634
}

packages/common/src/directives/ng_optimized_image/image_loaders/image_loader.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export interface ImageLoaderConfig {
2727
* Width of the requested image (to be used when generating srcset).
2828
*/
2929
width?: number;
30+
/**
31+
* Additional user-provided parameters for use by the ImageLoader.
32+
*/
33+
loaderParams?: {[key: string]: any;};
3034
}
3135

3236
/**

packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {isPlatformServer} from '../../platform_id';
1313

1414
import {imgDirectiveDetails} from './error_helper';
1515
import {cloudinaryLoaderInfo} from './image_loaders/cloudinary_loader';
16-
import {IMAGE_LOADER, ImageLoader, noopImageLoader} from './image_loaders/image_loader';
16+
import {IMAGE_LOADER, ImageLoader, ImageLoaderConfig, noopImageLoader} from './image_loaders/image_loader';
1717
import {imageKitLoaderInfo} from './image_loaders/imagekit_loader';
1818
import {imgixLoaderInfo} from './image_loaders/imgix_loader';
1919
import {LCPImageObserver} from './lcp_image_observer';
@@ -317,6 +317,11 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
317317
}
318318
private _priority = false;
319319

320+
/**
321+
* Data to pass through to custom loaders.
322+
*/
323+
@Input() loaderParams?: {[key: string]: any};
324+
320325
/**
321326
* Disables automatic srcset generation for this image.
322327
*/
@@ -386,6 +391,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
386391
}
387392
assertNotMissingBuiltInLoader(this.ngSrc, this.imageLoader);
388393
assertNoNgSrcsetWithoutLoader(this, this.imageLoader);
394+
assertNoLoaderParamsWithoutLoader(this, this.imageLoader);
389395
if (this.priority) {
390396
const checker = this.injector.get(PreconnectLinkChecker);
391397
checker.assertPreconnect(this.getRewrittenSrc(), this.ngSrc);
@@ -462,11 +468,21 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
462468
'fill',
463469
'loading',
464470
'sizes',
471+
'loaderParams',
465472
'disableOptimizedSrcset',
466473
]);
467474
}
468475
}
469476

477+
private callImageLoader(configWithoutCustomParams: Omit<ImageLoaderConfig, 'loaderParams'>):
478+
string {
479+
let augmentedConfig: ImageLoaderConfig = configWithoutCustomParams;
480+
if (this.loaderParams) {
481+
augmentedConfig.loaderParams = this.loaderParams;
482+
}
483+
return this.imageLoader(augmentedConfig);
484+
}
485+
470486
private getLoadingBehavior(): string {
471487
if (!this.priority && this.loading !== undefined) {
472488
return this.loading;
@@ -485,7 +501,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
485501
if (!this._renderedSrc) {
486502
const imgConfig = {src: this.ngSrc};
487503
// Cache calculated image src to reuse it later in the code.
488-
this._renderedSrc = this.imageLoader(imgConfig);
504+
this._renderedSrc = this.callImageLoader(imgConfig);
489505
}
490506
return this._renderedSrc;
491507
}
@@ -495,7 +511,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
495511
const finalSrcs = this.ngSrcset.split(',').filter(src => src !== '').map(srcStr => {
496512
srcStr = srcStr.trim();
497513
const width = widthSrcSet ? parseFloat(srcStr) : parseFloat(srcStr) * this.width!;
498-
return `${this.imageLoader({src: this.ngSrc, width})} ${srcStr}`;
514+
return `${this.callImageLoader({src: this.ngSrc, width})} ${srcStr}`;
499515
});
500516
return finalSrcs.join(', ');
501517
}
@@ -518,15 +534,16 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
518534
filteredBreakpoints = breakpoints!.filter(bp => bp >= VIEWPORT_BREAKPOINT_CUTOFF);
519535
}
520536

521-
const finalSrcs =
522-
filteredBreakpoints.map(bp => `${this.imageLoader({src: this.ngSrc, width: bp})} ${bp}w`);
537+
const finalSrcs = filteredBreakpoints.map(
538+
bp => `${this.callImageLoader({src: this.ngSrc, width: bp})} ${bp}w`);
523539
return finalSrcs.join(', ');
524540
}
525541

526542
private getFixedSrcset(): string {
527-
const finalSrcs = DENSITY_SRCSET_MULTIPLIERS.map(
528-
multiplier => `${this.imageLoader({src: this.ngSrc, width: this.width! * multiplier})} ${
529-
multiplier}x`);
543+
const finalSrcs = DENSITY_SRCSET_MULTIPLIERS.map(multiplier => `${this.callImageLoader({
544+
src: this.ngSrc,
545+
width: this.width! * multiplier
546+
})} ${multiplier}x`);
530547
return finalSrcs.join(', ');
531548
}
532549

@@ -961,10 +978,25 @@ function assertNotMissingBuiltInLoader(ngSrc: string, imageLoader: ImageLoader)
961978
function assertNoNgSrcsetWithoutLoader(dir: NgOptimizedImage, imageLoader: ImageLoader) {
962979
if (dir.ngSrcset && imageLoader === noopImageLoader) {
963980
console.warn(formatRuntimeError(
964-
RuntimeErrorCode.NGSRCSET_WITHOUT_LOADER,
981+
RuntimeErrorCode.MISSING_NECESSARY_LOADER,
965982
`${imgDirectiveDetails(dir.ngSrc)} the \`ngSrcset\` attribute is present but ` +
966983
`no image loader is configured (i.e. the default one is being used), ` +
967984
`which would result in the same image being used for all configured sizes. ` +
968985
`To fix this, provide a loader or remove the \`ngSrcset\` attribute from the image.`));
969986
}
970987
}
988+
989+
/**
990+
* Warns if loaderParams is present and no loader is configured (i.e. the default one is being
991+
* used).
992+
*/
993+
function assertNoLoaderParamsWithoutLoader(dir: NgOptimizedImage, imageLoader: ImageLoader) {
994+
if (dir.loaderParams && imageLoader === noopImageLoader) {
995+
console.warn(formatRuntimeError(
996+
RuntimeErrorCode.MISSING_NECESSARY_LOADER,
997+
`${imgDirectiveDetails(dir.ngSrc)} the \`loaderParams\` attribute is present but ` +
998+
`no image loader is configured (i.e. the default one is being used), ` +
999+
`which means that the loaderParams data will not be consumed and will not affect the URL. ` +
1000+
`To fix this, provide a custom loader or remove the \`loaderParams\` attribute from the image.`));
1001+
}
1002+
}

packages/common/src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ export const enum RuntimeErrorCode {
3333
OVERSIZED_IMAGE = 2960,
3434
TOO_MANY_PRELOADED_IMAGES = 2961,
3535
MISSING_BUILTIN_LOADER = 2962,
36-
NGSRCSET_WITHOUT_LOADER = 2963,
36+
MISSING_NECESSARY_LOADER = 2963,
3737
}

0 commit comments

Comments
 (0)