Skip to content

Commit

Permalink
Merge pull request #456 from ismail9k/generic-enhancements
Browse files Browse the repository at this point in the history
Enhance carousel performance and slide management
  • Loading branch information
ismail9k authored Dec 20, 2024
2 parents 64072d8 + 4c99163 commit 4a15258
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 75 deletions.
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pnpm lint
pnpm test
pnpm build
15 changes: 15 additions & 0 deletions docs/api/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,22 @@ Triggered while the carousel is being dragged, providing live positional data. E
- `x`: The horizontal drag position.
- `y`: The vertical drag position.

### @slide-registered

Triggered when a new slide is registered with the carousel. Emits the following data:

- `slide`: The Vue component instance of the registered slide
- `index`: The index position where the slide was registered

### @slide-unregistered

Triggered when a slide is unregistered (removed) from the carousel. Emits the following data:

- `slide`: The Vue component instance of the unregistered slide
- `index`: The index position from which the slide was removed

## Notes

- Events are reactive and can be used to trigger animations, analytics, or other custom behaviors.
- Registration events (`slide-registered`, `slide-unregistered`) are particularly useful for tracking slide lifecycle and managing external state.
- Ensure your event handlers account for edge cases, such as looping or rapid navigation.
147 changes: 138 additions & 9 deletions playground/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,25 @@ import { DIR_MAP, SNAP_ALIGN_OPTIONS, BREAKPOINT_MODE_OPTIONS } from '@/shared/c
const carouselWrapper = ref<HTMLDivElement | null>(null)
const numItems = 10
const breakpoints = reactive({
100: { itemsToShow: 1 },
200: { itemsToShow: 2 },
400: { itemsToShow: 3 },
600: { itemsToShow: 4 },
})
const defaultSlides = [
{ id: 1, title: 'Slide 1', description: 'First slide description' },
{ id: 2, title: 'Slide 2', description: 'Second slide description' },
{ id: 3, title: 'Slide 3', description: 'Third slide description' },
{ id: 4, title: 'Slide 4', description: 'Fourth slide description' },
{ id: 5, title: 'Slide 5', description: 'Fifth slide description' },
]
const defaultConfig = {
currentSlide: 0,
snapAlign: 'center',
itemsToScroll: 1,
itemsToShow: 1,
itemsToShow: 2,
autoplay: null,
wrapAround: true,
height: '200',
Expand All @@ -35,6 +42,7 @@ const defaultConfig = {
}
const config = reactive({ ...defaultConfig })
const items = reactive([...defaultSlides])
const getConfigValue = (path: string) => config[path]
Expand Down Expand Up @@ -128,11 +136,76 @@ const handleReset = () => {
// Reset config values
Object.entries(defaultConfig).forEach(([key, value]) => setConfigValue(key, value))
// Reset items
items.splice(0, items.length, ...defaultSlides)
}
const handelButtonClick = () => {
alert('Button clicked')
}
const getRandomInt = (min: number, max: number): number => {
min = Math.ceil(min)
max = Math.floor(max)
return Math.floor(Math.random() * (max - min)) + min
}
const handleAddingASlide = () => {
const newId = items.length + 1
const randomPosition = getRandomInt(0, newId)
const newSlide = {
id: newId,
title: `Dynamic Slide ${newId}`,
description: `Dynamically inserted at index ${randomPosition}`,
color: '#2fa265',
}
items.splice(randomPosition, 0, newSlide)
}
const handleRemovingASlide = () => {
if (items.length > 1) {
const randomPosition = getRandomInt(0, items.length)
const removedSlide = items[randomPosition]
items.splice(randomPosition, 1)
if (config.currentSlide >= items.length) {
config.currentSlide = items.length - 1
}
}
}
const events = [
'before-init',
'init',
'slide-start',
'slide-end',
'loop',
'drag',
'slide-registered',
'slide-unregistered',
]
const lastEvent = ref('')
const lastEventData = ref<any>(null)
const getSerializableData = (data: any): any => {
if (!data) return null
// For slide-registered and slide-unregistered events, only return relevant info
if (data.slide) {
return {
...data,
slide: '[Complex Object]',
}
}
return data
}
const handleEvent = (eventName: string) => (data?: any) => {
lastEvent.value = eventName
lastEventData.value = getSerializableData(data)
console.log(`Event: ${eventName}`, data)
}
</script>

<template>
Expand All @@ -143,10 +216,17 @@ const handelButtonClick = () => {
v-model="config.currentSlide"
v-bind="config"
:breakpoints="config.useBreakpoints ? breakpoints : null"
v-on="Object.fromEntries(events.map((e) => [e, handleEvent(e)]))"
>
<CarouselSlide v-for="i in numItems" :key="i" v-slot="{ isActive, isClone }">
<div class="carousel-item">
{{ i }}<button @click="handelButtonClick">This is a button</button>
<CarouselSlide v-for="(item, index) in items" :key="item.id" :index="index">
<div
class="carousel-item"
:key="item.id"
:style="{ backgroundColor: `${item.color}` }"
>
<h3>{{ item.title }}</h3>
<p>{{ item.description }}</p>
<button @click="handelButtonClick">This is a button</button>
</div>
</CarouselSlide>
<template #addons>
Expand All @@ -155,6 +235,10 @@ const handelButtonClick = () => {
</template>
</VueCarousel>
</div>
<div v-if="lastEvent" class="event-debug">
Last Event: {{ lastEvent }}
<pre v-if="lastEventData">{{ JSON.stringify(lastEventData, null, 2) }}</pre>
</div>
</main>

<aside class="config-panel">
Expand Down Expand Up @@ -189,7 +273,15 @@ const handelButtonClick = () => {
/>
</label>
</div>
<button @click="handleReset" class="reset-button">Reset Config</button>
<div class="config-panel-buttons-row">
<button @click="handleAddingASlide" class="config-panel-button">
Add a new slide
</button>
<button @click="handleRemovingASlide" class="config-panel-button">
Remove a new slide
</button>
<button @click="handleReset" class="config-panel-button">Reset</button>
</div>
</aside>
</div>
</template>
Expand Down Expand Up @@ -284,7 +376,7 @@ select {
width: auto;
}
.config-panel button {
.config-panel-button {
margin-top: 10px;
padding: 8px 16px;
border-radius: 4px;
Expand All @@ -295,8 +387,8 @@ select {
transition: opacity 0.2s;
}
.config-panel button:hover {
background: rgba(200, 0, 0, 0.9);
.config-panel-button:hover {
background: var(--brand-color);
}
@keyframes pop-in {
Expand Down Expand Up @@ -340,4 +432,41 @@ select {
justify-content: center;
align-items: center;
}
.carousel-item h3 {
margin: 0 0 10px;
}
.carousel-item p {
margin: 0 0 15px;
font-size: 16px;
text-align: center;
padding: 0 20px;
}
.event-debug {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
padding: 10px;
border-radius: 4px;
font-size: 0.9em;
max-width: 300px;
overflow: auto;
}
.event-debug pre {
margin: 5px 0 0;
font-size: 0.8em;
white-space: pre-wrap;
}
.carousel__slide--active .carousel-item {
border: 2px solid red;
}
.config-panel-buttons-row {
display: flex;
flex-direction: column;
}
</style>
37 changes: 11 additions & 26 deletions src/components/Carousel/Carousel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
SetupContext,
Ref,
ComputedRef,
ComponentInternalInstance,
watchEffect,
shallowReactive,
} from 'vue'
Expand All @@ -25,6 +24,7 @@ import {
NonNormalizedDir,
NormalizedDir,
injectCarousel,
createSlideRegistry,
} from '@/shared'
import {
except,
Expand Down Expand Up @@ -57,13 +57,17 @@ export const Carousel = defineComponent({
'update:modelValue',
'slide-end',
'before-init',
'slide-registered',
'slide-unregistered',
],
setup(props: CarouselConfig, { slots, emit, expose }: SetupContext) {
const slideRegistry = createSlideRegistry(emit)
const slides = slideRegistry.getSlides()
const slidesCount = computed(() => slides.length)

const root: Ref<Element | null> = ref(null)
const viewport: Ref<Element | null> = ref(null)
const slides = shallowReactive<Array<ComponentInternalInstance>>([])
const slideSize: Ref<number> = ref(0)
const slidesCount = computed(() => slides.length)

const fallbackConfig = computed(() => ({
...DEFAULT_CONFIG,
Expand All @@ -73,7 +77,7 @@ export const Carousel = defineComponent({
}))

// current active config
const config = reactive<CarouselConfig>({ ...fallbackConfig.value })
const config = shallowReactive<CarouselConfig>({ ...fallbackConfig.value })

// slides
const currentSlideIndex = ref(props.modelValue ?? 0)
Expand All @@ -97,23 +101,6 @@ export const Carousel = defineComponent({
return dir in DIR_MAP ? DIR_MAP[dir as NonNormalizedDir] : (dir as NormalizedDir)
})

const indexCbs: Array<(index: number) => void> = []
const registerSlide: InjectedCarousel['registerSlide'] = (slide, indexCb) => {
indexCb(slides.length)
slides.push(slide)
indexCbs.push(indexCb)
}

const unregisterSlide: InjectedCarousel['unregisterSlide'] = (slide) => {
const found = slides.indexOf(slide)
if (found >= 0) {
slides.splice(found, 1)
indexCbs.splice(found, 1)
// Update indexes after the one that was removed
indexCbs.slice(found).forEach((cb, index) => cb(found + index))
}
}

const isReversed = computed(() => ['rtl', 'btt'].includes(normalizedDir.value))
const isVertical = computed(() => ['ttb', 'btt'].includes(normalizedDir.value))

Expand Down Expand Up @@ -284,9 +271,8 @@ export const Carousel = defineComponent({

onBeforeUnmount(() => {
mounted.value = false
// Empty the slides before they unregister for better performance
slides.splice(0, slides.length)
indexCbs.splice(0, indexCbs.length)

slideRegistry.cleanup()

if (transitionTimer) {
clearTimeout(transitionTimer)
Expand Down Expand Up @@ -576,8 +562,7 @@ export const Carousel = defineComponent({
normalizedDir,
nav,
isSliding,
registerSlide,
unregisterSlide,
slideRegistry,
})

provide(injectCarousel, provided)
Expand Down
8 changes: 2 additions & 6 deletions src/components/Carousel/Carousel.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ShallowReactive,
} from 'vue'

import { CarouselConfig, NormalizedDir } from '@/shared/types'
import { SlideRegistry, CarouselConfig, NormalizedDir } from '@/shared'

export interface CarouselNav {
slideTo: (index: number) => void
Expand All @@ -29,11 +29,7 @@ export type InjectedCarousel = Reactive<{
normalizedDir: ComputedRef<NormalizedDir>
nav: CarouselNav
isSliding: Ref<boolean>
registerSlide: (
slide: ComponentInternalInstance,
indexCb: (idx: number) => void
) => void
unregisterSlide: (slide: ComponentInternalInstance) => void
slideRegistry: SlideRegistry
}>

export interface CarouselData {
Expand Down
Loading

0 comments on commit 4a15258

Please sign in to comment.