slim-select 3.4.3 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/classes.d.ts +46 -0
  2. package/dist/helpers.d.ts +5 -0
  3. package/dist/index.d.ts +53 -319
  4. package/dist/index.umd.d.ts +2 -0
  5. package/dist/react/index.d.ts +4 -319
  6. package/dist/react/index.js +1459 -1697
  7. package/dist/react/index.umd.js +2 -2
  8. package/dist/react/react.d.ts +17 -0
  9. package/dist/react/react.test.d.ts +1 -0
  10. package/dist/react/slim-select/react/index.d.ts +4 -0
  11. package/dist/render.d.ts +88 -0
  12. package/dist/select.d.ts +38 -0
  13. package/dist/settings.d.ts +39 -0
  14. package/dist/slimselect.cjs.js +2 -2
  15. package/dist/slimselect.cjs.js.map +1 -1
  16. package/dist/slimselect.css +1 -1
  17. package/dist/slimselect.css.map +1 -1
  18. package/dist/slimselect.es.js +1233 -1385
  19. package/dist/slimselect.es.js.map +1 -1
  20. package/dist/slimselect.iife.js +2 -2
  21. package/dist/slimselect.iife.js.map +1 -1
  22. package/dist/slimselect.js +2 -2
  23. package/dist/slimselect.js.map +1 -1
  24. package/dist/src/docs/main.d.ts +0 -0
  25. package/dist/src/docs/router.d.ts +2 -0
  26. package/dist/src/docs/store.d.ts +12 -0
  27. package/dist/store.d.ts +53 -0
  28. package/dist/vue/index.d.ts +4 -319
  29. package/dist/vue/index.js +1335 -1504
  30. package/dist/vue/index.umd.js +1 -1
  31. package/dist/vue/slim-select/vue/index.d.ts +4 -0
  32. package/package.json +30 -36
  33. package/src/slim-select/slimselect.scss +34 -5
  34. package/.nvmrc +0 -1
  35. package/.prettierignore +0 -1
  36. package/.prettierrc +0 -7
  37. package/CODE_OF_CONDUCT.md +0 -46
  38. package/CONTRIBUTING.md +0 -1
  39. package/cdn.html +0 -21
  40. package/docs.go +0 -12
  41. package/example.html +0 -25
  42. package/index.html +0 -76
  43. package/release.js +0 -191
  44. package/sitemap.js +0 -108
  45. package/src/slim-select/accessibility.test.ts +0 -782
  46. package/src/slim-select/classes.test.ts +0 -73
  47. package/src/slim-select/classes.ts +0 -117
  48. package/src/slim-select/helpers.test.ts +0 -162
  49. package/src/slim-select/helpers.ts +0 -61
  50. package/src/slim-select/index.test.ts +0 -1356
  51. package/src/slim-select/index.ts +0 -594
  52. package/src/slim-select/index.umd.ts +0 -8
  53. package/src/slim-select/react/index.tsx +0 -9
  54. package/src/slim-select/react/react.test.tsx +0 -498
  55. package/src/slim-select/react/react.tsx +0 -209
  56. package/src/slim-select/render.test.ts +0 -1927
  57. package/src/slim-select/render.ts +0 -1771
  58. package/src/slim-select/select.test.ts +0 -683
  59. package/src/slim-select/select.ts +0 -659
  60. package/src/slim-select/settings.test.ts +0 -285
  61. package/src/slim-select/settings.ts +0 -78
  62. package/src/slim-select/store.test.ts +0 -1007
  63. package/src/slim-select/store.ts +0 -433
  64. package/src/slim-select/vue/index.ts +0 -9
  65. package/src/slim-select/vue/vue.test.ts +0 -1091
  66. package/src/slim-select/vue/vue.vue +0 -224
  67. package/tsconfig.json +0 -26
  68. package/vite.config.docs.mts +0 -29
  69. package/vite.config.lib.es-cjs.ts +0 -40
  70. package/vite.config.lib.umd-iife.ts +0 -34
  71. package/vite.config.react.mts +0 -41
  72. package/vite.config.vue.mts +0 -40
  73. package/vitest.config.ts +0 -30
