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.'); });