sveltacular 1.1.0 → 1.1.1
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.
- package/dist/forms/check-box/check-box-group.svelte +32 -9
- package/dist/forms/check-box/check-box-group.svelte.d.ts +17 -4
- package/dist/forms/list-box/list-box.svelte +46 -13
- package/dist/forms/list-box/list-box.svelte.d.ts +17 -4
- package/dist/forms/radio-group/radio-group.svelte +50 -7
- package/dist/forms/radio-group/radio-group.svelte.d.ts +18 -5
- package/dist/forms/reference-box/reference-box.svelte +72 -17
- package/dist/forms/reference-box/reference-box.svelte.d.ts +52 -19
- package/dist/types/form.d.ts +75 -0
- package/dist/types/form.js +73 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
|
-
import type { ReferenceItem, ComponentSize } from '../../types/form.js';
|
|
3
|
+
import type { ReferenceItem, ComponentSize, FieldNameMapping } from '../../types/form.js';
|
|
4
|
+
import { createFieldMapper } from '../../types/form.js';
|
|
4
5
|
import FormField from '../form-field/form-field.svelte';
|
|
5
6
|
import CheckBox from './check-box.svelte';
|
|
6
7
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
@@ -8,40 +9,62 @@
|
|
|
8
9
|
const id = uniqueId();
|
|
9
10
|
|
|
10
11
|
let {
|
|
11
|
-
group = $bindable([] as string[]),
|
|
12
|
+
group = $bindable([] as (string | number)[]),
|
|
12
13
|
items = [],
|
|
14
|
+
fieldNames = undefined as FieldNameMapping | undefined,
|
|
13
15
|
size = 'md' as ComponentSize,
|
|
14
16
|
disabled = false,
|
|
15
17
|
required = false,
|
|
16
18
|
onChange,
|
|
17
19
|
label
|
|
18
20
|
}: {
|
|
19
|
-
group?: string[];
|
|
20
|
-
items?:
|
|
21
|
+
group?: (string | number)[];
|
|
22
|
+
items?: any[];
|
|
23
|
+
/**
|
|
24
|
+
* Maps database field names to ReferenceItem properties.
|
|
25
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Basic usage
|
|
29
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // With description field
|
|
33
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
34
|
+
*/
|
|
35
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
21
36
|
size?: ComponentSize;
|
|
22
37
|
disabled?: boolean;
|
|
23
38
|
required?: boolean;
|
|
24
|
-
onChange?: ((selected: string[]) => void) | undefined;
|
|
39
|
+
onChange?: ((selected: (string | number)[]) => void) | undefined;
|
|
25
40
|
label?: string;
|
|
26
41
|
} = $props();
|
|
27
42
|
|
|
43
|
+
// Create field mapper
|
|
44
|
+
const mapper = $derived(createFieldMapper<any>(fieldNames));
|
|
45
|
+
|
|
46
|
+
// Transform items for internal use (always work with ReferenceItem internally)
|
|
47
|
+
const referenceItems = $derived(
|
|
48
|
+
fieldNames ? items.map(item => mapper.toReferenceItem(item)) : items as ReferenceItem[]
|
|
49
|
+
);
|
|
50
|
+
|
|
28
51
|
// Create reactive items with checked state, synced with group
|
|
29
52
|
let itemsWithState = $state<Array<ReferenceItem & { isChecked: boolean }>>([]);
|
|
30
53
|
|
|
31
54
|
// Sync itemsWithState when items or group changes (one-way: items/group -> itemsWithState)
|
|
32
55
|
// Reassign the entire array to avoid reading itemsWithState in the effect
|
|
33
56
|
$effect(() => {
|
|
34
|
-
// Track
|
|
35
|
-
const currentItems =
|
|
57
|
+
// Track referenceItems and group as dependencies
|
|
58
|
+
const currentItems = referenceItems;
|
|
36
59
|
const currentGroup = group;
|
|
37
60
|
|
|
38
61
|
// Use untrack to prevent writing to itemsWithState from triggering this effect again
|
|
39
62
|
untrack(() => {
|
|
40
|
-
// Rebuild itemsWithState from
|
|
63
|
+
// Rebuild itemsWithState from referenceItems, using group to determine checked state
|
|
41
64
|
// Reassign instead of mutate to avoid circular dependency
|
|
42
65
|
const newItems = currentItems.map((item) => ({
|
|
43
66
|
...item,
|
|
44
|
-
isChecked: currentGroup.includes(item.value
|
|
67
|
+
isChecked: currentGroup.includes(item.value as any)
|
|
45
68
|
}));
|
|
46
69
|
itemsWithState = newItems;
|
|
47
70
|
});
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComponentSize, FieldNameMapping } from '../../types/form.js';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
|
-
group?: string[];
|
|
4
|
-
items?:
|
|
3
|
+
group?: (string | number)[];
|
|
4
|
+
items?: any[];
|
|
5
|
+
/**
|
|
6
|
+
* Maps database field names to ReferenceItem properties.
|
|
7
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Basic usage
|
|
11
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* // With description field
|
|
15
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
16
|
+
*/
|
|
17
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
5
18
|
size?: ComponentSize;
|
|
6
19
|
disabled?: boolean;
|
|
7
20
|
required?: boolean;
|
|
8
|
-
onChange?: ((selected: string[]) => void) | undefined;
|
|
21
|
+
onChange?: ((selected: (string | number)[]) => void) | undefined;
|
|
9
22
|
label?: string;
|
|
10
23
|
};
|
|
11
24
|
declare const CheckBoxGroup: import("svelte").Component<$$ComponentProps, {}, "group">;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { ReferenceItem, ComponentSize } from '../../types/form.js';
|
|
2
|
+
import type { ReferenceItem, ComponentSize, FieldNameMapping } from '../../types/form.js';
|
|
3
|
+
import { createFieldMapper } from '../../types/form.js';
|
|
3
4
|
import FormField, { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
4
5
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
5
6
|
import Menu from '../../generic/menu/menu.svelte';
|
|
@@ -12,8 +13,9 @@
|
|
|
12
13
|
import type { CreateNewFunction, SearchFunction } from '../../types/form.js';
|
|
13
14
|
|
|
14
15
|
let {
|
|
15
|
-
value = $bindable(null as string | null),
|
|
16
|
-
items = [] as
|
|
16
|
+
value = $bindable(null as string | number | null),
|
|
17
|
+
items = [] as any[],
|
|
18
|
+
fieldNames = undefined as FieldNameMapping | undefined,
|
|
17
19
|
size = 'md',
|
|
18
20
|
disabled = false,
|
|
19
21
|
required = false,
|
|
@@ -32,8 +34,21 @@
|
|
|
32
34
|
createNew = undefined,
|
|
33
35
|
resourceName = undefined
|
|
34
36
|
}: {
|
|
35
|
-
value?: string | null;
|
|
36
|
-
items?:
|
|
37
|
+
value?: string | number | null;
|
|
38
|
+
items?: any[];
|
|
39
|
+
/**
|
|
40
|
+
* Maps database field names to ReferenceItem properties.
|
|
41
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Basic usage
|
|
45
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // With description field
|
|
49
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
50
|
+
*/
|
|
51
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
37
52
|
size?: ComponentSize;
|
|
38
53
|
disabled?: boolean;
|
|
39
54
|
required?: boolean;
|
|
@@ -41,7 +56,7 @@
|
|
|
41
56
|
searchable?: boolean;
|
|
42
57
|
search?: SearchFunction | undefined;
|
|
43
58
|
placeholder?: string;
|
|
44
|
-
onChange?: ((value: string | null) => void) | undefined;
|
|
59
|
+
onChange?: ((value: string | number | null) => void) | undefined;
|
|
45
60
|
onFocus?: ((e: FocusEvent) => void) | undefined;
|
|
46
61
|
onBlur?: ((e: FocusEvent) => void) | undefined;
|
|
47
62
|
label?: string;
|
|
@@ -56,9 +71,17 @@
|
|
|
56
71
|
const id = uniqueId();
|
|
57
72
|
const listboxId = `${id}-listbox`;
|
|
58
73
|
|
|
59
|
-
//
|
|
74
|
+
// Create field mapper
|
|
75
|
+
const mapper = $derived(createFieldMapper<any>(fieldNames));
|
|
76
|
+
|
|
77
|
+
// Transform items for internal use (always work with ReferenceItem internally)
|
|
78
|
+
const referenceItems = $derived(
|
|
79
|
+
fieldNames ? items.map(item => mapper.toReferenceItem(item)) : items as ReferenceItem[]
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Use local items state when search function is provided, otherwise use transformed items
|
|
60
83
|
let localItems = $state<ReferenceItem[]>([]);
|
|
61
|
-
let currentItems = $derived(search ? localItems :
|
|
84
|
+
let currentItems = $derived(search ? localItems : referenceItems);
|
|
62
85
|
|
|
63
86
|
const getText = () => currentItems.find((item) => item.value == value)?.label || '';
|
|
64
87
|
|
|
@@ -78,10 +101,18 @@
|
|
|
78
101
|
// Initialize localItems when items prop changes (only when no search function)
|
|
79
102
|
$effect(() => {
|
|
80
103
|
if (!search) {
|
|
81
|
-
localItems = [...
|
|
104
|
+
localItems = [...referenceItems];
|
|
82
105
|
}
|
|
83
106
|
});
|
|
84
107
|
|
|
108
|
+
// Internal value for Menu component (always string | null)
|
|
109
|
+
let internalValue = $state<string | null>(value != null ? String(value) : null);
|
|
110
|
+
|
|
111
|
+
// Sync internal value from external value
|
|
112
|
+
$effect(() => {
|
|
113
|
+
internalValue = value != null ? String(value) : null;
|
|
114
|
+
});
|
|
115
|
+
|
|
85
116
|
// Initialize text from value on mount and when value/items change (but not when user is typing)
|
|
86
117
|
$effect(() => {
|
|
87
118
|
// Track value and currentItems to update text when they change
|
|
@@ -141,7 +172,8 @@
|
|
|
141
172
|
// When an item is selected from the dropdown menu
|
|
142
173
|
const onSelect = (item: ReferenceItem) => {
|
|
143
174
|
isUserTyping = false;
|
|
144
|
-
|
|
175
|
+
// Keep value as-is (string | number | null)
|
|
176
|
+
value = item.value;
|
|
145
177
|
onChange?.(value);
|
|
146
178
|
text = getText();
|
|
147
179
|
isMenuOpen = false;
|
|
@@ -337,11 +369,12 @@
|
|
|
337
369
|
const result = await createNew(name);
|
|
338
370
|
|
|
339
371
|
if (result) {
|
|
340
|
-
items
|
|
372
|
+
// Note: items prop is read-only, so we can't update it
|
|
373
|
+
// Just add to localItems for display
|
|
341
374
|
localItems = [...localItems, result];
|
|
342
375
|
|
|
343
376
|
// Select the newly created item
|
|
344
|
-
value = result.value
|
|
377
|
+
value = result.value;
|
|
345
378
|
onChange?.(value);
|
|
346
379
|
text = result.label;
|
|
347
380
|
isMenuOpen = false;
|
|
@@ -475,7 +508,7 @@
|
|
|
475
508
|
searchText={isSearchable ? text : ''}
|
|
476
509
|
{onSelect}
|
|
477
510
|
bind:highlightIndex
|
|
478
|
-
bind:value
|
|
511
|
+
bind:value={internalValue}
|
|
479
512
|
{listboxId}
|
|
480
513
|
{virtualScroll}
|
|
481
514
|
{itemHeight}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import type { ReferenceItem, ComponentSize } from '../../types/form.js';
|
|
1
|
+
import type { ReferenceItem, ComponentSize, FieldNameMapping } from '../../types/form.js';
|
|
2
2
|
import { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
3
3
|
import type { CreateNewFunction, SearchFunction } from '../../types/form.js';
|
|
4
4
|
type $$ComponentProps = {
|
|
5
|
-
value?: string | null;
|
|
6
|
-
items?:
|
|
5
|
+
value?: string | number | null;
|
|
6
|
+
items?: any[];
|
|
7
|
+
/**
|
|
8
|
+
* Maps database field names to ReferenceItem properties.
|
|
9
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Basic usage
|
|
13
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // With description field
|
|
17
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
18
|
+
*/
|
|
19
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
7
20
|
size?: ComponentSize;
|
|
8
21
|
disabled?: boolean;
|
|
9
22
|
required?: boolean;
|
|
@@ -11,7 +24,7 @@ type $$ComponentProps = {
|
|
|
11
24
|
searchable?: boolean;
|
|
12
25
|
search?: SearchFunction | undefined;
|
|
13
26
|
placeholder?: string;
|
|
14
|
-
onChange?: ((value: string | null) => void) | undefined;
|
|
27
|
+
onChange?: ((value: string | number | null) => void) | undefined;
|
|
15
28
|
onFocus?: ((e: FocusEvent) => void) | undefined;
|
|
16
29
|
onBlur?: ((e: FocusEvent) => void) | undefined;
|
|
17
30
|
label?: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { ReferenceItem, ComponentSize } from '../../types/form.js';
|
|
2
|
+
import type { ReferenceItem, ComponentSize, FieldNameMapping } from '../../types/form.js';
|
|
3
|
+
import { createFieldMapper } from '../../types/form.js';
|
|
3
4
|
import FormField, { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
4
5
|
import { uniqueId } from '../../helpers/unique-id.js';
|
|
5
6
|
import RadioBox from './radio-box.svelte';
|
|
@@ -7,8 +8,9 @@
|
|
|
7
8
|
const id = uniqueId();
|
|
8
9
|
|
|
9
10
|
let {
|
|
10
|
-
group = '',
|
|
11
|
+
group = $bindable(''),
|
|
11
12
|
items = [],
|
|
13
|
+
fieldNames = undefined as FieldNameMapping | undefined,
|
|
12
14
|
size = 'md' as ComponentSize,
|
|
13
15
|
disabled = false,
|
|
14
16
|
required = false,
|
|
@@ -17,22 +19,63 @@
|
|
|
17
19
|
feedback = undefined,
|
|
18
20
|
onChange = undefined
|
|
19
21
|
}: {
|
|
20
|
-
group?: string;
|
|
21
|
-
items?:
|
|
22
|
+
group?: string | number;
|
|
23
|
+
items?: any[];
|
|
24
|
+
/**
|
|
25
|
+
* Maps database field names to ReferenceItem properties.
|
|
26
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Basic usage
|
|
30
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // With description field
|
|
34
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
35
|
+
*/
|
|
36
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
22
37
|
size?: ComponentSize;
|
|
23
38
|
disabled?: boolean;
|
|
24
39
|
required?: boolean;
|
|
25
40
|
label?: string;
|
|
26
41
|
helperText?: string;
|
|
27
42
|
feedback?: FormFieldFeedback;
|
|
28
|
-
onChange?: ((value: string) => void) | undefined;
|
|
43
|
+
onChange?: ((value: string | number) => void) | undefined;
|
|
29
44
|
} = $props();
|
|
45
|
+
|
|
46
|
+
// Create field mapper
|
|
47
|
+
const mapper = $derived(createFieldMapper<any>(fieldNames));
|
|
48
|
+
|
|
49
|
+
// Transform items for internal use (always work with ReferenceItem internally)
|
|
50
|
+
const referenceItems = $derived(
|
|
51
|
+
fieldNames ? items.map(item => mapper.toReferenceItem(item)) : items as ReferenceItem[]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Internal string group for binding to RadioBox components
|
|
55
|
+
let internalGroup = $state<string>(String(group ?? ''));
|
|
56
|
+
|
|
57
|
+
// Sync internal group from external group
|
|
58
|
+
$effect(() => {
|
|
59
|
+
internalGroup = String(group ?? '');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Handle change to convert back to original type
|
|
63
|
+
function handleChange(value: string) {
|
|
64
|
+
// Try to convert back to number if the original was a number
|
|
65
|
+
const numValue = Number(value);
|
|
66
|
+
if (!isNaN(numValue) && String(numValue) === value) {
|
|
67
|
+
group = numValue;
|
|
68
|
+
} else {
|
|
69
|
+
group = value;
|
|
70
|
+
}
|
|
71
|
+
onChange?.(group);
|
|
72
|
+
}
|
|
30
73
|
</script>
|
|
31
74
|
|
|
32
75
|
<FormField {size} {label} {id} {required} {disabled} {helperText} {feedback}>
|
|
33
76
|
<div>
|
|
34
|
-
{#each
|
|
35
|
-
<RadioBox bind:group {disabled} value={item.value} {
|
|
77
|
+
{#each referenceItems as item}
|
|
78
|
+
<RadioBox bind:group={internalGroup} {disabled} value={item.value} onChange={handleChange}>{item.label}</RadioBox>
|
|
36
79
|
{/each}
|
|
37
80
|
</div>
|
|
38
81
|
</FormField>
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ComponentSize, FieldNameMapping } from '../../types/form.js';
|
|
2
2
|
import { type FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
3
3
|
type $$ComponentProps = {
|
|
4
|
-
group?: string;
|
|
5
|
-
items?:
|
|
4
|
+
group?: string | number;
|
|
5
|
+
items?: any[];
|
|
6
|
+
/**
|
|
7
|
+
* Maps database field names to ReferenceItem properties.
|
|
8
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Basic usage
|
|
12
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // With description field
|
|
16
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
17
|
+
*/
|
|
18
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
6
19
|
size?: ComponentSize;
|
|
7
20
|
disabled?: boolean;
|
|
8
21
|
required?: boolean;
|
|
9
22
|
label?: string;
|
|
10
23
|
helperText?: string;
|
|
11
24
|
feedback?: FormFieldFeedback;
|
|
12
|
-
onChange?: ((value: string) => void) | undefined;
|
|
25
|
+
onChange?: ((value: string | number) => void) | undefined;
|
|
13
26
|
};
|
|
14
|
-
declare const RadioGroup: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
27
|
+
declare const RadioGroup: import("svelte").Component<$$ComponentProps, {}, "group">;
|
|
15
28
|
type RadioGroup = ReturnType<typeof RadioGroup>;
|
|
16
29
|
export default RadioGroup;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts">
|
|
1
|
+
<script lang="ts" generics="T = ReferenceItem">
|
|
2
2
|
import MultiSelectBase, {
|
|
3
3
|
type MultiSelectAdapter
|
|
4
4
|
} from '../multi-select-base/multi-select-base.svelte';
|
|
@@ -8,15 +8,18 @@
|
|
|
8
8
|
ReferenceItem,
|
|
9
9
|
SearchFunction,
|
|
10
10
|
CreateNewFunction,
|
|
11
|
-
LinkBuilderFunction
|
|
11
|
+
LinkBuilderFunction,
|
|
12
|
+
FieldNameMapping
|
|
12
13
|
} from '../../types/form.js';
|
|
14
|
+
import { createFieldMapper } from '../../types/form.js';
|
|
13
15
|
import Prompt from '../../modals/prompt.svelte';
|
|
14
16
|
import { ucfirst } from '../../helpers/ucfirst.js';
|
|
15
17
|
import Icon from '../../icons/icon.svelte';
|
|
16
18
|
|
|
17
19
|
let {
|
|
18
|
-
value = $bindable([] as
|
|
19
|
-
items = [] as
|
|
20
|
+
value = $bindable([] as T[]),
|
|
21
|
+
items = [] as T[],
|
|
22
|
+
fieldNames = undefined as FieldNameMapping | undefined,
|
|
20
23
|
search = undefined,
|
|
21
24
|
createNew = undefined,
|
|
22
25
|
linkBuilder = undefined as LinkBuilderFunction | undefined,
|
|
@@ -29,10 +32,23 @@
|
|
|
29
32
|
helperText = undefined as string | undefined,
|
|
30
33
|
feedback = undefined as FormFieldFeedback | undefined,
|
|
31
34
|
maxItems = undefined as number | undefined,
|
|
32
|
-
onChange = undefined as ((value:
|
|
35
|
+
onChange = undefined as ((value: T[]) => void) | undefined
|
|
33
36
|
}: {
|
|
34
|
-
value?:
|
|
35
|
-
items?:
|
|
37
|
+
value?: T[];
|
|
38
|
+
items?: T[];
|
|
39
|
+
/**
|
|
40
|
+
* Maps database field names to ReferenceItem properties.
|
|
41
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Basic usage
|
|
45
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // With description field
|
|
49
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
50
|
+
*/
|
|
51
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
36
52
|
search?: SearchFunction | undefined;
|
|
37
53
|
createNew?: CreateNewFunction<ReferenceItem> | undefined;
|
|
38
54
|
linkBuilder?: LinkBuilderFunction | undefined;
|
|
@@ -45,7 +61,7 @@
|
|
|
45
61
|
helperText?: string;
|
|
46
62
|
feedback?: FormFieldFeedback | undefined;
|
|
47
63
|
maxItems?: number | undefined;
|
|
48
|
-
onChange?: ((value:
|
|
64
|
+
onChange?: ((value: T[]) => void) | undefined;
|
|
49
65
|
} = $props();
|
|
50
66
|
|
|
51
67
|
let baseComponent: MultiSelectBase<ReferenceItem> | null = $state(null);
|
|
@@ -57,10 +73,40 @@
|
|
|
57
73
|
let promptKey = $state(0);
|
|
58
74
|
let currentSearchText = $state('');
|
|
59
75
|
|
|
60
|
-
//
|
|
61
|
-
|
|
76
|
+
// Create field mapper
|
|
77
|
+
const mapper = $derived(createFieldMapper<T>(fieldNames));
|
|
62
78
|
|
|
63
|
-
//
|
|
79
|
+
// Transform items for internal use (always work with ReferenceItem internally)
|
|
80
|
+
const referenceItems = $derived(
|
|
81
|
+
fieldNames ? items.map(item => mapper.toReferenceItem(item)) : items as unknown as ReferenceItem[]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Internal value state (always ReferenceItem[])
|
|
85
|
+
let internalValue = $state<ReferenceItem[]>([]);
|
|
86
|
+
|
|
87
|
+
// Sync internal value from external value
|
|
88
|
+
$effect(() => {
|
|
89
|
+
if (fieldNames) {
|
|
90
|
+
internalValue = value.map(item => mapper.toReferenceItem(item));
|
|
91
|
+
} else {
|
|
92
|
+
internalValue = value as unknown as ReferenceItem[];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Update external value from internal value
|
|
97
|
+
function updateExternalValue(newInternalValue: ReferenceItem[]) {
|
|
98
|
+
if (fieldNames) {
|
|
99
|
+
value = newInternalValue.map(ref => mapper.fromReferenceItem(ref));
|
|
100
|
+
} else {
|
|
101
|
+
value = newInternalValue as unknown as T[];
|
|
102
|
+
}
|
|
103
|
+
onChange?.(value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Use local items when search function is provided, otherwise use transformed items
|
|
107
|
+
let currentItems = $derived(search ? localItems : referenceItems);
|
|
108
|
+
|
|
109
|
+
// Convert to menu options format
|
|
64
110
|
let menuOptions = $derived(currentItems);
|
|
65
111
|
|
|
66
112
|
// Adapter to work with ReferenceItems
|
|
@@ -69,7 +115,15 @@
|
|
|
69
115
|
getKey: (item: ReferenceItem) => String(item.value ?? item.label),
|
|
70
116
|
equals: (a: ReferenceItem, b: ReferenceItem) => a.value === b.value,
|
|
71
117
|
fromMenuOption: (option: ReferenceItem) => option,
|
|
72
|
-
getLink: linkBuilder
|
|
118
|
+
getLink: linkBuilder
|
|
119
|
+
? (item: ReferenceItem) => {
|
|
120
|
+
// Find original item to pass to linkBuilder
|
|
121
|
+
const original = fieldNames
|
|
122
|
+
? value.find(v => mapper.toReferenceItem(v).value === item.value)
|
|
123
|
+
: item;
|
|
124
|
+
return linkBuilder(original as any);
|
|
125
|
+
}
|
|
126
|
+
: undefined,
|
|
73
127
|
getTooltip: (item: ReferenceItem) => item.description
|
|
74
128
|
});
|
|
75
129
|
|
|
@@ -122,9 +176,10 @@
|
|
|
122
176
|
localItems = [...localItems, result];
|
|
123
177
|
}
|
|
124
178
|
|
|
125
|
-
// Add the newly created item to value
|
|
126
|
-
|
|
127
|
-
|
|
179
|
+
// Add the newly created item to internal value, then sync to external
|
|
180
|
+
const newInternalValue = [...internalValue, result];
|
|
181
|
+
internalValue = newInternalValue;
|
|
182
|
+
updateExternalValue(newInternalValue);
|
|
128
183
|
showPrompt = false;
|
|
129
184
|
} else {
|
|
130
185
|
createError = 'Failed to create new item';
|
|
@@ -158,7 +213,7 @@
|
|
|
158
213
|
|
|
159
214
|
<MultiSelectBase
|
|
160
215
|
bind:this={baseComponent}
|
|
161
|
-
bind:value
|
|
216
|
+
bind:value={internalValue}
|
|
162
217
|
adapter={referenceAdapter}
|
|
163
218
|
{placeholder}
|
|
164
219
|
{required}
|
|
@@ -169,7 +224,7 @@
|
|
|
169
224
|
{helperText}
|
|
170
225
|
{feedback}
|
|
171
226
|
{maxItems}
|
|
172
|
-
{
|
|
227
|
+
onChange={updateExternalValue}
|
|
173
228
|
onInputChange={handleInputChange}
|
|
174
229
|
{isLoading}
|
|
175
230
|
filterSuggestions={!search}
|
|
@@ -1,22 +1,55 @@
|
|
|
1
1
|
import type { FormFieldFeedback } from '../form-field/form-field.svelte';
|
|
2
|
-
import type { ComponentSize, ReferenceItem, SearchFunction, CreateNewFunction, LinkBuilderFunction } from '../../types/form.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
2
|
+
import type { ComponentSize, ReferenceItem, SearchFunction, CreateNewFunction, LinkBuilderFunction, FieldNameMapping } from '../../types/form.js';
|
|
3
|
+
declare function $$render<T = ReferenceItem>(): {
|
|
4
|
+
props: {
|
|
5
|
+
value?: T[];
|
|
6
|
+
items?: T[];
|
|
7
|
+
/**
|
|
8
|
+
* Maps database field names to ReferenceItem properties.
|
|
9
|
+
* Use this when your data uses different field names (e.g., 'name' instead of 'label').
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Basic usage
|
|
13
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // With description field
|
|
17
|
+
* fieldNames={{ label: 'title', value: 'id', description: 'subtitle' }}
|
|
18
|
+
*/
|
|
19
|
+
fieldNames?: FieldNameMapping | undefined;
|
|
20
|
+
search?: SearchFunction | undefined;
|
|
21
|
+
createNew?: CreateNewFunction<ReferenceItem> | undefined;
|
|
22
|
+
linkBuilder?: LinkBuilderFunction | undefined;
|
|
23
|
+
resourceName?: string | undefined;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
required?: boolean;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
size?: ComponentSize;
|
|
28
|
+
label?: string;
|
|
29
|
+
helperText?: string;
|
|
30
|
+
feedback?: FormFieldFeedback | undefined;
|
|
31
|
+
maxItems?: number | undefined;
|
|
32
|
+
onChange?: ((value: T[]) => void) | undefined;
|
|
33
|
+
};
|
|
34
|
+
exports: {};
|
|
35
|
+
bindings: "value";
|
|
36
|
+
slots: {};
|
|
37
|
+
events: {};
|
|
19
38
|
};
|
|
20
|
-
declare
|
|
21
|
-
|
|
39
|
+
declare class __sveltets_Render<T = ReferenceItem> {
|
|
40
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
41
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
42
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
43
|
+
bindings(): "value";
|
|
44
|
+
exports(): {};
|
|
45
|
+
}
|
|
46
|
+
interface $$IsomorphicComponent {
|
|
47
|
+
new <T = ReferenceItem>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
48
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
49
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
50
|
+
<T = ReferenceItem>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
51
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
52
|
+
}
|
|
53
|
+
declare const ReferenceBox: $$IsomorphicComponent;
|
|
54
|
+
type ReferenceBox<T = ReferenceItem> = InstanceType<typeof ReferenceBox<T>>;
|
|
22
55
|
export default ReferenceBox;
|
package/dist/types/form.d.ts
CHANGED
|
@@ -33,3 +33,78 @@ export type CreateNewFunction<ReferenceItem> = (inputName: string) => Promise<Re
|
|
|
33
33
|
* Link builder function type
|
|
34
34
|
*/
|
|
35
35
|
export type LinkBuilderFunction = (item: ReferenceItem) => string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Maps database object field names to ReferenceItem properties.
|
|
38
|
+
* Allows components to work directly with your database objects.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Basic usage - map database fields to display format
|
|
42
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // With optional fields
|
|
46
|
+
* fieldNames={{
|
|
47
|
+
* label: 'title',
|
|
48
|
+
* value: 'id',
|
|
49
|
+
* description: 'subtitle',
|
|
50
|
+
* disabled: 'isInactive'
|
|
51
|
+
* }}
|
|
52
|
+
*/
|
|
53
|
+
export type FieldNameMapping = {
|
|
54
|
+
/**
|
|
55
|
+
* Database field name to use for display label
|
|
56
|
+
* @default 'label'
|
|
57
|
+
*/
|
|
58
|
+
label?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Database field name to use for unique value/ID
|
|
61
|
+
* @default 'value'
|
|
62
|
+
*/
|
|
63
|
+
value?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Database field name to use for description/subtitle text
|
|
66
|
+
* @default 'description'
|
|
67
|
+
*/
|
|
68
|
+
description?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Database field name to use for disabled state
|
|
71
|
+
* @default 'disabled'
|
|
72
|
+
*/
|
|
73
|
+
disabled?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Creates field mapping transformation functions.
|
|
77
|
+
* Used internally by components to convert between user types and ReferenceItem.
|
|
78
|
+
*
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export declare function createFieldMapper<T>(fieldNames?: FieldNameMapping): {
|
|
82
|
+
/**
|
|
83
|
+
* Converts user's database object to ReferenceItem for internal use
|
|
84
|
+
*/
|
|
85
|
+
toReferenceItem: (item: T) => ReferenceItem;
|
|
86
|
+
/**
|
|
87
|
+
* Converts ReferenceItem back to user's database object type
|
|
88
|
+
* Note: Only reconstructs mapped fields, additional fields are lost
|
|
89
|
+
*/
|
|
90
|
+
fromReferenceItem: (ref: ReferenceItem) => T;
|
|
91
|
+
/**
|
|
92
|
+
* Extracts just the value field from user's object
|
|
93
|
+
*/
|
|
94
|
+
extractValue: (item: T) => string | number | null;
|
|
95
|
+
/**
|
|
96
|
+
* Finds an item in array by matching value field
|
|
97
|
+
*/
|
|
98
|
+
findByValue: (items: T[], value: string | number | null) => T | undefined;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Helper function to create field name mappings with less syntax.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* // Instead of:
|
|
105
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
106
|
+
*
|
|
107
|
+
* // You can write:
|
|
108
|
+
* fieldNames={mapFields('name', 'id')}
|
|
109
|
+
*/
|
|
110
|
+
export declare function mapFields(label: string, value: string, description?: string, disabled?: string): FieldNameMapping;
|
package/dist/types/form.js
CHANGED
|
@@ -1 +1,73 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Creates field mapping transformation functions.
|
|
3
|
+
* Used internally by components to convert between user types and ReferenceItem.
|
|
4
|
+
*
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export function createFieldMapper(fieldNames) {
|
|
8
|
+
const labelField = fieldNames?.label ?? 'label';
|
|
9
|
+
const valueField = fieldNames?.value ?? 'value';
|
|
10
|
+
const descriptionField = fieldNames?.description ?? 'description';
|
|
11
|
+
const disabledField = fieldNames?.disabled ?? 'disabled';
|
|
12
|
+
return {
|
|
13
|
+
/**
|
|
14
|
+
* Converts user's database object to ReferenceItem for internal use
|
|
15
|
+
*/
|
|
16
|
+
toReferenceItem: (item) => {
|
|
17
|
+
const obj = item;
|
|
18
|
+
return {
|
|
19
|
+
label: String(obj[labelField] ?? ''),
|
|
20
|
+
value: obj[valueField] ?? null,
|
|
21
|
+
description: obj[descriptionField],
|
|
22
|
+
disabled: obj[disabledField]
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
/**
|
|
26
|
+
* Converts ReferenceItem back to user's database object type
|
|
27
|
+
* Note: Only reconstructs mapped fields, additional fields are lost
|
|
28
|
+
*/
|
|
29
|
+
fromReferenceItem: (ref) => {
|
|
30
|
+
const obj = {
|
|
31
|
+
[labelField]: ref.label,
|
|
32
|
+
[valueField]: ref.value
|
|
33
|
+
};
|
|
34
|
+
if (ref.description !== undefined) {
|
|
35
|
+
obj[descriptionField] = ref.description;
|
|
36
|
+
}
|
|
37
|
+
if (ref.disabled !== undefined) {
|
|
38
|
+
obj[disabledField] = ref.disabled;
|
|
39
|
+
}
|
|
40
|
+
return obj;
|
|
41
|
+
},
|
|
42
|
+
/**
|
|
43
|
+
* Extracts just the value field from user's object
|
|
44
|
+
*/
|
|
45
|
+
extractValue: (item) => {
|
|
46
|
+
return item[valueField] ?? null;
|
|
47
|
+
},
|
|
48
|
+
/**
|
|
49
|
+
* Finds an item in array by matching value field
|
|
50
|
+
*/
|
|
51
|
+
findByValue: (items, value) => {
|
|
52
|
+
return items.find(item => item[valueField] === value);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Helper function to create field name mappings with less syntax.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* // Instead of:
|
|
61
|
+
* fieldNames={{ label: 'name', value: 'id' }}
|
|
62
|
+
*
|
|
63
|
+
* // You can write:
|
|
64
|
+
* fieldNames={mapFields('name', 'id')}
|
|
65
|
+
*/
|
|
66
|
+
export function mapFields(label, value, description, disabled) {
|
|
67
|
+
return {
|
|
68
|
+
label,
|
|
69
|
+
value,
|
|
70
|
+
...(description && { description }),
|
|
71
|
+
...(disabled && { disabled })
|
|
72
|
+
};
|
|
73
|
+
}
|