Skip to content
This repository has been archived by the owner on Jun 21, 2023. It is now read-only.

Commit

Permalink
Add onLoadingComplete() prop to Image component (vercel#26824)
Browse files Browse the repository at this point in the history
This adds a new prop, `onLoadingComplete()`, to handle the most common use case of `ref`.

I also added docs and a warning when using `ref` so we recommend the new prop instead.
 
- Fixes vercel#18398 
- Fixes vercel#22482
  • Loading branch information
styfle authored Jul 1, 2021
1 parent 18296ae commit 61cdc72
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 20 deletions.
5 changes: 5 additions & 0 deletions docs/api-reference/next/image.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ The image position when using `layout="fill"`.

[Learn more](https://developer.mozilla.org/en-US/docs/Web/CSS/object-position)

### onLoadingComplete

A callback function that is invoked once the image is completely loaded and the placeholder has been removed.

### loading

> **Attention**: This property is only meant for advanced usage. Switching an
Expand Down Expand Up @@ -242,6 +246,7 @@ Other properties on the `<Image />` component will be passed to the underlying
- `srcSet`. Use
[Device Sizes](/docs/basic-features/image-optimization.md#device-sizes)
instead.
- `ref`. Use [`onLoadingComplete`](#onloadingcomplete) instead.
- `decoding`. It is always `"async"`.

## Related
Expand Down
54 changes: 34 additions & 20 deletions packages/next/client/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export type ImageProps = Omit<
unoptimized?: boolean
objectFit?: ImgElementStyle['objectFit']
objectPosition?: ImgElementStyle['objectPosition']
onLoadingComplete?: () => void
} & (StringImageProps | ObjectImageProps)

const {
Expand Down Expand Up @@ -261,30 +262,37 @@ function defaultImageLoader(loaderProps: ImageLoaderProps) {

// See https://stackoverflow.com/q/39777833/266535 for why we use this ref
// handler instead of the img's onLoad attribute.
function removePlaceholder(
function handleLoading(
img: HTMLImageElement | null,
placeholder: PlaceholderValue
placeholder: PlaceholderValue,
onLoadingComplete?: () => void
) {
if (placeholder === 'blur' && img) {
const handleLoad = () => {
if (!img.src.startsWith('data:')) {
const p = 'decode' in img ? img.decode() : Promise.resolve()
p.catch(() => {}).then(() => {
if (!img) {
return
}
const handleLoad = () => {
if (!img.src.startsWith('data:')) {
const p = 'decode' in img ? img.decode() : Promise.resolve()
p.catch(() => {}).then(() => {
if (placeholder === 'blur') {
img.style.filter = 'none'
img.style.backgroundSize = 'none'
img.style.backgroundImage = 'none'
})
}
}
if (img.complete) {
// If the real image fails to load, this will still remove the placeholder.
// This is the desired behavior for now, and will be revisited when error
// handling is worked on for the image component itself.
handleLoad()
} else {
img.onload = handleLoad
}
if (onLoadingComplete) {
onLoadingComplete()
}
})
}
}
if (img.complete) {
// If the real image fails to load, this will still remove the placeholder.
// This is the desired behavior for now, and will be revisited when error
// handling is worked on for the image component itself.
handleLoad()
} else {
img.onload = handleLoad
}
}

export default function Image({
Expand All @@ -299,6 +307,7 @@ export default function Image({
height,
objectFit,
objectPosition,
onLoadingComplete,
loader = defaultImageLoader,
placeholder = 'empty',
blurDataURL,
Expand Down Expand Up @@ -401,6 +410,11 @@ export default function Image({
)
}
}
if ('ref' in rest) {
console.warn(
`Image with src "${src}" is using unsupported "ref" property. Consider using the "onLoadingComplete" property instead.`
)
}
}
let isLazy =
!priority && (loading === 'lazy' || typeof loading === 'undefined')
Expand Down Expand Up @@ -589,9 +603,9 @@ export default function Image({
{...imgAttributes}
decoding="async"
className={className}
ref={(element) => {
setRef(element)
removePlaceholder(element, placeholder)
ref={(img) => {
setRef(img)
handleLoading(img, placeholder, onLoadingComplete)
}}
style={imgStyle}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useState } from 'react'
import Image from 'next/image'

const Page = () => (
<div>
<h1>On Loading Complete Test</h1>
<ImageWithMessage id="1" src="/test.jpg" />
<ImageWithMessage
id="2"
src={require('../public/test.png')}
placeholder="blur"
/>
</div>
)

function ImageWithMessage({ id, src }) {
const [msg, setMsg] = useState('[LOADING]')
return (
<>
<Image
id={`img${id}`}
src={src}
width="400"
height="400"
onLoadingComplete={() => setMsg(`loaded img${id}`)}
/>
<p id={`msg${id}`}>{msg}</p>
</>
)
}

export default Page
29 changes: 29 additions & 0 deletions test/integration/image-component/default/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,35 @@ function runTests(mode) {
}
})

it('should callback onLoadingComplete when image is fully loaded', async () => {
let browser
try {
browser = await webdriver(appPort, '/on-loading-complete')

await check(
() => browser.eval(`document.getElementById("img1").src`),
/test(.*)jpg/
)

await check(
() => browser.eval(`document.getElementById("img2").src`),
/test(.*).png/
)
await check(
() => browser.eval(`document.getElementById("msg1").textContent`),
'loaded img1'
)
await check(
() => browser.eval(`document.getElementById("msg2").textContent`),
'loaded img2'
)
} finally {
if (browser) {
await browser.close()
}
}
})

it('should work when using flexbox', async () => {
let browser
try {
Expand Down

0 comments on commit 61cdc72

Please sign in to comment.