Skip to content

Commit 17ea288

Browse files
Mini-ghostilyaliao
andauthored
feat(useSortable): add watchElement option for auto-reinitialize on element change (#5189)
Co-authored-by: IlyaL <[email protected]>
1 parent e8be8f8 commit 17ea288

File tree

2 files changed

+194
-11
lines changed

2 files changed

+194
-11
lines changed

packages/integrations/useSortable/index.browser.test.ts

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { defineComponent, shallowRef, useTemplateRef } from 'vue'
66
import { useSortable } from './index'
77

88
describe('useSortable', () => {
9-
it('should initialise Sortable', () => {
9+
it('should initialize Sortable', () => {
1010
const wrapper = mount(defineComponent({
1111
template: '<div ref="el"></div>',
1212
setup() {
@@ -179,4 +179,141 @@ describe('useSortable', () => {
179179
wrapper.unmount()
180180
}
181181
})
182+
183+
describe('watchElement', () => {
184+
it('should auto-reinitialize when element changes with watchElement: true', async () => {
185+
const wrapper = mount(defineComponent({
186+
template: '<div v-if="show" ref="el"></div>',
187+
setup() {
188+
const el = useTemplateRef<HTMLElement>('el')
189+
const show = shallowRef(true)
190+
const list = shallowRef<string[]>([])
191+
const result = useSortable(el, list, {
192+
watchElement: true,
193+
})
194+
195+
return { ...result, el, show }
196+
},
197+
}))
198+
const vm = wrapper.vm
199+
try {
200+
await wrapper.vm.$nextTick()
201+
202+
expect(vm.el).toBeDefined()
203+
let sortable = Sortable.get(vm.el!)
204+
expect(sortable).toBeDefined()
205+
206+
vm.show = false
207+
await wrapper.vm.$nextTick()
208+
expect(vm.el).toBeNull()
209+
210+
vm.show = true
211+
await wrapper.vm.$nextTick()
212+
await wrapper.vm.$nextTick()
213+
expect(vm.el).toBeDefined()
214+
215+
// Should be automatically reinitialized
216+
sortable = Sortable.get(vm.el!)
217+
expect(sortable).toBeDefined()
218+
}
219+
finally {
220+
wrapper.unmount()
221+
}
222+
})
223+
224+
it('should NOT auto-reinitialize when element changes with watchElement: false', async () => {
225+
const wrapper = mount(defineComponent({
226+
template: '<div v-if="show" ref="el"></div>',
227+
setup() {
228+
const el = useTemplateRef<HTMLElement>('el')
229+
const show = shallowRef(true)
230+
const list = shallowRef<string[]>([])
231+
const result = useSortable(el, list, {
232+
watchElement: false,
233+
})
234+
235+
return { ...result, el, show }
236+
},
237+
}))
238+
const vm = wrapper.vm
239+
try {
240+
expect(vm.el).toBeDefined()
241+
const firstElement = vm.el!
242+
let sortable = Sortable.get(firstElement)
243+
expect(sortable).toBeDefined()
244+
const firstInstance = sortable
245+
246+
vm.show = false
247+
await wrapper.vm.$nextTick()
248+
expect(vm.el).toBeNull()
249+
250+
vm.show = true
251+
await wrapper.vm.$nextTick()
252+
expect(vm.el).toBeDefined()
253+
const secondElement = vm.el!
254+
255+
expect(secondElement).not.toBe(firstElement)
256+
257+
// New element should not have Sortable
258+
sortable = Sortable.get(secondElement)
259+
expect(sortable).toBeFalsy()
260+
261+
// Old instance still bound to removed element
262+
sortable = Sortable.get(firstElement)
263+
expect(sortable).toBe(firstInstance)
264+
265+
// Manual cleanup and reinitialize required
266+
vm.stop()
267+
vm.start()
268+
sortable = Sortable.get(secondElement)
269+
expect(sortable).toBeDefined()
270+
}
271+
finally {
272+
wrapper.unmount()
273+
}
274+
})
275+
276+
it('should work with conditional rendering using watchElement: true', async () => {
277+
const wrapper = mount(defineComponent({
278+
template: `
279+
<div>
280+
<div v-if="condition === 'a'" ref="el" data-test="a"></div>
281+
<div v-if="condition === 'b'" ref="el" data-test="b"></div>
282+
</div>
283+
`,
284+
setup() {
285+
const el = useTemplateRef<HTMLElement>('el')
286+
const condition = shallowRef<'a' | 'b'>('a')
287+
const list = shallowRef<string[]>([])
288+
const result = useSortable(el, list, {
289+
watchElement: true,
290+
})
291+
292+
return { ...result, el, condition }
293+
},
294+
}))
295+
const vm = wrapper.vm
296+
try {
297+
await wrapper.vm.$nextTick()
298+
299+
expect(vm.el?.getAttribute('data-test')).toBe('a')
300+
let sortable = Sortable.get(vm.el!)
301+
expect(sortable).toBeDefined()
302+
const firstInstance = sortable
303+
304+
vm.condition = 'b'
305+
await wrapper.vm.$nextTick()
306+
await wrapper.vm.$nextTick()
307+
expect(vm.el?.getAttribute('data-test')).toBe('b')
308+
309+
// Should be a new instance
310+
sortable = Sortable.get(vm.el!)
311+
expect(sortable).toBeDefined()
312+
expect(sortable).not.toBe(firstInstance)
313+
}
314+
finally {
315+
wrapper.unmount()
316+
}
317+
})
318+
})
182319
})

packages/integrations/useSortable/index.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Options } from 'sortablejs'
33
import type { MaybeRef, MaybeRefOrGetter } from 'vue'
44
import { defaultDocument, tryOnMounted, tryOnScopeDispose, unrefElement } from '@vueuse/core'
55
import Sortable from 'sortablejs'
6-
import { isRef, nextTick, toValue } from 'vue'
6+
import { computed, isRef, nextTick, toValue, watch } from 'vue'
77

88
export interface UseSortableReturn {
99
/**
@@ -23,7 +23,21 @@ export interface UseSortableReturn {
2323
option: (<K extends keyof Sortable.Options>(name: K, value: Sortable.Options[K]) => void) & (<K extends keyof Sortable.Options>(name: K) => Sortable.Options[K])
2424
}
2525

26-
export type UseSortableOptions = Options & ConfigurableDocument
26+
export interface UseSortableOptions extends Options, ConfigurableDocument {
27+
/**
28+
* Watch the element reference for changes and automatically reinitialize Sortable
29+
* when the element changes.
30+
*
31+
* When `false` (default), Sortable is only initialized once on mount.
32+
* You must manually call `start()` if the element reference changes.
33+
*
34+
* When `true`, automatically watches the element reference and reinitializes
35+
* Sortable whenever it changes (e.g., conditional rendering with v-if).
36+
*
37+
* @default false
38+
*/
39+
watchElement?: boolean
40+
}
2741

2842
export function useSortable<T>(selector: string, list: MaybeRef<T[]>,
2943
options?: UseSortableOptions): UseSortableReturn
@@ -43,24 +57,31 @@ export function useSortable<T>(
4357
): UseSortableReturn {
4458
let sortable: Sortable | undefined
4559

46-
const { document = defaultDocument, ...resetOptions } = options
60+
const { document = defaultDocument, watchElement = false, ...resetOptions } = options
4761

4862
const defaultOptions: Options = {
4963
onUpdate: (e) => {
5064
moveArrayElement(list, e.oldIndex!, e.newIndex!, e)
5165
},
5266
}
5367

54-
const start = () => {
55-
const target = (typeof el === 'string' ? document?.querySelector(el) : unrefElement(el))
68+
const element = computed(() => (typeof el === 'string' ? document?.querySelector(el) : unrefElement(el)))
69+
70+
const cleanup = () => {
71+
sortable?.destroy()
72+
sortable = undefined
73+
}
74+
75+
const initSortable = (target: Element) => {
5676
if (!target || sortable !== undefined)
5777
return
5878
sortable = new Sortable(target as HTMLElement, { ...defaultOptions, ...resetOptions })
5979
}
6080

61-
const stop = () => {
62-
sortable?.destroy()
63-
sortable = undefined
81+
const start = () => {
82+
const target = element.value
83+
if (target)
84+
initSortable(target)
6485
}
6586

6687
const option = <K extends keyof Options>(name: K, value?: Options[K]) => {
@@ -70,9 +91,34 @@ export function useSortable<T>(
7091
return sortable?.option(name)
7192
}
7293

73-
tryOnMounted(start)
94+
// Conditionally set up element watching
95+
let stopWatch: (() => void) | undefined
96+
97+
if (watchElement && typeof el !== 'string') {
98+
// New behavior: watch element changes and auto-reinitialize
99+
stopWatch = watch(
100+
element,
101+
(newElement) => {
102+
cleanup()
103+
if (newElement)
104+
initSortable(newElement)
105+
},
106+
{ immediate: true, flush: 'post' },
107+
)
108+
}
109+
else {
110+
// Default behavior: initialize once on mount
111+
tryOnMounted(start)
112+
}
113+
114+
const stop = () => {
115+
cleanup()
116+
}
74117

75-
tryOnScopeDispose(stop)
118+
tryOnScopeDispose(() => {
119+
stopWatch?.()
120+
cleanup()
121+
})
76122

77123
return {
78124
stop,

0 commit comments

Comments
 (0)