Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 47 additions & 31 deletions src/runtime/components/NuxtImg.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img
v-if="!custom"
ref="imgEl"
:class="placeholder ? placeholderClass : undefined"
:class="placeholderSrc ? _placeholderClass : undefined"
v-bind="imgAttrs"
:src="src"
>
Expand Down Expand Up @@ -45,6 +45,7 @@ const emit = defineEmits<{
defineSlots<{ default(props: DefaultSlotProps): any }>()

const $img = useImage()

const { providerOptions, normalizedAttrs, imageModifiers } = useImageProps(props)

const sizes = computed(() => $img.getSizes(props.src!, {
Expand All @@ -56,43 +57,46 @@ const sizes = computed(() => $img.getSizes(props.src!, {

const placeholderLoaded = ref(false)

const attrs = useAttrs() as ImgHTMLAttributes
const imgAttrs = computed(() => ({
...normalizedAttrs.value,
'data-nuxt-img': '',
...(!props.placeholder || placeholderLoaded.value)
? { sizes: sizes.value.sizes, srcset: sizes.value.srcset }
: {},
...import.meta.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {},
...attrs,
}))
const _placeholder = computed(
() => props.placeholder || (props.preset && $img.options.presets[props.preset]?.placeholder),
)

const _placeholderClass = computed(
() => props.placeholderClass || (props.preset && $img.options.presets[props.preset]?.placeholderClass),
)

const placeholder = computed(() => {
const placeholderSrc = computed(() => {
if (placeholderLoaded.value) {
return false
return undefined
}

const placeholder = props.placeholder === '' ? [10, 10] : props.placeholder
const src = _placeholder.value

if (!placeholder) {
return false
if (!src) {
return undefined
}

if (typeof placeholder === 'string') {
return placeholder
if (typeof src === 'string') {
return src
}

const [width = 10, height = width, quality = 50, blur = 3] = Array.isArray(placeholder)
? placeholder
: typeof placeholder === 'number' ? [placeholder] : []

return $img(props.src!, {
...imageModifiers.value,
width,
height,
quality,
blur,
}, providerOptions.value)
const [width = 10, height = width, quality = 50, blur = 3] = Array.isArray(src)
? src
: typeof src === 'number'
? [src]
: []

return $img(
props.src!,
{
...imageModifiers.value,
width,
height,
quality,
blur,
},
providerOptions.value,
)
})

const mainSrc = computed(() =>
Expand All @@ -101,7 +105,19 @@ const mainSrc = computed(() =>
: $img(props.src!, imageModifiers.value, providerOptions.value),
)

const src = computed(() => placeholder.value || mainSrc.value)
const src = computed(() => placeholderSrc.value || mainSrc.value)

const attrs = useAttrs() as ImgHTMLAttributes

const imgAttrs = computed(() => ({
...normalizedAttrs.value,
'data-nuxt-img': '',
...!placeholderSrc.value
? { sizes: sizes.value.sizes, srcset: sizes.value.srcset }
: {},
...import.meta.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {},
...attrs,
}))

if (import.meta.server && props.preload) {
const hasMultipleDensities = sizes.value.srcset.includes('x, ')
Expand Down Expand Up @@ -134,7 +150,7 @@ const imgEl = useTemplateRef('imgEl')
defineExpose({ imgEl })

onMounted(() => {
if (placeholder.value || props.custom) {
if (placeholderSrc.value || props.custom) {
const img = new Image()

if (mainSrc.value) {
Expand Down
6 changes: 6 additions & 0 deletions src/types/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export interface ImageOptions<Provider extends keyof ConfiguredImageProviders =
provider?: Provider
preset?: string
densities?: string
placeholder?:
| boolean
| string
| number
| [w: number, h: number, q?: number, b?: number]
placeholderClass?: string
modifiers?: Partial<Omit<ImageModifiers, 'format' | 'quality' | 'background' | 'fit'>>
& ('modifiers' extends keyof ConfiguredImageProviders[Provider] ? ConfiguredImageProviders[Provider]['modifiers'] : Record<string, unknown>)
sizes?: string | Record<string, any>
Expand Down
30 changes: 29 additions & 1 deletion test/nuxt/image.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ describe('Sizes and densities behavior', () => {
})
})

describe('Preset sizes and densities inheritance', () => {
describe('Preset inheritance', () => {
const src = '/image.png'

beforeEach(() => {
Expand Down Expand Up @@ -505,6 +505,34 @@ describe('Preset sizes and densities inheritance', () => {
expect(srcset).not.toMatch(/\b\d+w\b/)
expect(sizes).toBeFalsy()
})

it('preset placeholder and placeholderClass are inherited', () => {
setImageContext({
presets: {
withPlaceholder: {
placeholder: true,
placeholderClass: 'placeholder-class',
},
},
})

const wrapper = mount(NuxtImg, {
propsData: {
src,
width: 200,
height: 200,
preset: 'withPlaceholder',
},
})

const imgElement = wrapper.find('img').element
const domSrc = imgElement.getAttribute('src')

expect(domSrc).toMatchInlineSnapshot(
'"/_ipx/q_50&blur_3&s_10x10/image.png"',
)
expect(imgElement.classList).toContain('placeholder-class')
})
})

describe('Renders image, applies module config', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/unit/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe.skipIf(process.env.ECOSYSTEM_CI || isWindows)('nuxt image bundle size',
image: { provider: 'ipx' },
})

expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.6k"`)
expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.8k"`)
})
})

Expand Down
Loading