@@ -1,498 +0,0 @@
1
- import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest'
2
- import { render, cleanup, act, waitFor } from '@testing-library/react'
3
- import { useState, useEffect, useRef } from 'react'
4
- import SlimSelect, { Option } from '../index'
5
- import SlimSelectReact, { SlimSelectRef } from './react'
6
-
7
- describe('SlimSelect React Component', () => {
8
- let consoleInfoSpy: any
9
- let consoleWarnSpy: any
10
-
11
- beforeEach(() => {
12
- // Mock console methods
13
- consoleInfoSpy = vi.spyOn(console, 'info').mockImplementation(() => {})
14
- consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
15
- })
16
-
17
- afterEach(() => {
18
- cleanup()
19
- // Restore console methods
20
- consoleInfoSpy.mockRestore()
21
- consoleWarnSpy.mockRestore()
22
- })
23
-
24
- describe('Basic Rendering', () => {
25
- test('renders a select element', () => {
26
- const { container } = render(<SlimSelectReact />)
27
- const select = container.querySelector('select')
28
- expect(select).toBeTruthy()
29
- })
30
-
31
- test('renders with single select by default', () => {
32
- const { container } = render(<SlimSelectReact />)
33
- const select = container.querySelector('select')
34
- expect(select?.hasAttribute('multiple')).toBe(false)
35
- })
36
-
37
- test('renders with multiple attribute when multiple prop is true', () => {
38
- const { container } = render(<SlimSelectReact multiple />)
39
- const select = container.querySelector('select')
40
- expect(select?.hasAttribute('multiple')).toBe(true)
41
- })
42
- })
43
-
44
- describe('Value and onChange binding', () => {
45
- test('accepts initial value for single select', async () => {
46
- const TestComponent = () => {
47
- const [value, setValue] = useState('2')
48
- const ref = useRef<SlimSelectRef>(null)
49
-
50
- return (
51
- <SlimSelectReact
52
- ref={ref}
53
- value={value}
54
- onChange={(val) => setValue(val as string)}
55
- data={[
56
- { value: '1', text: 'Option 1' },
57
- { value: '2', text: 'Option 2' },
58
- { value: '3', text: 'Option 3' }
59
- ]}
60
- />
61
- )
62
- }
63
-
64
- const { container } = render(<TestComponent />)
65
-
66
- await waitFor(
67
- () => {
68
- const select = container.querySelector('select')
69
- const slimInstance = (select as any)?._slimSelect as SlimSelect
70
- if (slimInstance) {
71
- const selected = slimInstance.getSelected()
72
- expect(selected[0]).toBe('2')
73
- }
74
- },
75
- { timeout: 500 }
76
- )
77
- })
78
-
79
- test('accepts initial value for multiple select', async () => {
80
- const TestComponent = () => {
81
- const [value, setValue] = useState(['2', '3'])
82
- return (
83
- <SlimSelectReact
84
- value={value}
85
- onChange={(val) => setValue(val as string[])}
86
- multiple
87
- data={[
88
- { value: '1', text: 'Option 1' },
89
- { value: '2', text: 'Option 2' },
90
- { value: '3', text: 'Option 3' }
91
- ]}
92
- />
93
- )
94
- }
95
-
96
- const { container } = render(<TestComponent />)
97
-
98
- await waitFor(
99
- () => {
100
- const select = container.querySelector('select')
101
- const slimInstance = (select as any)?._slimSelect as SlimSelect
102
- if (slimInstance) {
103
- const selected = slimInstance.getSelected()
104
- expect(selected).toEqual(['2', '3'])
105
- }
106
- },
107
- { timeout: 500 }
108
- )
109
- })
110
-
111
- test('calls onChange when selection changes', async () => {
112
- const onChangeMock = vi.fn()
113
- const TestComponent = () => {
114
- return (
115
- <SlimSelectReact
116
- value="1"
117
- onChange={onChangeMock}
118
- data={[
119
- { value: '1', text: 'Option 1' },
120
- { value: '2', text: 'Option 2' }
121
- ]}
122
- />
123
- )
124
- }
125
-
126
- const { container } = render(<TestComponent />)
127
-
128
- await waitFor(
129
- async () => {
130
- const select = container.querySelector('select')
131
- const slimInstance = (select as any)?._slimSelect as SlimSelect
132
- if (slimInstance) {
133
- await act(async () => {
134
- slimInstance.setSelected('2')
135
- })
136
- await waitFor(
137
- () => {
138
- expect(onChangeMock).toHaveBeenCalledWith('2')
139
- },
140
- { timeout: 500 }
141
- )
142
- }
143
- },
144
- { timeout: 500 }
145
- )
146
- })
147
-
148
- test('updates selection when value prop changes', async () => {
149
- const TestComponent = ({ initialValue }: { initialValue: string }) => {
150
- const [value, setValue] = useState(initialValue)
151
-
152
- useEffect(() => {
153
- // Simulate parent updating value
154
- const timer = setTimeout(() => {
155
- act(() => {
156
- setValue('2')
157
- })
158
- }, 100)
159
- return () => clearTimeout(timer)
160
- }, [])
161
-
162
- return (
163
- <SlimSelectReact
164
- value={value}
165
- onChange={(val) => setValue(val as string)}
166
- data={[
167
- { value: '1', text: 'Option 1' },
168
- { value: '2', text: 'Option 2' }
169
- ]}
170
- />
171
- )
172
- }
173
-
174
- const { container } = render(<TestComponent initialValue="1" />)
175
-
176
- await waitFor(
177
- () => {
178
- const select = container.querySelector('select')
179
- const slimInstance = (select as any)?._slimSelect as SlimSelect
180
- if (slimInstance) {
181
- const selected = slimInstance.getSelected()
182
- expect(selected[0]).toBe('2')
183
- }
184
- },
185
- { timeout: 500 }
186
- )
187
- })
188
- })
189
-
190
- describe('Data Prop', () => {
191
- test('initializes with data prop', async () => {
192
- const testData = [
193
- { value: '1', text: 'Option 1' },
194
- { value: '2', text: 'Option 2' },
195
- { value: '3', text: 'Option 3' }
196
- ]
197
-
198
- const { container } = render(<SlimSelectReact data={testData} />)
199
- await new Promise((resolve) => setTimeout(resolve, 100))
200
-
201
- const select = container.querySelector('select')
202
- const slimInstance = (select as any)?._slimSelect as SlimSelect
203
- if (slimInstance) {
204
- const data = slimInstance.getData()
205
- expect(data).toHaveLength(3)
206
- }
207
- })
208
-
209
- test('updates data when data prop changes', async () => {
210
- const TestComponent = () => {
211
- const [data, setData] = useState([
212
- { value: '1', text: 'Option 1' },
213
- { value: '2', text: 'Option 2' }
214
- ])
215
-
216
- useEffect(() => {
217
- const timer = setTimeout(() => {
218
- act(() => {
219
- setData([
220
- { value: '3', text: 'Option 3' },
221
- { value: '4', text: 'Option 4' },
222
- { value: '5', text: 'Option 5' }
223
- ])
224
- })
225
- }, 100)
226
- return () => clearTimeout(timer)
227
- }, [])
228
-
229
- return <SlimSelectReact data={data} />
230
- }
231
-
232
- const { container } = render(<TestComponent />)
233
-
234
- await waitFor(
235
- () => {
236
- const select = container.querySelector('select')
237
- const slimInstance = (select as any)?._slimSelect as SlimSelect
238
- if (slimInstance) {
239
- const data = slimInstance.getData()
240
- expect(data).toHaveLength(3)
241
- }
242
- },
243
- { timeout: 500 }
244
- )
245
- })
246
- })
247
-
248
- describe('Settings Prop', () => {
249
- test('applies settings prop', async () => {
250
- const { container } = render(
251
- <SlimSelectReact
252
- data={[{ value: '1', text: 'Option 1' }]}
253
- settings={{
254
- showSearch: false,
255
- placeholderText: 'Custom Placeholder'
256
- }}
257
- />
258
- )
259
- await new Promise((resolve) => setTimeout(resolve, 100))
260
-
261
- const select = container.querySelector('select')
262
- const slimInstance = (select as any)?._slimSelect as SlimSelect
263
- if (slimInstance) {
264
- expect(slimInstance.settings.showSearch).toBe(false)
265
- expect(slimInstance.settings.placeholderText).toBe('Custom Placeholder')
266
- }
267
- })
268
- })
269
-
270
- describe('Events Prop', () => {
271
- test('calls afterChange event from events prop', async () => {
272
- const afterChangeMock = vi.fn()
273
-
274
- const TestComponent = () => {
275
- return (
276
- <SlimSelectReact
277
- value="1"
278
- data={[
279
- { value: '1', text: 'Option 1' },
280
- { value: '2', text: 'Option 2' }
281
- ]}
282
- events={{
283
- afterChange: afterChangeMock
284
- }}
285
- />
286
- )
287
- }
288
-
289
- const { container } = render(<TestComponent />)
290
- await new Promise((resolve) => setTimeout(resolve, 100))
291
-
292
- const select = container.querySelector('select')
293
- const slimInstance = (select as any)?._slimSelect as SlimSelect
294
- if (slimInstance) {
295
- slimInstance.setSelected('2')
296
- await new Promise((resolve) => setTimeout(resolve, 100))
297
- expect(afterChangeMock).toHaveBeenCalled()
298
- }
299
- })
300
-
301
- test('calls both custom afterChange and onChange', async () => {
302
- const afterChangeMock = vi.fn()
303
- const onChangeMock = vi.fn()
304
-
305
- const TestComponent = () => {
306
- return (
307
- <SlimSelectReact
308
- value="1"
309
- onChange={onChangeMock}
310
- data={[
311
- { value: '1', text: 'Option 1' },
312
- { value: '2', text: 'Option 2' }
313
- ]}
314
- events={{
315
- afterChange: afterChangeMock
316
- }}
317
- />
318
- )
319
- }
320
-
321
- const { container } = render(<TestComponent />)
322
- await new Promise((resolve) => setTimeout(resolve, 100))
323
-
324
- const select = container.querySelector('select')
325
- const slimInstance = (select as any)?._slimSelect as SlimSelect
326
- if (slimInstance) {
327
- slimInstance.setSelected('2')
328
- await new Promise((resolve) => setTimeout(resolve, 100))
329
- expect(afterChangeMock).toHaveBeenCalled()
330
- expect(onChangeMock).toHaveBeenCalledWith('2')
331
- }
332
- })
333
- })
334
-
335
- describe('Empty and Invalid Values', () => {
336
- test('handles empty string as value for single select', async () => {
337
- const TestComponent = () => {
338
- const [value, setValue] = useState('')
339
- return (
340
- <SlimSelectReact
341
- value={value}
342
- onChange={(val) => setValue(val as string)}
343
- data={[
344
- { value: '1', text: 'Option 1' },
345
- { value: '2', text: 'Option 2' }
346
- ]}
347
- />
348
- )
349
- }
350
-
351
- const { container } = render(<TestComponent />)
352
- await new Promise((resolve) => setTimeout(resolve, 100))
353
-
354
- const select = container.querySelector('select')
355
- const slimInstance = (select as any)?._slimSelect as SlimSelect
356
- if (slimInstance) {
357
- // Verify placeholder exists in data
358
- const data = slimInstance.getData()
359
- const hasPlaceholder = (data as Option[]).some((opt: any) => opt.placeholder)
360
- expect(hasPlaceholder).toBe(true)
361
-
362
- // CRITICAL: Verify what's actually selected - should be empty string (placeholder)
363
- const selectedValues = slimInstance.getSelected()
364
- expect(selectedValues).toEqual([''])
365
-
366
- // Verify no valid option is selected
367
- const hasValidOptionSelected = selectedValues.some((val) => val !== '' && ['1', '2'].includes(val))
368
- expect(hasValidOptionSelected).toBe(false)
369
- }
370
- })
371
-
372
- test('shows placeholder when value does not exist in options for single select', async () => {
373
- const TestComponent = () => {
374
- const [value, setValue] = useState('banana')
375
- return (
376
- <SlimSelectReact
377
- value={value}
378
- onChange={(val) => setValue(val as string)}
379
- data={[
380
- { value: '1', text: 'Option 1' },
381
- { value: '2', text: 'Option 2' },
382
- { value: '3', text: 'Option 3' }
383
- ]}
384
- />
385
- )
386
- }
387
-
388
- const { container } = render(<TestComponent />)
389
- await waitFor(
390
- () => {
391
- const select = container.querySelector('select')
392
- const slimInstance = (select as any)?._slimSelect as SlimSelect
393
- if (slimInstance) {
394
- // Verify placeholder exists in data (with empty value, not 'banana')
395
- const data = slimInstance.getData()
396
- const hasPlaceholder = (data as Option[]).some((opt: any) => opt.placeholder)
397
- expect(hasPlaceholder).toBe(true)
398
-
399
- // CRITICAL: Check what's actually displayed in the SlimSelect UI
400
- // SlimSelect renders a custom UI - check the .ss-single or .ss-placeholder element
401
- const slimMain = container.querySelector('.ss-main') as HTMLElement
402
- expect(slimMain).toBeDefined()
403
-
404
- // Check what's in the .ss-values container
405
- const ssValues = slimMain?.querySelector('.ss-values') as HTMLElement
406
- expect(ssValues).toBeDefined()
407
-
408
- // If placeholder is selected, it should show placeholder (no .ss-single element)
409
- // If a real option is selected, it will show .ss-single with the option text
410
- const singleValue = ssValues?.querySelector('.ss-single') as HTMLElement
411
- const placeholderEl = ssValues?.querySelector('.ss-placeholder') as HTMLElement
412
-
413
- // CRITICAL: If placeholder is correctly selected, we should NOT see .ss-single with "Option 1"
414
- // Instead, we should either see placeholder OR empty (if placeholder text is empty)
415
- if (singleValue) {
416
- const displayedText = singleValue.textContent?.trim() || ''
417
- // This should NOT be showing a real option
418
- expect(displayedText).not.toBe('Option 1')
419
- expect(displayedText).not.toBe('Option 2')
420
- expect(displayedText).not.toBe('Option 3')
421
-
422
- // If it's showing something, it might be the placeholder (empty text is OK)
423
- // But we should verify the placeholder option is actually selected in the store
424
- }
425
-
426
- // Check the store to see what's actually selected
427
- const selectedOptions = (slimInstance as any).store.getSelectedOptions()
428
- const hasPlaceholderSelected = selectedOptions.some((opt: any) => opt.placeholder && opt.selected)
429
- const hasValidOptionSelected = selectedOptions.some(
430
- (opt: any) => opt.selected && !opt.placeholder && ['1', '2', '3'].includes(opt.value)
431
- )
432
-
433
- // The placeholder should be selected, not a valid option
434
- expect(hasPlaceholderSelected).toBe(true)
435
- expect(hasValidOptionSelected).toBe(false)
436
- }
437
- },
438
- { timeout: 500 }
439
- )
440
- })
441
-
442
- test('handles empty array as value for multiple select', async () => {
443
- const TestComponent = () => {
444
- const [value, setValue] = useState([] as string[])
445
- return (
446
- <SlimSelectReact
447
- value={value}
448
- onChange={(val) => setValue(val as string[])}
449
- multiple
450
- data={[
451
- { value: '1', text: 'Option 1' },
452
- { value: '2', text: 'Option 2' }
453
- ]}
454
- />
455
- )
456
- }
457
-
458
- const { container } = render(<TestComponent />)
459
- await new Promise((resolve) => setTimeout(resolve, 100))
460
-
461
- // Should not throw errors
462
- expect(container.querySelector('select')).toBeTruthy()
463
- })
464
-
465
- test('updates onChange for multiple select when invalid values are provided', async () => {
466
- const onChangeMock = vi.fn()
467
-
468
- const TestComponent = () => {
469
- return (
470
- <SlimSelectReact
471
- value={['banana', 'apple']}
472
- onChange={onChangeMock}
473
- multiple
474
- data={[
475
- { value: '1', text: 'Option 1' },
476
- { value: '2', text: 'Option 2' },
477
- { value: '3', text: 'Option 3' }
478
- ]}
479
- />
480
- )
481
- }
482
-
483
- const { container } = render(<TestComponent />)
484
- await new Promise((resolve) => setTimeout(resolve, 100))
485
-
486
- const select = container.querySelector('select')
487
- const slimInstance = (select as any)?._slimSelect as SlimSelect
488
- if (slimInstance) {
489
- // When user manually selects valid options, onChange should update
490
- slimInstance.setSelected(['1', '2'])
491
- await new Promise((resolve) => setTimeout(resolve, 100))
492
-
493
- // Verify onChange has been called with the valid values
494
- expect(onChangeMock).toHaveBeenCalledWith(['1', '2'])
495
- }
496
- })
497
- })
498
- })
@@ -1,209 +0,0 @@
1
- import React, { useEffect, useRef, forwardRef, useImperativeHandle } from 'react'
2
- import SlimSelectCore from '../index'
3
- import type { Config, Option, Optgroup } from '../index'
4
-
5
- export interface SlimSelectProps {
6
- data?: (Partial<Option> | Partial<Optgroup>)[]
7
- settings?: Config['settings']
8
- events?: Config['events']
9
- cssClasses?: Config['cssClasses']
10
- value?: string | string[]
11
- onChange?: (value: string | string[]) => void
12
- children?: React.ReactNode
13
- multiple?: boolean
14
- }
15
-
16
- export interface SlimSelectRef {
17
- slimSelect: SlimSelectCore | null
18
- }
19
-
20
- const SlimSelect = forwardRef<SlimSelectRef, SlimSelectProps>(
21
- ({ data, settings, events, cssClasses, value, onChange, children, multiple }, ref) => {
22
- const selectRef = useRef<HTMLSelectElement>(null)
23
- const slimSelectRef = useRef<SlimSelectCore | null>(null)
24
- const isInitialMount = useRef(true)
25
- const currentValueRef = useRef<string | string[] | undefined>(value)
26
-
27
- // Expose SlimSelect instance to parent via ref
28
- useImperativeHandle(ref, () => ({
29
- slimSelect: slimSelectRef.current
30
- }))
31
-
32
- // Helper to get clean value (similar to Vue's getCleanValue)
33
- const getCleanValue = (val: string | string[] | undefined): string | string[] => {
34
- // If its multiple and the value is a string, return an array with the string
35
- if (typeof val === 'string') {
36
- return multiple ? [val] : val
37
- }
38
-
39
- // If its not multiple and the value is an array, return the first item
40
- if (Array.isArray(val)) {
41
- return multiple ? val : val[0]
42
- }
43
-
44
- return multiple ? [] : ''
45
- }
46
-
47
- // Helper to sync value to SlimSelect (similar to Vue's syncModelValueToSlimSelect)
48
- const syncValueToSlimSelect = (val: string | string[] | undefined, runAfterChange: boolean = false) => {
49
- if (!slimSelectRef.current) return
50
-
51
- // Only sync if value is explicitly set (not undefined)
52
- // This prevents adding placeholders when value is not provided
53
- if (val === undefined) {
54
- return
55
- }
56
-
57
- const cleanValue = getCleanValue(val)
58
- const data = slimSelectRef.current.getData()
59
- // Extract options from data (flatten optgroups if any)
60
- const options = data.flatMap((item: Option | Optgroup) => ('label' in item ? item.options : [item])) as Option[]
61
-
62
- // Check if the value exists in options
63
- const valueExists = Array.isArray(cleanValue)
64
- ? cleanValue.length > 0 && cleanValue.every((val) => options.some((opt) => opt.value === val))
65
- : cleanValue !== '' && options.some((opt) => opt.value === cleanValue)
66
-
67
- // If value doesn't exist in options, add a placeholder option
68
- if (!valueExists) {
69
- // If its not multiple, add a placeholder option
70
- if (!Array.isArray(cleanValue)) {
71
- // For single select, check if placeholder already exists
72
- const hasPlaceholder = options.some((opt) => opt.placeholder)
73
- if (!hasPlaceholder) {
74
- // Get current data and prepend placeholder option
75
- const currentData = slimSelectRef.current.getData()
76
- const placeholderOption: Partial<Option> = {
77
- value: '',
78
- text: '',
79
- placeholder: true
80
- }
81
- slimSelectRef.current.setData([placeholderOption].concat(currentData))
82
- }
83
- }
84
- }
85
-
86
- // Set the selected value (will select placeholder if it was just added)
87
- // Match Vue component exactly - call setSelected with cleanValue directly
88
- slimSelectRef.current.setSelected(cleanValue, runAfterChange)
89
- }
90
-
91
- // Initialize SlimSelect
92
- useEffect(() => {
93
- if (!selectRef.current) return
94
-
95
- const config: Config = {
96
- select: selectRef.current
97
- }
98
-
99
- if (data) {
100
- config.data = data
101
- }
102
-
103
- if (settings) {
104
- config.settings = settings
105
- }
106
-
107
- if (cssClasses) {
108
- config.cssClasses = cssClasses
109
- }
110
-
111
- // Wrap onChange in afterChange event
112
- const ogAfterChange = events?.afterChange
113
- config.events = {
114
- ...events,
115
- afterChange: (newVal) => {
116
- if (!slimSelectRef.current) return
117
-
118
- const newValue = multiple ? newVal.map((option) => option.value) : (newVal[0]?.value ?? '')
119
- const slimData = slimSelectRef.current.getData()
120
- const options = slimData.flatMap((item: Option | Optgroup) =>
121
- 'label' in item ? item.options : [item]
122
- ) as Option[]
123
-
124
- // Check if the current value exists in options
125
- const currentValue = currentValueRef.current
126
- const currentValueExists =
127
- currentValue === undefined
128
- ? false
129
- : Array.isArray(currentValue)
130
- ? currentValue.length > 0 && currentValue.every((val) => options.some((opt) => opt.value === val))
131
- : currentValue !== '' && options.some((opt) => opt.value === currentValue)
132
-
133
- // Check if the new value is valid (exists in options)
134
- const newValueIsValid = Array.isArray(newValue)
135
- ? newValue.length > 0 && newValue.every((val) => options.some((opt) => opt.value === val))
136
- : newValue !== '' && options.some((opt) => opt.value === newValue)
137
-
138
- // Check if value actually changed (properly compare arrays)
139
- const valueChanged =
140
- Array.isArray(newValue) && Array.isArray(currentValue)
141
- ? JSON.stringify(newValue.sort()) !== JSON.stringify(currentValue.sort())
142
- : currentValue !== newValue
143
-
144
- // Only call onChange if:
145
- // 1. The value actually changed, AND
146
- // 2. Either the current value exists in options, OR we're setting to a valid non-empty value
147
- // This prevents clearing invalid values when we show placeholder
148
- if (onChange && valueChanged && (currentValueExists || newValueIsValid)) {
149
- onChange(newValue)
150
- currentValueRef.current = newValue
151
- }
152
-
153
- if (ogAfterChange) {
154
- ogAfterChange(newVal)
155
- }
156
- }
157
- }
158
-
159
- slimSelectRef.current = new SlimSelectCore(config)
160
-
161
- // Sync initial value if provided
162
- if (value !== undefined) {
163
- syncValueToSlimSelect(value, false)
164
- }
165
-
166
- return () => {
167
- if (slimSelectRef.current) {
168
- slimSelectRef.current.destroy()
169
- slimSelectRef.current = null
170
- }
171
- }
172
- }, []) // Only run on mount/unmount
173
-
174
- // Handle value changes from parent
175
- useEffect(() => {
176
- if (isInitialMount.current) {
177
- isInitialMount.current = false
178
- currentValueRef.current = value
179
- return
180
- }
181
-
182
- if (slimSelectRef.current && value !== undefined) {
183
- currentValueRef.current = value
184
- syncValueToSlimSelect(value, false)
185
- }
186
- }, [value])
187
-
188
- // Handle data changes
189
- useEffect(() => {
190
- if (slimSelectRef.current && data && !isInitialMount.current) {
191
- slimSelectRef.current.setData(data)
192
- // Re-sync value after data changes
193
- if (value !== undefined) {
194
- syncValueToSlimSelect(value, false)
195
- }
196
- }
197
- }, [data])
198
-
199
- return (
200
- <select ref={selectRef} multiple={multiple}>
201
- {children}
202
- </select>
203
- )
204
- }
205
- )
206
-
207
- SlimSelect.displayName = 'SlimSelect'
208
-
209
- export default SlimSelect