@@ -41,6 +41,18 @@ const noopImageLoader = (config: ImageLoaderConfig) => config.src;
4141 */
4242const BASE64_IMG_MAX_LENGTH_IN_ERROR = 50 ;
4343
44+ /**
45+ * RegExpr to determine whether a src in a srcset is using width descriptors.
46+ * Should match something like: "100w, 200w".
47+ */
48+ const VALID_WIDTH_DESCRIPTOR_SRCSET = / ^ ( ( \s * \d + w \s * ( , | $ ) ) { 1 , } ) $ / ;
49+
50+ /**
51+ * RegExpr to determine whether a src in a srcset is using density descriptors.
52+ * Should match something like: "1x, 2x".
53+ */
54+ const VALID_DENSITY_DESCRIPTOR_SRCSET = / ^ ( ( \s * \d ( \. \d ) ? x \s * ( , | $ ) ) { 1 , } ) $ / ;
55+
4456/**
4557 * Special token that allows to configure a function that will be used to produce an image URL based
4658 * on the specified input.
@@ -105,7 +117,7 @@ class LCPImageObserver implements OnDestroy {
105117 const imgRawSrc = this . images . get ( imgSrc ) ;
106118 if ( imgRawSrc && ! this . alreadyWarned . has ( imgSrc ) ) {
107119 this . alreadyWarned . add ( imgSrc ) ;
108- const directiveDetails = imgDirectiveDetails ( { rawSrc : imgRawSrc } as any ) ;
120+ const directiveDetails = imgDirectiveDetails ( imgRawSrc ) ;
109121 console . warn ( formatRuntimeError (
110122 RuntimeErrorCode . LCP_IMG_MISSING_PRIORITY ,
111123 `${ directiveDetails } : the image was detected as the Largest Contentful Paint (LCP) ` +
@@ -163,6 +175,17 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
163175 */
164176 @Input ( ) rawSrc ! : string ;
165177
178+ /**
179+ * A comma separated list of width or density descriptors.
180+ * The image name will be taken from `rawSrc` and combined with the list of width or density
181+ * descriptors to generate the final `srcset` property of the image.
182+ *
183+ * Example:
184+ * <img rawSrc="hello.jpg" rawSrcset="100w, 200w" /> =>
185+ * <img src="path/hello.jpg" srcset="path/hello.jpg?w=100 100w, path/hello.jpg?w=200 200w" />
186+ */
187+ @Input ( ) rawSrcset ! : string ;
188+
166189 /**
167190 * The intrinsic width of the image in px.
168191 */
@@ -201,7 +224,8 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
201224 /**
202225 * Get a value of the `src` and `srcset` if they're set on a host <img> element.
203226 * These inputs are needed to verify that there are no conflicting sources provided
204- * at the same time (thus causing an ambiguity on which src to use) and that images
227+ * at the same time (e.g. `src` and `rawSrc` together or `srcset` and `rawSrcset`,
228+ * thus causing an ambiguity on which src to use) and that images
205229 * don't start to load until a lazy loading strategy is set.
206230 * @internal
207231 */
@@ -210,7 +234,8 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
210234
211235 ngOnInit ( ) {
212236 if ( ngDevMode ) {
213- assertValidRawSrc ( this . rawSrc ) ;
237+ assertNonEmptyInput ( 'rawSrc' , this . rawSrc ) ;
238+ assertValidRawSrcset ( this . rawSrcset ) ;
214239 assertNoConflictingSrc ( this ) ;
215240 assertNoConflictingSrcset ( this ) ;
216241 assertNotBase64Image ( this ) ;
@@ -229,14 +254,18 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
229254 }
230255 this . setHostAttribute ( 'loading' , this . getLoadingBehavior ( ) ) ;
231256 this . setHostAttribute ( 'fetchpriority' , this . getFetchPriority ( ) ) ;
232- // The `src` attribute should be set last since other attributes
257+ // The `src` and `srcset` attributes should be set last since other attributes
233258 // could affect the image's loading behavior.
234259 this . setHostAttribute ( 'src' , this . getRewrittenSrc ( ) ) ;
260+ if ( this . rawSrcset ) {
261+ this . setHostAttribute ( 'srcset' , this . getRewrittenSrcset ( ) ) ;
262+ }
235263 }
236264
237265 ngOnChanges ( changes : SimpleChanges ) {
238266 if ( ngDevMode ) {
239- assertNoPostInitInputChange ( this , changes , [ 'rawSrc' , 'width' , 'height' , 'priority' ] ) ;
267+ assertNoPostInitInputChange (
268+ this , changes , [ 'rawSrc' , 'rawSrcset' , 'width' , 'height' , 'priority' ] ) ;
240269 }
241270 }
242271
@@ -256,6 +285,16 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
256285 return this . imageLoader ( imgConfig ) ;
257286 }
258287
288+ private getRewrittenSrcset ( ) : string {
289+ const widthSrcSet = VALID_WIDTH_DESCRIPTOR_SRCSET . test ( this . rawSrcset ) ;
290+ const finalSrcs = this . rawSrcset . split ( ',' ) . filter ( src => src !== '' ) . map ( srcStr => {
291+ srcStr = srcStr . trim ( ) ;
292+ const width = widthSrcSet ? parseFloat ( srcStr ) : parseFloat ( srcStr ) * this . width ! ;
293+ return `${ this . imageLoader ( { src : this . rawSrc , width} ) } ${ srcStr } ` ;
294+ } ) ;
295+ return finalSrcs . join ( ', ' ) ;
296+ }
297+
259298 ngOnDestroy ( ) {
260299 if ( ngDevMode && ! this . priority ) {
261300 // An image is only registered in dev mode, try to unregister only in dev mode as well.
@@ -329,7 +368,9 @@ function assertNoConflictingSrc(dir: NgOptimizedImage) {
329368 if ( dir . src ) {
330369 throw new RuntimeError (
331370 RuntimeErrorCode . UNEXPECTED_SRC_ATTR ,
332- `${ imgDirectiveDetails ( dir . rawSrc ) } has detected that the \`src\` is also set (to ` +
371+ `${
372+ imgDirectiveDetails (
373+ dir . rawSrc ) } has detected that the \`src\` has already been set (to ` +
333374 `\`${ dir . src } \`). Please remove the \`src\` attribute from this image. ` +
334375 `The NgOptimizedImage directive will use the \`rawSrc\` to compute ` +
335376 `the final image URL and set the \`src\` itself.` ) ;
@@ -379,19 +420,35 @@ function assertNotBlobURL(dir: NgOptimizedImage) {
379420 }
380421}
381422
382- // Verifies that the `rawSrc` is set to a non-empty string.
383- function assertValidRawSrc ( value : unknown ) {
423+ // Verifies that the input is set to a non-empty string.
424+ function assertNonEmptyInput ( name : string , value : unknown ) {
384425 const isString = typeof value === 'string' ;
385426 const isEmptyString = isString && value . trim ( ) === '' ;
386427 if ( ! isString || isEmptyString ) {
387428 const extraMessage = isEmptyString ? ' (empty string)' : '' ;
388429 throw new RuntimeError (
389430 RuntimeErrorCode . INVALID_INPUT ,
390- `The NgOptimizedImage directive has detected that the \`rawSrc \` has an invalid value: ` +
431+ `The NgOptimizedImage directive has detected that the \`${ name } \` has an invalid value: ` +
391432 `expecting a non-empty string, but got: \`${ value } \`${ extraMessage } .` ) ;
392433 }
393434}
394435
436+ // Verifies that the `rawSrcset` is in a valid format, e.g. "100w, 200w" or "1x, 2x"
437+ export function assertValidRawSrcset ( value : unknown ) {
438+ if ( value == null ) return ;
439+ assertNonEmptyInput ( 'rawSrcset' , value ) ;
440+ const isValidSrcset = VALID_WIDTH_DESCRIPTOR_SRCSET . test ( value as string ) ||
441+ VALID_DENSITY_DESCRIPTOR_SRCSET . test ( value as string ) ;
442+
443+ if ( ! isValidSrcset ) {
444+ throw new RuntimeError (
445+ RuntimeErrorCode . INVALID_INPUT ,
446+ `The NgOptimizedImage directive has detected that the \`rawSrcset\` has an invalid value: ` +
447+ `expecting width descriptors (e.g. "100w, 200w") or density descriptors (e.g. "1x, 2x"), ` +
448+ `but got: \`${ value } \`` ) ;
449+ }
450+ }
451+
395452// Creates a `RuntimeError` instance to represent a situation when an input is set after
396453// the directive has initialized.
397454function postInitInputChangeError ( dir : NgOptimizedImage , inputName : string ) : { } {
0 commit comments