Skip to content

Commit bd29062

Browse files
committed
✨ Add ImageLoader component
1 parent 615d432 commit bd29062

13 files changed

Lines changed: 301 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ import { Accordion } from 'webcoreui/react'
299299
- [Group](https://github.com/Frontendland/webcoreui/tree/main/src/components/Group)
300300
- [Icon](https://github.com/Frontendland/webcoreui/tree/main/src/components/Icon)
301301
- [Image](https://github.com/Frontendland/webcoreui/tree/main/src/components/Image)
302+
- [ImageLoader](https://github.com/Frontendland/webcoreui/tree/main/src/components/ImageLoader)
302303
- [Input](https://github.com/Frontendland/webcoreui/tree/main/src/components/Input)
303304
- [Kbd](https://github.com/Frontendland/webcoreui/tree/main/src/components/Kbd)
304305
- [List](https://github.com/Frontendland/webcoreui/tree/main/src/components/List)

scripts/buildTypes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const getTypeName = (component, framework) => {
1212
'Breadcrumb',
1313
'Icon',
1414
'Image',
15+
'ImageLoader',
1516
'OTPInput',
1617
'Rating',
1718
'Skeleton',

scripts/utilityTypes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ declare module 'webcoreui' {
157157
cancel: () => void
158158
}
159159
160-
export const get: (selector: string, all?: boolean) => Element | NodeListOf<Element> | null
160+
export const get: <T extends Element = Element>(
161+
selector: string,
162+
all?: boolean
163+
) => T | NodeListOf<T> | null
161164
export const on: (
162165
selector: string | HTMLElement | Document,
163166
event: string,
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
import type { ImageLoaderProps } from './imageloader'
3+
4+
import Image from '../Image/Image.astro'
5+
import Skeleton from '../Skeleton/Skeleton.astro'
6+
7+
import { classNames } from '../../utils/classNames'
8+
9+
import styles from './imageloader.module.scss'
10+
11+
interface Props extends ImageLoaderProps {}
12+
13+
const {
14+
fallback,
15+
animate,
16+
type,
17+
width,
18+
height,
19+
color,
20+
waveColor,
21+
className,
22+
...rest
23+
} = Astro.props
24+
25+
const styleVariables = classNames([
26+
`width: ${width}px;`,
27+
`height: ${height}px;`
28+
])
29+
---
30+
31+
<div data-id="w-image-loader" class={styles.loader} style={styleVariables}>
32+
<Skeleton
33+
animate={animate}
34+
type={type}
35+
width={Number(width)}
36+
height={Number(height)}
37+
color={color}
38+
waveColor={waveColor}
39+
className={className}
40+
/>
41+
<Image
42+
width={width}
43+
height={height}
44+
className={className}
45+
data-fallback={fallback}
46+
{...rest}
47+
/>
48+
</div>
49+
50+
<script>
51+
import { get, on } from '../../utils/DOMUtils'
52+
53+
const addEventListeners = () => {
54+
const images = get<HTMLImageElement>('[data-id="w-image-loader"] img', true)
55+
56+
if (!images || !(images instanceof NodeList)) {
57+
return
58+
}
59+
60+
images.forEach(img => {
61+
const container = img.parentElement as HTMLDivElement
62+
const skeleton = container.firstElementChild as HTMLDivElement
63+
64+
const handleError = () => {
65+
img.src = img.dataset.fallback || img.src
66+
skeleton?.remove()
67+
}
68+
69+
if (img.complete) {
70+
img.naturalWidth === 0 ? handleError() : skeleton?.remove()
71+
72+
return
73+
}
74+
75+
img.addEventListener('load', () => skeleton?.remove(), { once: true })
76+
img.addEventListener('error', () => handleError(), { once: true })
77+
})
78+
}
79+
80+
on(document, 'astro:after-swap', addEventListeners)
81+
addEventListeners()
82+
</script>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<script lang="ts">
2+
import { onMount } from 'svelte'
3+
import type { ImageLoaderProps } from './imageloader'
4+
5+
import Image from '../Image/Image.svelte'
6+
import Skeleton from '../Skeleton/Skeleton.svelte'
7+
8+
import { classNames } from '../../utils/classNames'
9+
10+
import styles from './imageloader.module.scss'
11+
12+
const {
13+
fallback,
14+
animate,
15+
type,
16+
width,
17+
height,
18+
color,
19+
waveColor,
20+
className,
21+
...rest
22+
}: ImageLoaderProps = $props()
23+
24+
let container: HTMLDivElement
25+
26+
const styleVariables = classNames([
27+
`width: ${width}px;`,
28+
`height: ${height}px;`
29+
])
30+
31+
onMount(() => {
32+
const img = container.querySelector<HTMLImageElement>('img')
33+
const skeleton = container.querySelector<HTMLDivElement>('div')
34+
35+
if (!img) {
36+
return
37+
}
38+
39+
const handleError = () => {
40+
img.src = img.dataset.fallback || img.src
41+
skeleton?.remove()
42+
}
43+
44+
if (img.complete) {
45+
img.naturalWidth === 0 ? handleError() : skeleton?.remove()
46+
47+
return
48+
}
49+
50+
img.addEventListener('load', () => skeleton?.remove(), { once: true })
51+
img.addEventListener('error', handleError, { once: true })
52+
})
53+
</script>
54+
55+
<div class={styles.loader} style={styleVariables} bind:this={container}>
56+
<Skeleton
57+
animate={animate}
58+
type={type}
59+
width={Number(width)}
60+
height={Number(height)}
61+
color={color}
62+
waveColor={waveColor}
63+
className={className}
64+
/>
65+
<Image
66+
width={width}
67+
height={height}
68+
className={className}
69+
data-fallback={fallback}
70+
{...rest}
71+
/>
72+
</div>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useEffect, useRef } from 'react'
2+
import type { ImageLoaderProps } from './imageloader'
3+
4+
import Image from '../Image/Image.tsx'
5+
import Skeleton from '../Skeleton/Skeleton.tsx'
6+
7+
import styles from './imageloader.module.scss'
8+
9+
const ImageLoader = ({
10+
fallback,
11+
animate,
12+
type,
13+
width,
14+
height,
15+
color,
16+
waveColor,
17+
className,
18+
...rest
19+
}: ImageLoaderProps) => {
20+
const containerRef = useRef<HTMLDivElement>(null)
21+
const styleVariables = {
22+
width,
23+
height
24+
}
25+
26+
useEffect(() => {
27+
if (!containerRef.current) {
28+
return
29+
}
30+
31+
const img = containerRef.current.querySelector<HTMLImageElement>('img')
32+
const skeleton = containerRef.current.querySelector<HTMLDivElement>('div')
33+
34+
if (!img) {
35+
return
36+
}
37+
38+
const removeSkeleton = () => skeleton?.remove()
39+
40+
const handleError = () => {
41+
img.src = img.dataset.fallback || img.src
42+
removeSkeleton()
43+
}
44+
45+
if (img.complete) {
46+
img.naturalWidth === 0 ? handleError() : removeSkeleton()
47+
48+
return
49+
}
50+
51+
img.addEventListener('load', removeSkeleton, { once: true })
52+
img.addEventListener('error', handleError, { once: true })
53+
54+
return () => {
55+
img.removeEventListener('load', removeSkeleton)
56+
img.removeEventListener('error', handleError)
57+
}
58+
}, [containerRef])
59+
60+
return (
61+
<div className={styles.loader} style={styleVariables} ref={containerRef}>
62+
<Skeleton
63+
animate={animate}
64+
type={type}
65+
width={Number(width)}
66+
height={Number(height)}
67+
color={color}
68+
waveColor={waveColor}
69+
className={className}
70+
/>
71+
<Image
72+
width={width}
73+
height={height}
74+
className={className}
75+
data-fallback={fallback}
76+
{...rest}
77+
/>
78+
</div>
79+
)
80+
}
81+
82+
export default ImageLoader
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@use '../../scss/config.scss' as *;
2+
3+
.loader {
4+
@include position(relative);
5+
6+
div + img {
7+
@include visibility(0);
8+
}
9+
10+
img {
11+
@include position(absolute, t0, l0);
12+
}
13+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { ImageProps } from '../Image/image'
2+
import type { SkeletonProps } from '../Skeleton/skeleton'
3+
4+
export type ImageLoaderProps = {
5+
fallback?: string
6+
} & ImageProps & Omit<SkeletonProps, 'width' | 'height'>

src/pages/components/image.astro

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Layout from '@static/Layout.astro'
55
import AstroImage from '@components/Image/Image.astro'
66
import SvelteImage from '@components/Image/Image.svelte'
77
import ReactImage from '@components/Image/Image.tsx'
8+
import ImageLoader from '@components/ImageLoader/ImageLoader.astro'
89
910
import { getSections } from '@helpers'
1011
@@ -17,7 +18,8 @@ const commonProps = {
1718
src: '/img/placeholder5.png',
1819
alt: 'Placeholder image',
1920
width: 250,
20-
height: 250
21+
height: 250,
22+
lazy: true
2123
}
2224
---
2325

@@ -98,6 +100,18 @@ const commonProps = {
98100
<ComponentWrapper title="Centered">
99101
<section.component {...commonProps} center={true} />
100102
</ComponentWrapper>
103+
104+
<ComponentWrapper title="Using image loader">
105+
<ImageLoader {...commonProps} />
106+
</ComponentWrapper>
107+
108+
<ComponentWrapper title="Using fallback image on error">
109+
<ImageLoader
110+
{...commonProps}
111+
src="/img/invalid-placeholder.png"
112+
fallback="/img/placeholder5.png"
113+
/>
114+
</ComponentWrapper>
101115
</div>
102116
))}
103117
</Layout>

src/playground/ReactPlayground.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import Copy from '@components/Copy/Copy.tsx'
1414
import Counter from '@components/Counter/Counter.tsx'
1515
import DataTable from '@components/DataTable/DataTable.tsx'
1616
import Icon from '@components/Icon/Icon.tsx'
17+
import ImageLoader from '@components/ImageLoader/ImageLoader.tsx'
1718
import Input from '@components/Input/Input.tsx'
1819
import List from '@components/List/List.tsx'
1920
import OTPInput from '@components/OTPInput/OTPInput.tsx'
@@ -201,6 +202,16 @@ const ReactPlayground = () => {
201202
/>
202203
</Card>
203204

205+
<Card title="Image Loader">
206+
<ImageLoader
207+
src="/img/placeholder5.png"
208+
alt="Placeholder image"
209+
width={250}
210+
height={250}
211+
lazy={true}
212+
/>
213+
</Card>
214+
204215
<Card title="Input">
205216
<Input
206217
label="Enter a value"

0 commit comments

Comments
 (0)