Skip to content

Commit a2f40b5

Browse files
committed
vue - update reactivity issues with cloning nested options
1 parent 7b5e8bf commit a2f40b5

3 files changed

Lines changed: 65 additions & 3 deletions

File tree

src/slim-select/vue/helpers.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { isReactive, isRef, toRaw, unref } from 'vue'
2+
3+
/** Recursively strip Vue reactivity so structuredClone won't throw on Proxy objects. */
4+
export function deepToRaw<T>(input: T): T {
5+
const seen = new WeakMap<object, any>()
6+
const convert = (val: any): any => {
7+
if (isRef(val)) val = unref(val)
8+
if (isReactive(val)) val = toRaw(val)
9+
if (val === null || typeof val !== 'object') return val
10+
if (seen.has(val)) return seen.get(val)
11+
if (Array.isArray(val)) {
12+
const arr: any[] = []
13+
seen.set(val, arr)
14+
for (const item of val) arr.push(convert(item))
15+
return arr
16+
}
17+
const obj: Record<string, any> = {}
18+
seen.set(val, obj)
19+
for (const key of Object.keys(val)) obj[key] = convert(val[key])
20+
return obj
21+
}
22+
return convert(input)
23+
}

src/slim-select/vue/vue.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest'
22
import { mount, VueWrapper } from '@vue/test-utils'
3-
import { nextTick, PropType } from 'vue'
3+
import { nextTick, PropType, reactive } from 'vue'
44
import SlimSelect, { Option, Events } from '@/slim-select'
55
import SlimSelectVue from './vue.vue'
66

@@ -556,6 +556,44 @@ describe('SlimSelect Vue Component', () => {
556556
})
557557

558558
describe('Reactivity Edge Cases', () => {
559+
test('mounts when data prop contains nested reactive option objects', async () => {
560+
const state = reactive({
561+
items: [{ value: 'MLD', text: 'MLD' }]
562+
})
563+
564+
const TestComponent = {
565+
components: { SlimSelectVue },
566+
template: `
567+
<SlimSelectVue
568+
v-model="selected"
569+
:data="options"
570+
/>
571+
`,
572+
data() {
573+
return {
574+
selected: 'MLD' as string
575+
}
576+
},
577+
computed: {
578+
options() {
579+
return [
580+
{ text: 'Select unit', value: '', placeholder: true },
581+
...state.items
582+
]
583+
}
584+
}
585+
}
586+
587+
const testWrapper = mount(TestComponent)
588+
await nextTick()
589+
590+
const slimComponent = testWrapper.findComponent(SlimSelectVue)
591+
const slim = (slimComponent.vm as any).slim as SlimSelect
592+
expect(slim).toBeInstanceOf(SlimSelect)
593+
expect(slim.getSelected()).toEqual(['MLD'])
594+
testWrapper.unmount()
595+
})
596+
559597
test('reactive data with v-model syncs without selected on options', async () => {
560598
const TestComponent = {
561599
components: { SlimSelectVue },

src/slim-select/vue/vue.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import SlimSelect, {
1717
Settings
1818
} from '../index'
1919
import { dataStructureEqual } from '../helpers'
20+
import { deepToRaw } from './helpers'
2021
2122
export default defineComponent({
2223
name: 'SlimSelect',
@@ -75,7 +76,7 @@ export default defineComponent({
7576
}
7677
7778
this.slim = new SlimSelect(config)
78-
this.lastAppliedData = structuredClone(toRaw(this.data))
79+
this.lastAppliedData = structuredClone(deepToRaw(this.data))
7980
// Push initial modelValue down without firing afterChange (no parent emit loop)
8081
this.syncModelValueToSlimSelect(false)
8182
},
@@ -104,7 +105,7 @@ export default defineComponent({
104105
return
105106
}
106107
this.slim.setData(newData)
107-
this.lastAppliedData = structuredClone(toRaw(newData))
108+
this.lastAppliedData = structuredClone(deepToRaw(newData))
108109
this.syncModelValueToSlimSelect(false)
109110
},
110111
deep: true

0 commit comments

Comments
 (0)