A small collection of reusable, event driven web components. Currently includes an Intersection Observer component, and a Get Request component.
# Clone the repository
git clone https://github.com/selmetwa/web-component-utils.git
cd web-component-utils
# Install dependencies for examples
cd vanilla-example && npm install && cd ..
cd react-example && npm install && cd ..
cd vue-example && npm install && cd ..
# Run an example
npm run dev:vanilla
npm run dev:react
npm run dev:vueA web component that detects when content becomes visible in the viewport. Perfect for lazy loading, animations, analytics, and more.
Features:
- β Framework-agnostic (works with React, Vue, Angular, Svelte, vanilla JS)
- β Event-based API
- β
Custom event names via
event-keyandready-event-keyattributes - β Configurable threshold and root margin
- β Shadow DOM with slot support
- β Fully documented with JSDoc
Basic Usage:
<intersection-observer
threshold="0.5"
event-key="my-visibility-event"
ready-event-key="my-ready-event">
<div>Your content here</div>
</intersection-observer>
<script type="module">
import './components/intersection-observer/index.js'
const observer = document.querySelector('intersection-observer')
// Listen for visibility changes
observer.addEventListener('my-visibility-event', (e) => {
console.log('Visible:', e.detail.isIntersecting)
console.log('Ratio:', e.detail.intersectionRatio)
})
// Listen for ready event
observer.addEventListener('my-ready-event', () => {
console.log('Observer is ready!')
})
</script>A web component that handles GET requests with built-in caching, loading states, and error handling. Perfect for fetching data from APIs without managing fetch logic yourself.
Features:
- β Framework-agnostic (works with React, Vue, Angular, Svelte, vanilla JS)
- β Built-in LRU cache with configurable size
- β Automatic fetching when URL changes
- β Custom event names for loading, success, and error states
- β Shadow DOM with slot support
- β Manual and automatic fetch modes
- β Fully documented with JSDoc
Basic Usage:
<get-request
url="https://api.example.com/data"
loading-event-key="my-loading"
success-event-key="my-success"
error-event-key="my-error">
<div>Your content here</div>
</get-request>
<script type="module">
import './components/get-request/index.js'
const request = document.querySelector('get-request')
// Listen for loading state
request.addEventListener('my-loading', (e) => {
console.log('Loading:', e.detail.url)
})
// Listen for success
request.addEventListener('my-success', (e) => {
console.log('Data:', e.detail.data)
console.log('From cache:', e.detail.fromCache)
})
// Listen for errors
request.addEventListener('my-error', (e) => {
console.error('Error:', e.detail.error)
})
</script>Attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
threshold |
number | 0.1 |
Percentage of visibility required to trigger (0-1) |
root-margin |
string | '0px' |
Margin around the root element |
event-key |
string | 'intersection-change' |
Custom event name for visibility changes |
ready-event-key |
string | 'observer-ready' |
Custom event name for ready state |
Events:
-
{event-key}(default:intersection-change) - Fired when intersection state changes{ isIntersecting: boolean, // Whether element is intersecting intersectionRatio: number, // Ratio of element visible (0-1) entry: IntersectionObserverEntry // Full entry object }
-
{ready-event-key}(default:observer-ready) - Fired when component is fully initialized
Properties (Getters):
isIntersecting- Get current intersection state (boolean)intersectionRatio- Get current intersection ratio (number)
Methods:
startObserving()- Start observing the targetstopObserving()- Stop observing the target
Attributes:
| Attribute | Type | Default | Description |
|---|---|---|---|
url |
string | '' |
The URL to fetch from |
cache-size |
number | 50 |
Maximum number of cached responses |
enable-cache |
boolean | true |
Whether to enable caching |
auto-fetch |
boolean | true |
Whether to automatically fetch when URL changes |
loading-event-key |
string | 'request-loading' |
Custom event name for loading state |
success-event-key |
string | 'request-success' |
Custom event name for success state |
error-event-key |
string | 'request-error' |
Custom event name for error state |
Events:
-
{loading-event-key}(default:request-loading) - Fired when request starts{ url: string // The URL being fetched }
-
{success-event-key}(default:request-success) - Fired when request succeeds{ data: any, // The response data fromCache: boolean // Whether data came from cache }
-
{error-event-key}(default:request-error) - Fired when request fails{ error: Error, // The error object message: string // Error message }
Properties (Getters):
data- Get the current data (any)loading- Get loading state (boolean)error- Get error state (Error | null)
Methods:
fetch()- Manually trigger a fetch requestclearCache()- Clear the request cache
Intersection Observer:
import './components/intersection-observer/index.js'
const observer = document.getElementById('observer')
observer.addEventListener('visibility-change', (event) => {
const { isIntersecting, intersectionRatio } = event.detail
// Update your UI
})
observer.addEventListener('observer-ready', () => {
console.log('Ready!')
})Get Request:
import './components/get-request/index.js'
const request = document.getElementById('request')
request.addEventListener('request-success', (event) => {
const { data, fromCache } = event.detail
console.log('Data:', data)
})
request.addEventListener('request-error', (event) => {
console.error('Error:', event.detail.error)
})Intersection Observer:
import { useState, useEffect, useRef } from 'react'
import './components/intersection-observer/index.js'
function MyComponent() {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef(null)
useEffect(() => {
const handleChange = (e) => setIsVisible(e.detail.isIntersecting)
const handleReady = () => console.log('Ready!')
ref.current?.addEventListener('visibility-change', handleChange)
ref.current?.addEventListener('observer-ready', handleReady)
return () => {
ref.current?.removeEventListener('visibility-change', handleChange)
ref.current?.removeEventListener('observer-ready', handleReady)
}
}, [])
return (
<intersection-observer
ref={ref}
threshold="0.5"
event-key="visibility-change"
ready-event-key="observer-ready">
<MyContent isVisible={isVisible} />
</intersection-observer>
)
}Get Request:
import { useState, useEffect, useRef } from 'react'
import './components/get-request/index.js'
function MyComponent() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const ref = useRef(null)
useEffect(() => {
const handleLoading = () => setLoading(true)
const handleSuccess = (e) => {
setData(e.detail.data)
setLoading(false)
}
ref.current?.addEventListener('request-loading', handleLoading)
ref.current?.addEventListener('request-success', handleSuccess)
return () => {
ref.current?.removeEventListener('request-loading', handleLoading)
ref.current?.removeEventListener('request-success', handleSuccess)
}
}, [])
return (
<get-request
ref={ref}
url="https://api.example.com/data"
loading-event-key="request-loading"
success-event-key="request-success">
{loading ? <div>Loading...</div> : <pre>{JSON.stringify(data, null, 2)}</pre>}
</get-request>
)
}Intersection Observer:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import './components/intersection-observer/index.js'
const isVisible = ref(false)
const observerRef = ref(null)
onMounted(() => {
const handleChange = (e) => {
isVisible.value = e.detail.isIntersecting
}
const handleReady = () => console.log('Ready!')
observerRef.value?.addEventListener('visibility-change', handleChange)
observerRef.value?.addEventListener('observer-ready', handleReady)
onUnmounted(() => {
observerRef.value?.removeEventListener('visibility-change', handleChange)
observerRef.value?.removeEventListener('observer-ready', handleReady)
})
})
</script>
<template>
<intersection-observer
ref="observerRef"
threshold="0.5"
event-key="visibility-change"
ready-event-key="observer-ready">
<MyComponent :is-visible="isVisible" />
</intersection-observer>
</template>Get Request:
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import './components/get-request/index.js'
const data = ref(null)
const loading = ref(false)
const requestRef = ref(null)
onMounted(() => {
const handleLoading = () => {
loading.value = true
}
const handleSuccess = (e) => {
data.value = e.detail.data
loading.value = false
}
requestRef.value?.addEventListener('request-loading', handleLoading)
requestRef.value?.addEventListener('request-success', handleSuccess)
onUnmounted(() => {
requestRef.value?.removeEventListener('request-loading', handleLoading)
requestRef.value?.removeEventListener('request-success', handleSuccess)
})
})
</script>
<template>
<get-request
ref="requestRef"
url="https://api.example.com/data"
loading-event-key="request-loading"
success-event-key="request-success">
<div v-if="loading">Loading...</div>
<pre v-else>{{ JSON.stringify(data, null, 2) }}</pre>
</get-request>
</template>web-component-utils/
βββ components/
β βββ intersection-observer/
β β βββ index.js # Intersection Observer component
β βββ get-request/
β βββ index.js # Get Request component
βββ vanilla-example/ # Vanilla JS examples
β βββ src/
β βββ main.js # Router
β βββ pages/
β βββ intersection-observer.js
β βββ get-request.js
βββ react-example/ # React examples
β βββ src/
β βββ App.jsx # Router
β βββ pages/
β βββ IntersectionObserverPage.jsx
β βββ GetRequestPage.jsx
βββ vue-example/ # Vue examples
β βββ src/
β βββ App.vue # Main layout
β βββ router/
β β βββ index.js
β βββ pages/
β βββ IntersectionObserverPage.vue
β βββ GetRequestPage.vue
βββ .github/
β βββ workflows/
β βββ deploy.yml # GitHub Pages deployment
βββ SETUP.md # Installation instructions
βββ package.json # Root scripts