diff --git a/.circleci/config.yml b/.circleci/config.yml
index 11f6eeb4ed6..d746c7c4453 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -71,7 +71,6 @@ var_11: &only_release_branches
branches:
only:
- main
- - image-directive
- /\d+\.\d+\.x/
# CircleCI orbs
diff --git a/goldens/public-api/common/index.md b/goldens/public-api/common/index.md
index 9691d75125f..fef58389a02 100644
--- a/goldens/public-api/common/index.md
+++ b/goldens/public-api/common/index.md
@@ -266,9 +266,7 @@ export type ImageLoader = (config: ImageLoaderConfig) => string;
// @public
export interface ImageLoaderConfig {
- // (undocumented)
src: string;
- // (undocumented)
width?: number;
}
@@ -535,7 +533,6 @@ export abstract class NgLocalization {
// @public
export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
- constructor(imageLoader: ImageLoader, renderer: Renderer2, imgElement: ElementRef, injector: Injector);
set height(value: string | number | undefined);
// (undocumented)
get height(): number | undefined;
@@ -551,8 +548,6 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
get priority(): boolean;
rawSrc: string;
rawSrcset: string;
- // (undocumented)
- srcset?: string;
set width(value: string | number | undefined);
// (undocumented)
get width(): number | undefined;
diff --git a/packages/common/src/directives/ng_optimized_image/asserts.ts b/packages/common/src/directives/ng_optimized_image/asserts.ts
index dfef38b052a..b254e1ecb90 100644
--- a/packages/common/src/directives/ng_optimized_image/asserts.ts
+++ b/packages/common/src/directives/ng_optimized_image/asserts.ts
@@ -11,15 +11,15 @@ import {ɵRuntimeError as RuntimeError} from '@angular/core';
import {RuntimeErrorCode} from '../../errors';
/**
- * Asserts whether an ngDevMode is enabled and throws an error if it's not the case.
- * This assert can be used to make sure that there is no dev-mode code invoked in
+ * Asserts that the application is in development mode. Throws an error if the application is in
+ * production mode. This assert can be used to make sure that there is no dev-mode code invoked in
* the prod mode accidentally.
*/
-export function assertDevMode(check: string) {
+export function assertDevMode(checkName: string) {
if (!ngDevMode) {
throw new RuntimeError(
RuntimeErrorCode.UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE,
- `Unexpected invocation of the ${check} in the prod mode. ` +
+ `Unexpected invocation of the ${checkName} in the prod mode. ` +
`Please make sure that the prod mode is enabled for production builds.`);
}
}
diff --git a/packages/common/src/directives/ng_optimized_image/image_loaders/cloudflare_loader.ts b/packages/common/src/directives/ng_optimized_image/image_loaders/cloudflare_loader.ts
index b55861efa6f..dc806fae2bc 100644
--- a/packages/common/src/directives/ng_optimized_image/image_loaders/cloudflare_loader.ts
+++ b/packages/common/src/directives/ng_optimized_image/image_loaders/cloudflare_loader.ts
@@ -9,23 +9,26 @@
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
- * Function that generates a built-in ImageLoader for Cloudflare Image Resizing
+ * Function that generates an ImageLoader for Cloudflare Image Resizing
* and turns it into an Angular provider. Note: Cloudflare has multiple image
* products - this provider is specifically for Cloudflare Image Resizing;
* it will not work with Cloudflare Images or Cloudflare Polish.
*
- * @param path Your domain name
- * e.g. https://mysite.com
+ * @param path Your domain name, e.g. https://mysite.com
+ * @param options An object with extra configuration:
+ * - `ensurePreconnect`: boolean flag indicating whether the NgOptimizedImage directive
+ * should verify that there is a corresponding ``
+ * present in the document's `
`.
* @returns Provider that provides an ImageLoader function
*
* @publicApi
* @developerPreview
*/
export const provideCloudflareLoader = createImageLoader(
- createCloudflareURL,
+ createCloudflareUrl,
ngDevMode ? ['https:///cdn-cgi/image//'] : undefined);
-function createCloudflareURL(path: string, config: ImageLoaderConfig) {
+function createCloudflareUrl(path: string, config: ImageLoaderConfig) {
let params = `format=auto`;
if (config.width) {
params += `,width=${config.width}`;
diff --git a/packages/common/src/directives/ng_optimized_image/image_loaders/cloudinary_loader.ts b/packages/common/src/directives/ng_optimized_image/image_loaders/cloudinary_loader.ts
index 097980fb857..093d4647e5a 100644
--- a/packages/common/src/directives/ng_optimized_image/image_loaders/cloudinary_loader.ts
+++ b/packages/common/src/directives/ng_optimized_image/image_loaders/cloudinary_loader.ts
@@ -9,15 +9,14 @@
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
- * Function that generates a built-in ImageLoader for Cloudinary
- * and turns it into an Angular provider.
+ * Function that generates an ImageLoader for Cloudinary and turns it into an Angular provider.
*
* @param path Base URL of your Cloudinary images
* This URL should match one of the following formats:
* https://res.cloudinary.com/mysite
* https://mysite.cloudinary.com
* https://subdomain.mysite.com
- * @param options An object that allows to provide extra configuration:
+ * @param options An object with extra configuration:
* - `ensurePreconnect`: boolean flag indicating whether the NgOptimizedImage directive
* should verify that there is a corresponding ``
* present in the document's ``.
@@ -27,7 +26,7 @@ import {createImageLoader, ImageLoaderConfig} from './image_loader';
* @developerPreview
*/
export const provideCloudinaryLoader = createImageLoader(
- createCloudinaryURL,
+ createCloudinaryUrl,
ngDevMode ?
[
'https://res.cloudinary.com/mysite', 'https://mysite.cloudinary.com',
@@ -35,7 +34,7 @@ export const provideCloudinaryLoader = createImageLoader(
] :
undefined);
-function createCloudinaryURL(path: string, config: ImageLoaderConfig) {
+function createCloudinaryUrl(path: string, config: ImageLoaderConfig) {
// Example of a Cloudinary image URL:
// https://res.cloudinary.com/mysite/image/upload/c_scale,f_auto,q_auto,w_600/marketing/tile-topics-m.png
let params = `f_auto,q_auto`; // sets image format and quality to "auto"
diff --git a/packages/common/src/directives/ng_optimized_image/image_loaders/image_loader.ts b/packages/common/src/directives/ng_optimized_image/image_loaders/image_loader.ts
index 06d184bdbca..b44d882aab0 100644
--- a/packages/common/src/directives/ng_optimized_image/image_loaders/image_loader.ts
+++ b/packages/common/src/directives/ng_optimized_image/image_loaders/image_loader.ts
@@ -19,9 +19,13 @@ import {isAbsoluteUrl, isValidPath, normalizePath, normalizeSrc} from '../url';
* @developerPreview
*/
export interface ImageLoaderConfig {
- // Name of the image to be added to the image request URL
+ /**
+ * Image file name to be added to the image request URL.
+ */
src: string;
- // Width of the requested image (to be used when generating srcset)
+ /**
+ * Width of the requested image (to be used when generating srcset).
+ */
width?: number;
}
@@ -40,8 +44,8 @@ export type ImageLoader = (config: ImageLoaderConfig) => string;
const noopImageLoader = (config: ImageLoaderConfig) => config.src;
/**
- * Special token that allows to configure a function that will be used to produce an image URL based
- * on the specified input.
+ * Injection token that configures the function that produces image URLs based on the specified
+ * input.
*
* @publicApi
* @developerPreview
@@ -62,6 +66,11 @@ export function createImageLoader(
const loaderFn = (config: ImageLoaderConfig) => {
if (ngDevMode && isAbsoluteUrl(config.src)) {
+ // Image loader functions expect an image file name (e.g. `my-image.png`)
+ // or a relative path + a file name (e.g. `/a/b/c/my-image.png`) as an input,
+ // so the final absolute URL can be constructed.
+ // When an absolute URL is provided instead - the loader can not
+ // build a final URL, thus the error is thrown to indicate that.
throwUnexpectedAbsoluteUrlError(path, config.src);
}
diff --git a/packages/common/src/directives/ng_optimized_image/image_loaders/imagekit_loader.ts b/packages/common/src/directives/ng_optimized_image/image_loaders/imagekit_loader.ts
index cb3e3073b8b..8e14f744b43 100644
--- a/packages/common/src/directives/ng_optimized_image/image_loaders/imagekit_loader.ts
+++ b/packages/common/src/directives/ng_optimized_image/image_loaders/imagekit_loader.ts
@@ -9,14 +9,13 @@
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
- * Function that generates a built-in ImageLoader for ImageKit
- * and turns it into an Angular provider.
+ * Function that generates an ImageLoader for ImageKit and turns it into an Angular provider.
*
* @param path Base URL of your ImageKit images
* This URL should match one of the following formats:
* https://ik.imagekit.io/myaccount
* https://subdomain.mysite.com
- * @param options An object that allows to provide extra configuration:
+ * @param options An object with extra configuration:
* - `ensurePreconnect`: boolean flag indicating whether the NgOptimizedImage directive
* should verify that there is a corresponding ``
* present in the document's ``.
@@ -26,10 +25,10 @@ import {createImageLoader, ImageLoaderConfig} from './image_loader';
* @developerPreview
*/
export const provideImageKitLoader = createImageLoader(
- createImagekitURL,
+ createImagekitUrl,
ngDevMode ? ['https://ik.imagekit.io/mysite', 'https://subdomain.mysite.com'] : undefined);
-export function createImagekitURL(path: string, config: ImageLoaderConfig) {
+export function createImagekitUrl(path: string, config: ImageLoaderConfig) {
// Example of an ImageKit image URL:
// https://ik.imagekit.io/demo/tr:w-300,h-300/medium_cafe_B1iTdD0C.jpg
let params = `tr:q-auto`; // applies the "auto quality" transformation
diff --git a/packages/common/src/directives/ng_optimized_image/image_loaders/imgix_loader.ts b/packages/common/src/directives/ng_optimized_image/image_loaders/imgix_loader.ts
index def659e08f6..8a2d534f1ab 100644
--- a/packages/common/src/directives/ng_optimized_image/image_loaders/imgix_loader.ts
+++ b/packages/common/src/directives/ng_optimized_image/image_loaders/imgix_loader.ts
@@ -9,12 +9,11 @@
import {createImageLoader, ImageLoaderConfig} from './image_loader';
/**
- * Function that generates a built-in ImageLoader for Imgix and turns it
- * into an Angular provider.
+ * Function that generates an ImageLoader for Imgix and turns it into an Angular provider.
*
* @param path path to the desired Imgix origin,
* e.g. https://somepath.imgix.net or https://images.mysite.com
- * @param options An object that allows to provide extra configuration:
+ * @param options An object with extra configuration:
* - `ensurePreconnect`: boolean flag indicating whether the NgOptimizedImage directive
* should verify that there is a corresponding ``
* present in the document's ``.
@@ -24,9 +23,9 @@ import {createImageLoader, ImageLoaderConfig} from './image_loader';
* @developerPreview
*/
export const provideImgixLoader =
- createImageLoader(createImgixURL, ngDevMode ? ['https://somepath.imgix.net/'] : undefined);
+ createImageLoader(createImgixUrl, ngDevMode ? ['https://somepath.imgix.net/'] : undefined);
-function createImgixURL(path: string, config: ImageLoaderConfig) {
+function createImgixUrl(path: string, config: ImageLoaderConfig) {
const url = new URL(`${path}/${config.src}`);
// This setting ensures the smallest allowable format is set.
url.searchParams.set('auto', 'format');
diff --git a/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts b/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts
index d801c5f09c7..4cc5266713e 100644
--- a/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts
+++ b/packages/common/src/directives/ng_optimized_image/lcp_image_observer.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Inject, Injectable, OnDestroy, ɵformatRuntimeError as formatRuntimeError} from '@angular/core';
+import {inject, Injectable, OnDestroy, ɵformatRuntimeError as formatRuntimeError} from '@angular/core';
import {DOCUMENT} from '../../dom_tokens';
import {RuntimeErrorCode} from '../../errors';
@@ -16,11 +16,11 @@ import {imgDirectiveDetails} from './error_helper';
import {getUrl} from './url';
/**
- * Contains the logic to detect whether an image with the `NgOptimizedImage` directive
- * is treated as an LCP element. If so, verifies that the image is marked as a priority,
- * using the `priority` attribute.
+ * Observer that detects whether an image with `NgOptimizedImage`
+ * is treated as a Largest Contentful Paint (LCP) element. If so,
+ * asserts that the image has the `priority` attribute.
*
- * Note: this is a dev-mode only class, which should not appear in prod bundles,
+ * Note: this is a dev-mode only class and it does not appear in prod bundles,
* thus there is no `ngDevMode` use in the code.
*
* Based on https://web.dev/lcp/#measure-lcp-in-javascript.
@@ -35,22 +35,24 @@ export class LCPImageObserver implements OnDestroy {
private window: Window|null = null;
private observer: PerformanceObserver|null = null;
- constructor(@Inject(DOCUMENT) doc: Document) {
+ constructor() {
assertDevMode('LCP checker');
- const win = doc.defaultView;
+ const win = inject(DOCUMENT).defaultView;
if (typeof win !== 'undefined' && typeof PerformanceObserver !== 'undefined') {
this.window = win;
this.observer = this.initPerformanceObserver();
}
}
- // Inits PerformanceObserver and subscribes to LCP events.
- // Based on https://web.dev/lcp/#measure-lcp-in-javascript
+ /**
+ * Inits PerformanceObserver and subscribes to LCP events.
+ * Based on https://web.dev/lcp/#measure-lcp-in-javascript
+ */
private initPerformanceObserver(): PerformanceObserver {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
if (entries.length === 0) return;
- // Note: we use the latest entry produced by the `PerformanceObserver` as the best
+ // We use the latest entry produced by the `PerformanceObserver` as the best
// signal on which element is actually an LCP one. As an example, the first image to load on
// a page, by virtue of being the only thing on the page so far, is often a LCP candidate
// and gets reported by PerformanceObserver, but isn't necessarily the LCP element.
@@ -64,13 +66,7 @@ export class LCPImageObserver implements OnDestroy {
const imgRawSrc = this.images.get(imgSrc);
if (imgRawSrc && !this.alreadyWarned.has(imgSrc)) {
this.alreadyWarned.add(imgSrc);
- const directiveDetails = imgDirectiveDetails(imgRawSrc);
- console.warn(formatRuntimeError(
- RuntimeErrorCode.LCP_IMG_MISSING_PRIORITY,
- `${directiveDetails} this image is the Largest Contentful Paint (LCP) ` +
- `element but was not marked "priority". This image should be marked ` +
- `"priority" in order to prioritize its loading. ` +
- `To fix this, add the "priority" attribute.`));
+ logMissingPriorityWarning(imgSrc);
}
});
observer.observe({type: 'largest-contentful-paint', buffered: true});
@@ -94,3 +90,13 @@ export class LCPImageObserver implements OnDestroy {
this.alreadyWarned.clear();
}
}
+
+function logMissingPriorityWarning(rawSrc: string) {
+ const directiveDetails = imgDirectiveDetails(rawSrc);
+ console.warn(formatRuntimeError(
+ RuntimeErrorCode.LCP_IMG_MISSING_PRIORITY,
+ `${directiveDetails} this image is the Largest Contentful Paint (LCP) ` +
+ `element but was not marked "priority". This image should be marked ` +
+ `"priority" in order to prioritize its loading. ` +
+ `To fix this, add the "priority" attribute.`));
+}
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 96be133555e..4f64277d1a5 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
@@ -6,12 +6,12 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Directive, ElementRef, Inject, Injector, Input, NgModule, NgZone, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core';
+import {Directive, ElementRef, inject, Injector, Input, NgZone, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChanges, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core';
import {RuntimeErrorCode} from '../../errors';
import {imgDirectiveDetails} from './error_helper';
-import {IMAGE_LOADER, ImageLoader} from './image_loaders/image_loader';
+import {IMAGE_LOADER} from './image_loaders/image_loader';
import {LCPImageObserver} from './lcp_image_observer';
import {PreconnectLinkChecker} from './preconnect_link_checker';
@@ -55,15 +55,16 @@ export const RECOMMENDED_SRCSET_DENSITY_CAP = 2;
const ASPECT_RATIO_TOLERANCE = .1;
/**
- * The directive that helps to improve image loading performance by following best practices.
+ * Directive that improves image loading performance by enforcing best practices.
*
- * The `NgOptimizedImage` ensures that the loading of the LCP image is prioritized by:
+ * `NgOptimizedImage` ensures that the loading of the Largest Contentful Paint (LCP) image is
+ * prioritized by:
* - Automatically setting the `fetchpriority` attribute on the `` tag
* - Lazy loading non-priority images by default
* - Asserting that there is a corresponding preconnect link tag in the document head
*
* In addition, the directive:
- * - Generates appropriate asset URLs (if a corresponding loader function is provided)
+ * - Generates appropriate asset URLs if a corresponding `ImageLoader` function is provided
* - Requires that `width` and `height` are set
* - Warns if `width` or `height` have been set incorrectly
* - Warns if the image will be visually distorted when rendered
@@ -74,8 +75,10 @@ const ASPECT_RATIO_TOLERANCE = .1;
*
* Follow the steps below to enable and use the directive:
* 1. Import it into the necessary NgModule or a standalone Component.
- * 2. Configure a loader that you want to use.
+ * 2. Optionally provide an `ImageLoader` if you use an image hosting service.
* 3. Update the necessary `` tags in templates and replace `src` attributes with `rawSrc`.
+ * Using a `rawSrc` allows the directive to control when the `src` gets set, which triggers an image
+ * download.
*
* Step 1: import the `NgOptimizedImage` directive.
*
@@ -140,7 +143,7 @@ const ASPECT_RATIO_TOLERANCE = .1;
* ],
* ```
*
- * Step 3: update `` tags in templates to use `rawSrc` instead of `rawSrc`.
+ * Step 3: update `` tags in templates to use `rawSrc` instead of `src`.
*
* ```
*
@@ -154,20 +157,18 @@ const ASPECT_RATIO_TOLERANCE = .1;
selector: 'img[rawSrc]',
})
export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
- constructor(
- @Inject(IMAGE_LOADER) private imageLoader: ImageLoader, private renderer: Renderer2,
- private imgElement: ElementRef, private injector: Injector) {}
+ private imageLoader = inject(IMAGE_LOADER);
+ private renderer = inject(Renderer2);
+ private imgElement: HTMLImageElement = inject(ElementRef).nativeElement;
+ private injector = inject(Injector);
- // Private fields to keep normalized input values.
- private _width?: number;
- private _height?: number;
- private _priority = false;
-
- // Calculate the rewritten `src` once and store it.
- // This is needed to avoid repetitive calculations and make sure the directive cleanup in the
- // `ngOnDestroy` does not rely on the `IMAGE_LOADER` logic (which in turn can rely on some other
- // instance that might be already destroyed).
- private _rewrittenSrc: string|null = null;
+ /**
+ * Calculate the rewritten `src` once and store it.
+ * This is needed to avoid repetitive calculations and make sure the directive cleanup in the
+ * `ngOnDestroy` does not rely on the `IMAGE_LOADER` logic (which in turn can rely on some other
+ * instance that might be already destroyed).
+ */
+ private _renderedSrc: string|null = null;
/**
* Name of the source image.
@@ -190,34 +191,36 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
@Input() rawSrcset!: string;
/**
- * The intrinsic width of the image in px.
+ * The intrinsic width of the image in pixels.
*/
@Input()
set width(value: string|number|undefined) {
- ngDevMode && assertGreaterThanZeroNumber(this, value, 'width');
+ ngDevMode && assertGreaterThanZero(this, value, 'width');
this._width = inputToInteger(value);
}
get width(): number|undefined {
return this._width;
}
+ private _width?: number;
/**
- * The intrinsic height of the image in px.
+ * The intrinsic height of the image in pixels.
*/
@Input()
set height(value: string|number|undefined) {
- ngDevMode && assertGreaterThanZeroNumber(this, value, 'height');
+ ngDevMode && assertGreaterThanZero(this, value, 'height');
this._height = inputToInteger(value);
}
get height(): number|undefined {
return this._height;
}
+ private _height?: number;
/**
* 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.
+ *
+ * Setting images as loading='eager' or loading='auto' marks them
+ * as non-priority images. Avoid changing this input for priority images.
*/
@Input() loading?: string;
@@ -231,16 +234,22 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
get priority(): boolean {
return this._priority;
}
+ private _priority = false;
/**
- * Get a value of the `src` and `srcset` if they're set on a host `` element.
- * These inputs are needed to verify that there are no conflicting sources provided
- * at the same time (e.g. `src` and `rawSrc` together or `srcset` and `rawSrcset`,
- * thus causing an ambiguity on which src to use) and that images
- * don't start to load until a lazy loading strategy is set.
+ * 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
+ * with `rawSrc` and that images don't start to load until a lazy loading strategy is set.
* @internal
*/
@Input() src?: string;
+
+ /**
+ * Value of the `srcset` attribute if set on the host `` element.
+ * This input is exclusively read to assert that `srcset` is not set in conflict
+ * with `rawSrcset` and that images don't start to load until a lazy loading strategy is set.
+ * @internal
+ */
@Input() srcset?: string;
ngOnInit() {
@@ -250,7 +259,7 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
assertNoConflictingSrc(this);
assertNoConflictingSrcset(this);
assertNotBase64Image(this);
- assertNotBlobURL(this);
+ assertNotBlobUrl(this);
assertNonEmptyWidthAndHeight(this);
assertValidLoadingInput(this);
assertNoImageDistortion(this, this.imgElement, this.renderer);
@@ -304,12 +313,12 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
// ImageLoaderConfig supports setting a width property. However, we're not setting width here
// because if the developer uses rendered width instead of intrinsic width in the HTML width
// attribute, the image requested may be too small for 2x+ screens.
- if (!this._rewrittenSrc) {
+ if (!this._renderedSrc) {
const imgConfig = {src: this.rawSrc};
// Cache calculated image src to reuse it later in the code.
- this._rewrittenSrc = this.imageLoader(imgConfig);
+ this._renderedSrc = this.imageLoader(imgConfig);
}
- return this._rewrittenSrc;
+ return this._renderedSrc;
}
private getRewrittenSrcset(): string {
@@ -324,27 +333,31 @@ export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
ngOnDestroy() {
if (ngDevMode) {
- if (!this.priority && this._rewrittenSrc !== null) {
+ if (!this.priority && this._renderedSrc !== null) {
withLCPImageObserver(
this.injector,
- (observer: LCPImageObserver) => observer.unregisterImage(this._rewrittenSrc!));
+ (observer: LCPImageObserver) => observer.unregisterImage(this._renderedSrc!));
}
}
}
private setHostAttribute(name: string, value: string): void {
- this.renderer.setAttribute(this.imgElement.nativeElement, name, value);
+ this.renderer.setAttribute(this.imgElement, name, value);
}
}
/***** Helpers *****/
-// Convert input value to integer.
+/**
+ * Convert input value to integer.
+ */
function inputToInteger(value: string|number|undefined): number|undefined {
return typeof value === 'string' ? parseInt(value, 10) : value;
}
-// Convert input value to boolean.
+/**
+ * Convert input value to boolean.
+ */
function inputToBoolean(value: unknown): boolean {
return value != null && `${value}` !== 'false';
}
@@ -376,31 +389,37 @@ function withLCPImageObserver(
/***** Assert functions *****/
-// Verifies that there is no `src` set on a host element.
+/**
+ * Verifies that there is no `src` set on a host element.
+ */
function assertNoConflictingSrc(dir: NgOptimizedImage) {
if (dir.src) {
throw new RuntimeError(
RuntimeErrorCode.UNEXPECTED_SRC_ATTR,
`${imgDirectiveDetails(dir.rawSrc)} both \`src\` and \`rawSrc\` have been set. ` +
- `Supplying both of these attributes is not necessary and will break lazy loading. ` +
- `The NgOptimizedImage directive will set \`src\` itself based on the value of \`rawSrc\`. ` +
+ `Supplying both of these attributes breaks lazy loading. ` +
+ `The NgOptimizedImage directive sets \`src\` itself based on the value of \`rawSrc\`. ` +
`To fix this, please remove the \`src\` attribute.`);
}
}
-// Verifies that there is no `srcset` set on a host element.
+/**
+ * Verifies that there is no `srcset` set on a host element.
+ */
function assertNoConflictingSrcset(dir: NgOptimizedImage) {
if (dir.srcset) {
throw new RuntimeError(
RuntimeErrorCode.UNEXPECTED_SRCSET_ATTR,
`${imgDirectiveDetails(dir.rawSrc)} both \`srcset\` and \`rawSrcset\` have been set. ` +
- `Supplying both of these attributes is not necessary and will break lazy loading. ` +
- `The NgOptimizedImage directive will set \`srcset\` itself based on the value of ` +
+ `Supplying both of these attributes breaks lazy loading. ` +
+ `The NgOptimizedImage directive sets \`srcset\` itself based on the value of ` +
`\`rawSrcset\`. To fix this, please remove the \`srcset\` attribute.`);
}
}
-// Verifies that the `rawSrc` is not a Base64-encoded image.
+/**
+ * Verifies that the `rawSrc` is not a Base64-encoded image.
+ */
function assertNotBase64Image(dir: NgOptimizedImage) {
let rawSrc = dir.rawSrc.trim();
if (rawSrc.startsWith('data:')) {
@@ -416,8 +435,10 @@ function assertNotBase64Image(dir: NgOptimizedImage) {
}
}
-// Verifies that the `rawSrc` is not a Blob URL.
-function assertNotBlobURL(dir: NgOptimizedImage) {
+/**
+ * Verifies that the `rawSrc` is not a Blob URL.
+ */
+function assertNotBlobUrl(dir: NgOptimizedImage) {
const rawSrc = dir.rawSrc.trim();
if (rawSrc.startsWith('blob:')) {
throw new RuntimeError(
@@ -429,7 +450,9 @@ function assertNotBlobURL(dir: NgOptimizedImage) {
}
}
-// Verifies that the input is set to a non-empty string.
+/**
+ * Verifies that the input is set to a non-empty string.
+ */
function assertNonEmptyInput(dir: NgOptimizedImage, name: string, value: unknown) {
const isString = typeof value === 'string';
const isEmptyString = isString && value.trim() === '';
@@ -441,7 +464,9 @@ function assertNonEmptyInput(dir: NgOptimizedImage, name: string, value: unknown
}
}
-// Verifies that the `rawSrcset` is in a valid format, e.g. "100w, 200w" or "1x, 2x"
+/**
+ * Verifies that the `rawSrcset` is in a valid format, e.g. "100w, 200w" or "1x, 2x".
+ */
export function assertValidRawSrcset(dir: NgOptimizedImage, value: unknown) {
if (value == null) return;
assertNonEmptyInput(dir, 'rawSrcset', value);
@@ -481,8 +506,10 @@ function assertUnderDensityCap(dir: NgOptimizedImage, value: string) {
}
}
-// Creates a `RuntimeError` instance to represent a situation when an input is set after
-// the directive has initialized.
+/**
+ * Creates a `RuntimeError` instance to represent a situation when an input is set after
+ * the directive has initialized.
+ */
function postInitInputChangeError(dir: NgOptimizedImage, inputName: string): {} {
return new RuntimeError(
RuntimeErrorCode.UNEXPECTED_INPUT_CHANGE,
@@ -492,7 +519,9 @@ function postInitInputChangeError(dir: NgOptimizedImage, inputName: string): {}
`in an *ngIf that is gated on the necessary value.`);
}
-// Verify that none of the listed inputs has changed.
+/**
+ * Verify that none of the listed inputs has changed.
+ */
function assertNoPostInitInputChange(
dir: NgOptimizedImage, changes: SimpleChanges, inputs: string[]) {
inputs.forEach(input => {
@@ -510,9 +539,10 @@ function assertNoPostInitInputChange(
});
}
-// Verifies that a specified input is a number greater than 0.
-function assertGreaterThanZeroNumber(
- dir: NgOptimizedImage, inputValue: unknown, inputName: string) {
+/**
+ * Verifies that a specified input is a number greater than 0.
+ */
+function assertGreaterThanZero(dir: NgOptimizedImage, inputValue: unknown, inputName: string) {
const validNumber = typeof inputValue === 'number' && inputValue > 0;
const validString =
typeof inputValue === 'string' && /^\d+$/.test(inputValue.trim()) && parseInt(inputValue) > 0;
@@ -525,21 +555,25 @@ function assertGreaterThanZeroNumber(
}
}
-// Verifies that the rendered image is not visually distorted. Effectively this is checking:
-// - Whether the "width" and "height" attributes reflect the actual dimensions of the image.
-// - Whether image styling is "correct" (see below for a longer explanation).
+/**
+ * Verifies that the rendered image is not visually distorted. Effectively this is checking:
+ * - Whether the "width" and "height" attributes reflect the actual dimensions of the image.
+ * - Whether image styling is "correct" (see below for a longer explanation).
+ */
function assertNoImageDistortion(
- dir: NgOptimizedImage, element: ElementRef, renderer: Renderer2) {
- const img = element.nativeElement;
+ dir: NgOptimizedImage, img: HTMLImageElement, renderer: Renderer2) {
const removeListenerFn = renderer.listen(img, 'load', () => {
removeListenerFn();
- const renderedWidth = parseFloat(img.clientWidth);
- const renderedHeight = parseFloat(img.clientHeight);
+ // TODO: `clientWidth`, `clientHeight`, `naturalWidth` and `naturalHeight`
+ // are typed as number, but we run `parseFloat` (which accepts strings only).
+ // Verify whether `parseFloat` is needed in the cases below.
+ const renderedWidth = parseFloat(img.clientWidth as any);
+ const renderedHeight = parseFloat(img.clientHeight as any);
const renderedAspectRatio = renderedWidth / renderedHeight;
const nonZeroRenderedDimensions = renderedWidth !== 0 && renderedHeight !== 0;
- const intrinsicWidth = parseFloat(img.naturalWidth);
- const intrinsicHeight = parseFloat(img.naturalHeight);
+ const intrinsicWidth = parseFloat(img.naturalWidth as any);
+ const intrinsicHeight = parseFloat(img.naturalHeight as any);
const intrinsicAspectRatio = intrinsicWidth / intrinsicHeight;
const suppliedWidth = dir.width!;
@@ -583,7 +617,9 @@ function assertNoImageDistortion(
});
}
-// Verifies that a specified input is set.
+/**
+ * Verifies that a specified input is set.
+ */
function assertNonEmptyWidthAndHeight(dir: NgOptimizedImage) {
let missingAttributes = [];
if (dir.width === undefined) missingAttributes.push('width');
@@ -598,8 +634,10 @@ function assertNonEmptyWidthAndHeight(dir: NgOptimizedImage) {
}
}
-// Verifies that the `loading` attribute is set to a valid input &
-// is not used on priority images.
+/**
+ * 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(
diff --git a/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts b/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts
index a9fbc010637..a59051e22c2 100644
--- a/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts
+++ b/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts
@@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Inject, Injectable, InjectionToken, Optional, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core';
+import {inject, Injectable, InjectionToken, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core';
import {DOCUMENT} from '../../dom_tokens';
import {RuntimeErrorCode} from '../../errors';
@@ -49,25 +49,28 @@ export const PRECONNECT_CHECK_BLOCKLIST =
*/
@Injectable({providedIn: 'root'})
export class PreconnectLinkChecker {
- // Set of tags found on this page.
- // The `null` value indicates that there was no DOM query operation performed.
+ /**
+ * Set of tags found on this page.
+ * The `null` value indicates that there was no DOM query operation performed.
+ */
private preconnectLinks: Set|null = null;
- // Keep track of all already seen origin URLs to avoid repeating the same check.
+ /*
+ * Keep track of all already seen origin URLs to avoid repeating the same check.
+ */
private alreadySeen = new Set();
private window: Window|null = null;
private blocklist = new Set(INTERNAL_PRECONNECT_CHECK_BLOCKLIST);
- constructor(
- @Inject(DOCUMENT) private doc: Document,
- @Optional() @Inject(PRECONNECT_CHECK_BLOCKLIST) blocklist: Array|null) {
+ constructor() {
assertDevMode('preconnect link checker');
- const win = doc.defaultView;
+ const win = inject(DOCUMENT).defaultView;
if (typeof win !== 'undefined') {
this.window = win;
}
+ const blocklist = inject(PRECONNECT_CHECK_BLOCKLIST, {optional: true});
if (blocklist) {
this.populateBlocklist(blocklist);
}
@@ -124,8 +127,7 @@ export class PreconnectLinkChecker {
private queryPreconnectLinks(): Set {
const preconnectUrls = new Set();
const selector = 'link[rel=preconnect]';
- const links =
- Array.from(document.querySelectorAll('link[rel=preconnect]')) as HTMLLinkElement[];
+ const links: HTMLLinkElement[] = Array.from(document.querySelectorAll(selector));
for (let link of links) {
const url = getUrl(link.href, this.window!);
preconnectUrls.add(url.origin);
@@ -139,8 +141,10 @@ export class PreconnectLinkChecker {
}
}
-// Invokes a callback for each element in the array. Also invokes a callback
-// recursively for each nested array.
+/**
+ * Invokes a callback for each element in the array. Also invokes a callback
+ * recursively for each nested array.
+ */
function deepForEach(input: (T|any[])[], fn: (value: T) => void): void {
for (let value of input) {
Array.isArray(value) ? deepForEach(value, fn) : fn(value);
diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts
index 3a53f139ea3..be4f5bf9412 100644
--- a/packages/common/test/directives/ng_optimized_image_spec.ts
+++ b/packages/common/test/directives/ng_optimized_image_spec.ts
@@ -94,9 +94,9 @@ describe('Image directive', () => {
'NG02950: The NgOptimizedImage directive (activated on an ' +
'element with the `rawSrc="path/img.png"`) has detected that both ' +
'`src` and `rawSrc` have been set. Supplying both of these attributes ' +
- 'is not necessary and will break lazy loading. ' +
- 'The NgOptimizedImage directive will set `src` itself based on the ' +
- 'value of `rawSrc`. To fix this, please remove the `src` attribute.');
+ 'breaks lazy loading. The NgOptimizedImage directive sets `src` ' +
+ 'itself based on the value of `rawSrc`. To fix this, please remove ' +
+ 'the `src` attribute.');
});
it('should throw if both `rawSrc` and `srcset` is present', () => {
@@ -112,8 +112,8 @@ describe('Image directive', () => {
'NG02951: The NgOptimizedImage directive (activated on an ' +
'element with the `rawSrc="img-100.png"`) has detected that both ' +
'`srcset` and `rawSrcset` have been set. Supplying both of these ' +
- 'attributes is not necessary and will break lazy loading. ' +
- 'The NgOptimizedImage directive will set `srcset` itself based ' +
+ 'attributes breaks lazy loading. ' +
+ 'The NgOptimizedImage directive sets `srcset` itself based ' +
'on the value of `rawSrcset`. To fix this, please remove the `srcset` ' +
'attribute.');
});