Repository: nuxt/image
Pull request #1111 (2023-11-15 15:23:28)

feat(nuxt-img): add `placeholderClass` prop
This allows a user to apply a custom class to the underyling original `<img>` element.

```html
<!-- Apply a static class to the original image -->
<nuxt-img src="/nuxt.png" img-class="custom" />

<!-- Apply a dynamic class to the original image -->
<nuxt-img src="/nuxt.png" :img-class="custom" />
```

Use case:
- The user wants to utilize the `placeholder` prop to have a fallback image, but wants to manipulate the loaded image src's display directly, rather than using `:deep` to style the `<img>` rendered by `<NuxtImg>` .

nuxt-studio[bot]: 
### <span aria-hidden="true">✅</span> Live Preview ready!

| Name | Edit | Preview | Latest Commit |
| :--- | :----- | :------ | :------ |
| **Image** | [Edit on Studio ↗︎](https://nuxt.studio/@nuxt/image/studio/content?refId=%231111) | [View Live Preview](https://image.nuxt.com?preview=f11420b6855c2599be52f00aafbbe02b) | 1363890005bd81887b97ff0c6060b1037ed85ea9 |

<!-- This comment is generated by [Nuxt Studio](https://nuxt.studio) -->
<!-- Do not remove or edit this comment. -->

danielroe: Is there a reason `<nuxt-img class="custom" />` doesn't work?
nathanchase: > Is there a reason `<nuxt-img class="custom" />` doesn't work?

Yes, because it will apply to the placeholder as well. There needs to be a way to style both the placeholder image AND the actual image differently.
danielroe: I wonder if it might be more useful to have a data attribute (`data-placeholder`?) to allow styling with/without placeholder directly in CSS rather than providing options for passing separate classes through. What do you think?
nathanchase: Possibly - although what if someone needs to style the placeholder?

The use case that led me down this path was that I wanted to have a placeholder image load, which then gets replaced by the loaded src image. This happens with an abrupt flip from placeholder to src image, which you could transition with CSS, but not without the ability to style both the placeholder AND the src image.
nathanchase: I suppose with `data-placeholder` you would be able to style it like so:
```
:deep(data-placeholder]) {
   // styles go here
}
```
but that isn't particularly elegant...
Atinux: Thank you for the PR! I would also go with the `data-placeholder` attribute seems the `class` is the most easy to understand about what you are styling.
nathanchase: As long as there is a way to style both the underlying image and the placeholder image, I'm ambivalent on the implementation.

So the thought is that you'd have to use `:deep` to style the `img`, and the `[data-placeholder]`?

Either way, that data-placeholder attribute would need to be added to the module.
nathanchase: Also, it would be nice to have an event that is emitted when the NuxtImg changes from displaying the placeholder image to the main image, so you could do something like:
```js
<NuxtImg src='/images/user.png' placeholder="/images/default_user.png" @placeholder-replaced="doSomething" /> // if you needed to hook into when the placeholder is replaced
```
and
```js
<NuxtImg src='/images/user.png' placeholder="/images/default_user.png" @main-image-loaded="doSomething" /> // if you needed to hook into the main image loading after placeholder was displayed
```
nathanchase: I've also been trying to figure out how to hook into the HTMLImageElement's complete property, as that does not seem to be an emitted event bubbled up to `<NuxtImg`.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/complete
danielroe: I feel it makes more sense to have a `placeholder-class` that applies to placeholder. Anything passed with `class` will apply to both placeholder + loaded image. You can style with `img:not(.my-placeholder-class)` for example.

How about that?

Patch:
diff --git a/docs/content/2.usage/1.nuxt-img.md b/docs/content/2.usage/1.nuxt-img.md
index 26e2bd5..594eb69 100644
--- a/docs/content/2.usage/1.nuxt-img.md
+++ b/docs/content/2.usage/1.nuxt-img.md
@@ -131,6 +131,27 @@ const img = useImage()
 </template>
 ```
 
+### `placeholder-class`
+
+When using a placeholder, you can use `placeholder-class` to apply a class to the original underlying `<img>` element (while the placeholder is being rendered).
+
+```html
+<!-- Apply a static class to the original image -->
+<nuxt-img src="/nuxt.png" placeholder placeholder-class="custom" />
+
+<!-- Apply a dynamic class to the original image -->
+<nuxt-img src="/nuxt.png" placeholder :placeholder-class="custom" />
+```
+
+::callout
+If you need to apply some CSS to only the _loaded_ image you can do so with something like:
+```css
+img:not(.my-placeholder-class) {
+  /* styles here */
+}
+```
+::
+
 ### `provider`
 
 Use other provider instead of default [provider option](/get-started/configuration#provider) specified in `nuxt.config`
diff --git a/src/runtime/components/nuxt-img.ts b/src/runtime/components/nuxt-img.ts
index 65adc15..37388aa 100644
--- a/src/runtime/components/nuxt-img.ts
+++ b/src/runtime/components/nuxt-img.ts
@@ -9,6 +9,7 @@ import { useHead, useNuxtApp } from '#imports'
 export const imgProps = {
   ...baseImageProps,
   placeholder: { type: [Boolean, String, Number, Array], default: undefined },
+  placeholderClass: { type: String, default: undefined },
 }
 
 export default defineComponent({
@@ -20,6 +21,7 @@ export default defineComponent({
     const _base = useBaseImage(props)
 
     const placeholderLoaded = ref(false)
+    const imgEl = ref<HTMLImageElement>()
 
     type AttrsT = typeof _base.attrs.value & {
       'sizes'?: string
@@ -103,8 +105,6 @@ export default defineComponent({
       prerenderStaticImages(src.value, sizes.value.srcset)
     }
 
-    const imgEl = ref<HTMLImageElement>()
-
     const nuxtApp = useNuxtApp()
     const initialLoad = nuxtApp.isHydrating
     onMounted(() => {
@@ -151,6 +151,7 @@ export default defineComponent({
       ...import.meta.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {},
       ...attrs.value,
       ...ctx.attrs,
+      class: props.placeholder && !placeholderLoaded.value ? [props.placeholderClass] : undefined,
     })
   },
 })


Test patch:
diff --git a/test/unit/image.test.ts b/test/unit/image.test.ts
index bf9b5ba..b51016a 100644
--- a/test/unit/image.test.ts
+++ b/test/unit/image.test.ts
@@ -248,6 +248,38 @@ describe('Renders placeholder image', () => {
     expect(sizes).toMatchInlineSnapshot('"(max-width: 500px) 200px, (max-width: 900px) 500px, 900px"')
     expect(wrapper.emitted().load[0]).toStrictEqual([loadEvent])
   })
+
+  it('placeholder class can be set', async () => {
+    const { resolve: resolveImage } = getImageLoad(() => {
+      wrapper = mount(NuxtImg, {
+        propsData: {
+          src,
+          placeholder: true,
+          placeholderClass: 'placeholder',
+        },
+        attrs: {
+          class: [{ test: true }, 'other'],
+        },
+      })
+    })
+    expect([...wrapper.element.classList]).toMatchInlineSnapshot(`
+      [
+        "placeholder",
+        "test",
+        "other",
+      ]
+    `)
+    expect(wrapper.element.getAttribute('src')).toMatchInlineSnapshot('"/_ipx/q_50&blur_3&s_10x10/image.png"')
+    resolveImage()
+    await nextTick()
+    expect([...wrapper.element.classList]).toMatchInlineSnapshot(`
+      [
+        "test",
+        "other",
+      ]
+    `)
+    expect(wrapper.element.getAttribute('src')).toMatchInlineSnapshot(`"/_ipx/_/image.png"`)
+  })
 })
 
 describe('Renders image, applies module config', () => {