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
feat(common): add NgOptimizedImage
directive
#47082
Conversation
This commit adds Image directive skeleton as well as a set of basic tests.
This commit exports experimental `NgImage` directive via a private API of the `@angular/common` package, so that it can be used acorss other Angular packages for testing purposes.
This commit adds a simple app that uses the `NgImage` directive to simplify further investigation and tests.
…dImage We want it to be clear what benefits the image directive confers over a normal <img> tag, and the `NgImage` name didn't provide much information. `NgOptimizedImage` makes it obvious that the new directive is intended to improve performance.
…token This commit updates the `NgOptimizedImage` directive to drop the `loader` input. Component-specific loaders can still be configured via `IMAGE_LOADER` token and the `loader` input was only useful in case different loaders have to be present in a single template, which doesn't seem to be a common case. We'll be able to re-introduce the input later if needed.
This commit changes the `NgOptimizedImage` directive selector from `raw-src` to `rawSrc` to better align with the styleguide.
…directive This commit updates the `NgOptimizedImage` directive to throw an error when `data:` and `blob:` inputs are used.
…tandalone This commit updates the `NgOptimizedImage` directive as standalone, so it's easier to import it in an app (without importing any NgModules). The `NgOptimizedImageModule` is removed as no longer needed.
This commit updates the `NgOptimizeImage` directive to add asserts to make sure no inputs are changed after directive initialization.
…ssing or invalid This commit updates the `NgOptimizeImage` directive to add asserts to make sure the `width` and `height` inputs are set and have correct values (numbers only).
…different hook Currently, the logic that sets the `src` on the host `<img>` element is located in the `ngOnChanges` lifecycle hook and guarded by the dev-mode checks that the inputs do not change. However, those checks would be tree-shaken in prod mode and the `src` would be set each time the `ngOnChanges` hook is invoked. This is undesirable and may potentially lead to performance issues. This commit moves the `src`-related logic to the `ngOnInit` hook instead, which would have the same effect (executed only once, after all inputs are set) and would behave consistently in dev and prod modes.
…age` test app This commit adds the necessary e2e testing infrastructure to the `NgOptimizedImage` test app, so that the test coverage can be extended and extra scenarios can be tested in a browser.
…priority` is set This commit adds extra logic into the `NgOptimizedImage` experimental directive to detect an LCP image and assert whether the `priority` attribute is applied.
…age` directive This commit adds e2e tests for the LCP check logic. Those tests are needed to verify the behavior in a real browser (vs relying on a Node environment).
This commit optimizes the logic that monitors whether a give image is an LCP element. If an image has the `priority` attribute set, there is no need to include it into monitoring. Also, if we already warned about a particular image (via a `console.warn`) - there is no need to warn again later (to avoid spamming a console).
Previously NgOptimizedImage would default to requesting an image at the width matching the width attribute in the image HTML. While this works for width attrs that match the intrinsic size of the image (e.g. responsive images or images sized with CSS), this can be a sharp edge for developers who use the width/height attributes to set rendered size (i.e. instead of CSS, which one can do for a fixed size image). In this case, defaulting to the width attribute for the requested image would mean requesting always at 1x quality, so screens with a DPR of 2+ get an image that is too small. Without a default request width, the image served by the CDN would likely be at intrinsic size, so 2x images would look correct. This PR also updates the NgOptimizedImage sandbox and tests.
The CI failed to run on the last PR, so we didn't catch that there were web tests failing due to absolute/relative URL issues. This commit should fix the issue.
…tests This commit updates the e2e folder to separate e2e tests and playground scripts (so it's clear where files are used).
This commit enables publishing of snapshots for the `image-directive` feature branch. The artifacts can be accessed with the following steps: 1. Land your change in `image-directive` 2. Go to the corresponding snapshot repo (e.g. `angular/common-builds`) 3. Go to the `image-directive` branch 4. Copy the SHA of the latest commit in that branch 5. Use that SHA to install via NPM. e.g. `https://github.com/angular/common-builds.git#SHA`.
…tive This commit updates the `NgOptimizedImage` directive to drop the `standalone` flag and create a new NgModule which declares and exports it, so that the directive can be used in apps that use pre-v14 version of Angular.
Currently if you use a static `srcset` with the image directive, lazy loading no longer works because the image would start to load before the loading attribute could be set to "lazy". This commit throws an error if you try to use `srcset` this way. In a follow-up commit, a new attribute will be added to support responsive images in a lazy-loading-friendly way.
This commit adds a `rawSrcset` attribute as a replacement for the `srcset` attribute. The `srcset` attribute cannot be set statically on the image directive because it would cause images to start downloading before the "loading" attribute could be set to "lazy". Changing the name to `rawSrcset` allows the directive to control the timing of image loading. It also makes it possible to support custom loaders for `srcset` file names. Rather than having to repeat the image origin for each image, the existing `rawSrc` value and image loader can be composed to generate each full URL string. The developer must only provide the necessary widths for the `srcset`. For example, the developer might write: ```markup <img rawSrc="hermes.jpg" rawSrcset="100w, 200w" ... /> ``` with a loader like: ```js const loader = (config) => `path/${config.src}?w=${config.width}`; ``` and the img tag will ultimately be set up as something like: ```markup <img src="path/hermes.jpg?w=100" srcset="path/hermes.jpg?w=100 100w, path/hermes.jpg?w=200 200w" .../> ```
Add loading attribute to NgOptimizedImage.
This commit updates the error codes used in the `NgOptimizedImage` tests. The error codes got u[dated recently in a PR that got merged earlier.
This commit adds a built-in Imgix loader for the NgOptimizedImage directive. If you provide the desired Imgix hostname, an ImageLoader will be generated with the correct options. Usage looks like this: ```ts providers: [ provideImgixLoader('https://some.imgix.net') ] ``` It sets the "auto=format" flag by default, which ensures that the smallest image format supported by the browser is served. This change also moves the IMAGE_LOADER, ImageLoader, and ImageLoaderConfig into a new directory that will be shared by all built-in image loaders.
This commit updates the `NgOptimizedImage` directive to add a logic to detect whether an image, marked with the "priority" attribute has a corresponding `<link rel="preconnect">` tag in the `document.head` for better performance.
…hecks in NgOptimizedImage This commit adds an extra logic to add an ability to exclude origins from preconnect checks in NgOptimizedImage by configuring the `PRECONNECT_CHECK_BLOCKLIST` multi-provider.
…nnect checks in NgOptimizedImage
A small refactor that removes explicit toString calls when not needed. PR Close #47082
This refactoring pulls url-related error reporting into one place. It also makes sure that error messages and the related error reporting logic are tree-shakable. PR Close #47082
This commit moves the URL normalization logic in loaders to the common loader logic. PR Close #47082
This commit simplifies the URL construction logic by capturing the base path and loader config argumnets in one function. PR Close #47082
With this commit, the NgOptimizedImage directive will throw a runtime error if it detects that one of the density descriptors in rawSrcset is higher than 3x. It's generally not recommended to use density descriptors higher than ~2, as it causes image to download at very large sizes on mobile screens (thus slowing down LCP). The density max is set conservatively to 3 in case apps expect users to zoom in. In future commits, we may want to throw even at densities > than 2 and provide a configuration override for the zoom case. PR Close #47082
Update error message text to explain why width & height are required attributes. PR Close #47082
This commit updates the NgOptimizedImage directive to become standalone and removes no longer needed NgOptimizedImageModule class. PR Close #47082
This commit fixes a bug in NgOptimizedImage where if you bound the width or height attribute (e.g. `[width]=width`), the attribute would only be reflected as "ng-reflect-x". The actual "width" or "height" attribute would not be set on the host element. This is a problem because the exact named attribute must be set on the element for the browser to detect it and use it to reserve space for the image (and thus prevent CLS). PR Close #47082
Addresses part of the review feedback for the NgOptimizedImage directive. PR Close #47082
…47082) This commit adds a console warning in development mode if the ultimate rendered size of the image is much smaller than the dimensions of the requested image. In this case, the warning recommends adjusting the size of the source image or using the `rawSrcset` attribute to implement responsive sizing. PR Close #47082
* RegExpr to determine whether a src in a srcset is using density descriptors. | ||
* Should match something like: "1x, 2x". | ||
*/ | ||
const VALID_DENSITY_DESCRIPTOR_SRCSET = /^((\s*\d(\.\d)?x\s*(,|$)){1,})$/; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about density descriptors with more that one digits (in the integer or fractional part) or omitting the integer part. For example, the current regex will not match the following:
1.25x
10x
.9x
From what I understand by reading the spec, these are valid density descriptors.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10x
- This one, we intentionally don't support because we have a max density of 3 that we enforce with a warning/error.
.9x
- While technically correct, I don't know of any devices that actually have 0.9x DPR? I don't think we need to support this. Curious what Katie/Alex think on this one though.
1.25x
- Ah, I think we are missing multiple digit support for decimals (our existing test just looks for 1.5). Will fix this in a follow-up! Thanks for catching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to support DPRs < 1. Although it's technically possible for devices have that DPR, I think it would imply the existence of a common display format "slightly worse than a CRT display" (which AFAIK is not a thing).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
10x
This one, we intentionally don't support because we have a max density of 3 that we enforce with a warning/error.
That's the point. Without properly detecting 10x
as a valid density descriptor, we throw a confusing error (that the descriptor is not in a valid format) instead of throwing the intended error that the density is too high 😃
Notice in the code below that isValidDensityDescriptor
will be false
for 10x
, which will skip the assertUnderDensityCap()
check and throw the INVALID_INPUT
error instead:
angular/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts
Lines 480 to 493 in 1555226
const isValidDensityDescriptor = VALID_DENSITY_DESCRIPTOR_SRCSET.test(stringVal); | |
if (isValidDensityDescriptor) { | |
assertUnderDensityCap(dir, stringVal); | |
} | |
const isValidSrcset = isValidWidthDescriptor || isValidDensityDescriptor; | |
if (!isValidSrcset) { | |
throw new RuntimeError( | |
RuntimeErrorCode.INVALID_INPUT, | |
`${imgDirectiveDetails(dir.rawSrc)} \`rawSrcset\` has an invalid value (\`${value}\`). ` + | |
`To fix this, supply \`rawSrcset\` using a comma-separated list of one or more width ` + | |
`descriptors (e.g. "100w, 200w") or density descriptors (e.g. "1x, 2x").`); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohh I see. Yeah, that's definitely not what we want. Will fix that in a follow-up too 👍
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@angular/animations](https://github.com/angular/angular) | dependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fanimations/14.1.3/14.2.0) | | [@angular/common](https://github.com/angular/angular) | dependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fcommon/14.1.3/14.2.0) | | [@angular/compiler](https://github.com/angular/angular) | dependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fcompiler/14.1.3/14.2.0) | | [@angular/compiler-cli](https://github.com/angular/angular/tree/main/packages/compiler-cli) ([source](https://github.com/angular/angular)) | devDependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fcompiler-cli/14.1.3/14.2.0) | | [@angular/core](https://github.com/angular/angular) | dependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fcore/14.1.3/14.2.0) | | [@angular/forms](https://github.com/angular/angular) | dependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fforms/14.1.3/14.2.0) | | [@angular/platform-browser](https://github.com/angular/angular) | dependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser/14.1.3/14.2.0) | | [@angular/platform-browser-dynamic](https://github.com/angular/angular) | dependencies | minor | [`14.1.3` -> `14.2.0`](https://renovatebot.com/diffs/npm/@angular%2fplatform-browser-dynamic/14.1.3/14.2.0) | --- ### Release Notes <details> <summary>angular/angular</summary> ### [`v14.2.0`](https://github.com/angular/angular/blob/HEAD/CHANGELOG.md#​1420-2022-08-25) [Compare Source](angular/angular@14.1.3...14.2.0) ##### animations | Commit | Type | Description | | -- | -- | -- | | [b96e571897](angular/angular@b96e571) | fix | fix stagger timing not handling params ([#​47208](angular/angular#47208)) | ##### common | Commit | Type | Description | | -- | -- | -- | | [b380fdd59e](angular/angular@b380fdd) | feat | add a density cap for image srcsets ([#​47082](angular/angular#47082)) | | [7ce497e5bc](angular/angular@7ce497e) | feat | add built-in Imgix loader ([#​47082](angular/angular#47082)) | | [bff870db61](angular/angular@bff870d) | feat | add cloudflare loader ([#​47082](angular/angular#47082)) | | [86e77a5d55](angular/angular@86e77a5) | feat | add Image directive skeleton ([#​45627](angular/angular#45627)) ([#​47082](angular/angular#47082)) | | [0566205a02](angular/angular@0566205) | feat | Add image lazy loading and fetchpriority ([#​47082](angular/angular#47082)) | | [4e952ba216](angular/angular@4e952ba) | feat | add loaders for cloudinary & imagekit ([#​47082](angular/angular#47082)) | | [e854a8cdde](angular/angular@e854a8c) | feat | add loading attr to NgOptimizedImage ([#​47082](angular/angular#47082)) | | [8d3701cb4c](angular/angular@8d3701c) | feat | add warnings re: image distortion ([#​47082](angular/angular#47082)) | | [d5f7da2120](angular/angular@d5f7da2) | feat | define public API surface for NgOptimizedImage directive ([#​47082](angular/angular#47082)) | | [d3c3426aa4](angular/angular@d3c3426) | feat | detect LCP images in `NgOptimizedImage` and assert if `priority` is set ([#​47082](angular/angular#47082)) | | [451b85ca17](angular/angular@451b85c) | feat | explain why width/height is required ([#​47082](angular/angular#47082)) | | [586274fe65](angular/angular@586274f) | feat | provide an ability to exclude origins from preconnect checks in NgOptimizedImage ([#​47082](angular/angular#47082)) | | [57f3386e5b](angular/angular@57f3386) | feat | support custom srcset attributes in NgOptimizedImage ([#​47082](angular/angular#47082)) | | [7baf9a46cd](angular/angular@7baf9a4) | feat | verify that priority images have preconnect links ([#​47082](angular/angular#47082)) | | [f81765b333](angular/angular@f81765b) | feat | warn if rendered size is much smaller than intrinsic ([#​47082](angular/angular#47082)) | | [e2ab99b95e](angular/angular@e2ab99b) | fix | allow null/undefined to be passed to ngClass input ([#​39280](angular/angular#39280)) ([#​46906](angular/angular#46906)) | | [bedf537951](angular/angular@bedf537) | fix | allow null/undefined to be passed to ngStyle input ([#​47069](angular/angular#47069)) | | [f9511bf6e8](angular/angular@f9511bf) | fix | avoid interacting with a destroyed injector ([#​47243](angular/angular#47243)) | | [dc29e21b14](angular/angular@dc29e21) | fix | consider density descriptors with multiple digits as valid ([#​47230](angular/angular#47230)) | | [801daf82d1](angular/angular@801daf8) | fix | detect `data:` and `blob:` inputs in `NgOptimizedImage` directive ([#​47082](angular/angular#47082)) | | [fff8056e7f](angular/angular@fff8056) | fix | fix formatting on oversized image error ([#​47188](angular/angular#47188)) ([#​47232](angular/angular#47232)) | | [1ca2ce19ab](angular/angular@1ca2ce1) | fix | remove default for image width ([#​47082](angular/angular#47082)) | | [c5db867ddc](angular/angular@c5db867) | fix | remove duplicate deepForEach ([#​47189](angular/angular#47189)) | | [1cf43deb18](angular/angular@1cf43de) | fix | sanitize `rawSrc` and `rawSrcset` values in NgOptimizedImage directive ([#​47082](angular/angular#47082)) | | [d71dfe931f](angular/angular@d71dfe9) | fix | set bound width and height onto host element ([#​47082](angular/angular#47082)) | | [32caa8b669](angular/angular@32caa8b) | fix | support density descriptors with 2+ decimals ([#​47197](angular/angular#47197)) ([#​47232](angular/angular#47232)) | | [ae4405f0bf](angular/angular@ae4405f) | fix | throw if srcset is used with rawSrc ([#​47082](angular/angular#47082)) | | [0c8eb8bc82](angular/angular@0c8eb8b) | perf | monitor LCP only for images without `priority` attribute ([#​47082](angular/angular#47082)) | ##### compiler-cli | Commit | Type | Description | | -- | -- | -- | | [ea89677c12](angular/angular@ea89677) | feat | support more recent version of `tsickle` ([#​47018](angular/angular#47018)) | ##### core | Commit | Type | Description | | -- | -- | -- | | [d1e83e1b30](angular/angular@d1e83e1) | feat | add `createComponent` function ([#​46685](angular/angular#46685)) | | [10becab70e](angular/angular@10becab) | feat | add `reflectComponentType` function ([#​46685](angular/angular#46685)) | | [4b377d3a6d](angular/angular@4b377d3) | feat | introduce createApplication API ([#​46475](angular/angular#46475)) | | [31429eaccc](angular/angular@31429ea) | feat | support TypeScript 4.8 ([#​47038](angular/angular#47038)) | | [796840209c](angular/angular@7968402) | fix | align TestBed interfaces and implementation ([#​46635](angular/angular#46635)) | ##### forms | Commit | Type | Description | | -- | -- | -- | | [426af91a42](angular/angular@426af91) | feat | add `FormBuilder.record()` method ([#​46485](angular/angular#46485)) | | [b302797de4](angular/angular@b302797) | fix | Correctly infer `FormBuilder` types involving `[value, validators]` shorthand in more cases. ([#​47034](angular/angular#47034)) | ##### language-service | Commit | Type | Description | | -- | -- | -- | | [598b72bd05](angular/angular@598b72b) | feat | support fix the component missing member ([#​46764](angular/angular#46764)) | ##### platform-browser | Commit | Type | Description | | -- | -- | -- | | [07606e3181](angular/angular@07606e3) | feat | add `isEmpty` method to the `TransferState` class ([#​46915](angular/angular#46915)) | ##### platform-server | Commit | Type | Description | | -- | -- | -- | | [2b4d7f6733](angular/angular@2b4d7f6) | feat | support document reference in render functions ([#​47032](angular/angular#47032)) | ##### router | Commit | Type | Description | | -- | -- | -- | | [0abb67af59](angular/angular@0abb67a) | feat | allow guards and resolvers to be plain functions ([#​46684](angular/angular#46684)) | | [75df404467](angular/angular@75df404) | feat | Create APIs for using Router without RouterModule ([#​47010](angular/angular#47010)) | | [10289f1f6e](angular/angular@10289f1) | feat | expose resolved route title ([#​46826](angular/angular#46826)) | | [8600732b09](angular/angular@8600732) | feat | Expose the default matcher for `Routes` used by the `Router` ([#​46913](angular/angular#46913)) | | [422323cee0](angular/angular@422323c) | feat | improve typings for RouterLink boolean inputs ([#​47101](angular/angular#47101)) | | [26ea97688c](angular/angular@26ea976) | feat | Make router directives standalone ([#​46758](angular/angular#46758)) | | [2a43beec15](angular/angular@2a43bee) | fix | Fix route recognition behavior with some versions of rxjs ([#​47098](angular/angular#47098)) | ##### service-worker | Commit | Type | Description | | -- | -- | -- | | [383090858c](angular/angular@3830908) | feat | support `sendRequest` as a `notificationclick` action ([#​46912](angular/angular#46912)) | | [3f548610dd](angular/angular@3f54861) | fix | export NoNewVersionDetectedEvent ([#​47044](angular/angular#47044)) | | [482b6119c2](angular/angular@482b611) | fix | update golden `index.md` ([#​47044](angular/angular#47044)) | #### Special Thanks Alex Rickabaugh, Andrew Kushnir, Andrew Scott, Bob Watson, Cédric Exbrayat, Dylan Hunn, Emmanuel Roux, FatalMerlin, George Kalpakas, Ilia Mirkin, Jan Kuehle, Jeremy Elbourn, Jessica Janiuk, JiaLiPassion, Kalbarczyk, Kara Erickson, Katie Hempenius, Kristiyan Kostadinov, Merlin, Paul Gschwendtner, Pawel Kozlowski, Tristan Sprößer, Victor Porof, angular-robot\[bot], dario-piotrowicz, ivanwonder and vyom <!-- CHANGELOG SPLIT MARKER --> </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox. --- This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzMi4xNzQuMiIsInVwZGF0ZWRJblZlciI6IjMyLjE3Ny4xIn0=--> Co-authored-by: cabr2-bot <cabr2.help@gmail.com> Reviewed-on: https://codeberg.org/Calciumdibromid/CaBr2/pulls/1523 Reviewed-by: Epsilon_02 <epsilon_02@noreply.codeberg.org> Co-authored-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org> Co-committed-by: Calciumdibromid Bot <cabr2_bot@noreply.codeberg.org>
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
This PR brings the code of the NgOptimizedImage directive into the main branch.
The development happened in a separate
image-directive
branch and all incoming code was reviewed by both Aurora and Angular teams. Before merging the code, we are conducting one more round of code reviews in this PR.Notes:
NgOptimizedImage
directive is marked as standalone, so it can be imported and used directlyNgOptimizedImage
directive is a part of the@angular/common
NPM package, it would NOT be included into theCommonModule
for some time (we'll consider doing that later once all APIs are stabilized)// cc @pkozlowski-opensource @kara @khempenius
PR Type
What kind of change does this PR introduce?
Does this PR introduce a breaking change?