Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport NgOptimizedImage to v13 #49486

Merged
merged 1 commit into from Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .pullapprove.yml
Expand Up @@ -340,6 +340,7 @@ groups:
'aio/content/guide/dependency-injection-navtree.md',
'aio/content/guide/dependency-injection-providers.md',
'aio/content/guide/lightweight-injection-tokens.md',
'aio/content/guide/image-directive.md',
'aio/content/guide/displaying-data.md',
'aio/content/examples/displaying-data/**',
'aio/content/images/guide/displaying-data/**',
Expand Down
323 changes: 323 additions & 0 deletions aio/content/guide/image-directive.md

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions aio/content/navigation.json
Expand Up @@ -355,6 +355,11 @@
"title": "HTTP Client",
"tooltip": "Use HTTP to talk to a remote server."
},
{
"url": "guide/image-directive",
"title": "Image Directive",
"tooltip": "Performant images with the Angular image directive."
},
{
"title": "Testing",
"tooltip": "Testing your Angular apps.",
Expand Down
30 changes: 29 additions & 1 deletion goldens/public-api/common/errors.md
Expand Up @@ -6,10 +6,38 @@

// @public
export const enum RuntimeErrorCode {
// (undocumented)
INVALID_INPUT = 2952,
// (undocumented)
INVALID_LOADER_ARGUMENTS = 2959,
// (undocumented)
INVALID_PIPE_ARGUMENT = 2100,
// (undocumented)
PARENT_NG_SWITCH_NOT_FOUND = 2000
INVALID_PRECONNECT_CHECK_BLOCKLIST = 2957,
// (undocumented)
LCP_IMG_MISSING_PRIORITY = 2955,
// (undocumented)
MISSING_BUILTIN_LOADER = 2962,
// (undocumented)
MISSING_NECESSARY_LOADER = 2963,
// (undocumented)
OVERSIZED_IMAGE = 2960,
// (undocumented)
PARENT_NG_SWITCH_NOT_FOUND = 2000,
// (undocumented)
PRIORITY_IMG_MISSING_PRECONNECT_TAG = 2956,
// (undocumented)
REQUIRED_INPUT_MISSING = 2954,
// (undocumented)
TOO_MANY_PRELOADED_IMAGES = 2961,
// (undocumented)
UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958,
// (undocumented)
UNEXPECTED_INPUT_CHANGE = 2953,
// (undocumented)
UNEXPECTED_SRC_ATTR = 2950,
// (undocumented)
UNEXPECTED_SRCSET_ATTR = 2951
}

// (No @packageDocumentation comment for this package)
Expand Down
87 changes: 87 additions & 0 deletions goldens/public-api/common/index.md
Expand Up @@ -17,7 +17,9 @@ import { NgModuleFactory } from '@angular/core';
import { Observable } from 'rxjs';
import { OnChanges } from '@angular/core';
import { OnDestroy } from '@angular/core';
import { OnInit } from '@angular/core';
import { PipeTransform } from '@angular/core';
import { Provider } from '@angular/core';
import { Renderer2 } from '@angular/core';
import { SimpleChanges } from '@angular/core';
import { Subscribable } from 'rxjs';
Expand Down Expand Up @@ -254,6 +256,29 @@ export class I18nSelectPipe implements PipeTransform {
static ɵpipe: i0.ɵɵPipeDeclaration<I18nSelectPipe, "i18nSelect">;
}

// @public
export const IMAGE_CONFIG: InjectionToken<ImageConfig>;

// @public
export const IMAGE_LOADER: InjectionToken<ImageLoader>;

// @public
export type ImageConfig = {
breakpoints?: number[];
};

// @public
export type ImageLoader = (config: ImageLoaderConfig) => string;

// @public
export interface ImageLoaderConfig {
loaderParams?: {
[key: string]: any;
};
src: string;
width?: number;
}

// @public
export function isPlatformBrowser(platformId: Object): boolean;

Expand Down Expand Up @@ -509,6 +534,53 @@ export abstract class NgLocalization {
static ɵprov: i0.ɵɵInjectableDeclaration<NgLocalization>;
}

// @public
export class NgOptimizedImage implements OnInit, OnChanges, OnDestroy {
constructor(imageLoader: ImageLoader, config: ImageConfig, renderer: Renderer2, elementRef: ElementRef, injector: Injector, platformId: string, preloadLinkChecker: PreloadLinkCreator);
set disableOptimizedSrcset(value: string | boolean | undefined);
// (undocumented)
get disableOptimizedSrcset(): boolean;
set fill(value: string | boolean | undefined);
// (undocumented)
get fill(): boolean;
set height(value: string | number | undefined);
// (undocumented)
get height(): number | undefined;
loaderParams?: {
[key: string]: any;
};
loading?: 'lazy' | 'eager' | 'auto';
// (undocumented)
ngOnChanges(changes: SimpleChanges): void;
// (undocumented)
ngOnDestroy(): void;
// (undocumented)
ngOnInit(): void;
ngSrc: string;
ngSrcset: string;
set priority(value: string | boolean | undefined);
// (undocumented)
get priority(): boolean;
sizes?: string;
set width(value: string | number | undefined);
// (undocumented)
get width(): number | undefined;
// (undocumented)
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<NgOptimizedImage, never>;
}

// @public @deprecated
export class NgOptimizedImageModule {
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<NgOptimizedImageModule, never>;
// (undocumented)
static ɵinj: i0.ɵɵInjectorDeclaration<NgOptimizedImageModule>;
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<NgOptimizedImageModule, [typeof NgOptimizedImage], never, [typeof NgOptimizedImage]>;
}

// @public
export class NgPlural {
constructor(_localization: NgLocalization);
Expand Down Expand Up @@ -732,6 +804,21 @@ interface PopStateEvent_2 {
}
export { PopStateEvent_2 as PopStateEvent }

// @public
export const PRECONNECT_CHECK_BLOCKLIST: InjectionToken<(string | string[])[]>;

// @public
export const provideCloudflareLoader: (path: string) => Provider[];

// @public
export const provideCloudinaryLoader: (path: string) => Provider[];

// @public
export const provideImageKitLoader: (path: string) => Provider[];

// @public
export const provideImgixLoader: (path: string) => Provider[];

// @public
export function registerLocaleData(data: any, localeId?: string | any, extraData?: any): void;

Expand Down
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Expand Up @@ -33,7 +33,7 @@
"cli-hello-world-lazy": {
"uncompressed": {
"runtime": 2835,
"main": 230780,
"main": 230267,
"polyfills": 37244,
"src_app_lazy_lazy_module_ts": 795
}
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/common.ts
Expand Up @@ -27,3 +27,4 @@ export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPL
export {VERSION} from './version';
export {ViewportScroller, NullViewportScroller as ɵNullViewportScroller} from './viewport_scroller';
export {XhrFactory} from './xhr';
export {IMAGE_CONFIG, ImageConfig, IMAGE_LOADER, ImageLoader, ImageLoaderConfig, NgOptimizedImage, NgOptimizedImageModule, PRECONNECT_CHECK_BLOCKLIST, provideCloudflareLoader, provideCloudinaryLoader, provideImageKitLoader, provideImgixLoader} from './directives/ng_optimized_image';
6 changes: 5 additions & 1 deletion packages/common/src/directives/index.ts
Expand Up @@ -7,10 +7,12 @@
*/

import {Provider} from '@angular/core';

import {NgClass} from './ng_class';
import {NgComponentOutlet} from './ng_component_outlet';
import {NgForOf, NgForOfContext} from './ng_for_of';
import {NgIf, NgIfContext} from './ng_if';
import {NgOptimizedImage, NgOptimizedImageModule} from './ng_optimized_image/ng_optimized_image';
import {NgPlural, NgPluralCase} from './ng_plural';
import {NgStyle} from './ng_style';
import {NgSwitch, NgSwitchCase, NgSwitchDefault} from './ng_switch';
Expand All @@ -23,13 +25,15 @@ export {
NgForOfContext,
NgIf,
NgIfContext,
NgOptimizedImage,
NgOptimizedImageModule,
NgPlural,
NgPluralCase,
NgStyle,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,
NgTemplateOutlet,
NgTemplateOutlet
};


Expand Down
25 changes: 25 additions & 0 deletions packages/common/src/directives/ng_optimized_image/asserts.ts
@@ -0,0 +1,25 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {ɵRuntimeError as RuntimeError} from '@angular/core';

import {RuntimeErrorCode} from '../../errors';

/**
* 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(checkName: string) {
if (!ngDevMode) {
throw new RuntimeError(
RuntimeErrorCode.UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE,
`Unexpected invocation of the ${checkName} in the prod mode. ` +
`Please make sure that the prod mode is enabled for production builds.`);
}
}
14 changes: 14 additions & 0 deletions packages/common/src/directives/ng_optimized_image/error_helper.ts
@@ -0,0 +1,14 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

// Assembles directive details string, useful for error messages.
export function imgDirectiveDetails(ngSrc: string, includeNgSrc = true) {
const ngSrcInfo =
includeNgSrc ? `(activated on an <img> element with the \`ngSrc="${ngSrc}"\`) ` : '';
return `The NgOptimizedImage directive ${ngSrcInfo}has detected that`;
}
@@ -0,0 +1,35 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {createImageLoader, ImageLoaderConfig} from './image_loader';

/**
* Function that generates an ImageLoader for [Cloudflare Image
* Resizing](https://developers.cloudflare.com/images/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
* @returns Provider that provides an ImageLoader function
*
* @publicApi
*/
export const provideCloudflareLoader = createImageLoader(
createCloudflareUrl,
ngDevMode ? ['https://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE>'] : undefined);

// Exported for testing purposes in backport only. Not to be accessed except in unit tests.
export function createCloudflareUrl(path: string, config: ImageLoaderConfig) {
let params = `format=auto`;
if (config.width) {
params += `,width=${config.width}`;
}
// Cloudflare image URLs format:
// https://developers.cloudflare.com/images/image-resizing/url-format/
return `${path}/cdn-cgi/image/${params}/${config.src}`;
}
@@ -0,0 +1,59 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {createImageLoader, ImageLoaderConfig, ImageLoaderInfo} from './image_loader';

/**
* Name and URL tester for Cloudinary.
*/
export const cloudinaryLoaderInfo: ImageLoaderInfo = {
name: 'Cloudinary',
testUrl: isCloudinaryUrl
};

const CLOUDINARY_LOADER_REGEX = /https?\:\/\/[^\/]+\.cloudinary\.com\/.+/;
/**
* Tests whether a URL is from Cloudinary CDN.
*/
function isCloudinaryUrl(url: string): boolean {
return CLOUDINARY_LOADER_REGEX.test(url);
}

/**
* 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
* @returns Set of providers to configure the Cloudinary loader.
*
* @publicApi
*/
export const provideCloudinaryLoader = createImageLoader(
createCloudinaryUrl,
ngDevMode ?
[
'https://res.cloudinary.com/mysite', 'https://mysite.cloudinary.com',
'https://subdomain.mysite.com'
] :
undefined);

// Exported for testing purposes in backport only. Not to be accessed except in unit tests.
export function createCloudinaryUrl(path: string, config: ImageLoaderConfig) {
// Cloudinary image URLformat:
// https://cloudinary.com/documentation/image_transformations#transformation_url_structure
// 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"
if (config.width) {
params += `,w_${config.width}`;
}
return `${path}/image/upload/${params}/${config.src}`;
}