@bagelink/vue 1.0.38 → 1.0.43

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.
@@ -1,19 +1,18 @@
1
1
  <script setup lang="ts" generic="T extends {[key:string]:any}">
2
- import type { BglFormSchemaFnT, Field } from '@bagelink/vue'
2
+ import type { BglFormSchemaFnT, Field, BglFormSchemaT } from '@bagelink/vue'
3
3
  import type { VNode } from 'vue'
4
- import { onMounted, watch, ref } from 'vue'
4
+ import { Loading } from '@bagelink/vue'
5
+ import { onMounted, watch, ref, toRef, computed } from 'vue'
5
6
  import { useSchemaField } from '../../composables/useSchemaField'
6
7
  import { getNestedValue } from '../../utils'
7
8
 
8
- export interface BagelFormProps<_T> {
9
- modelValue?: _T
10
- schema?: BglFormSchemaFnT<_T>
9
+ const props = withDefaults(defineProps<{
10
+ modelValue?: T
11
+ schema?: BglFormSchemaFnT<T>
11
12
  tag?: 'form' | 'template'
12
13
  class?: string
13
- onSubmit?: (data: _T) => Promise<void> | void
14
- }
15
-
16
- const props = withDefaults(defineProps<BagelFormProps<T>>(), {
14
+ onSubmit?: (data: T) => Promise<void> | void
15
+ }>(), {
17
16
  modelValue: undefined,
18
17
  schema: undefined,
19
18
  tag: 'form',
@@ -24,131 +23,192 @@ const emit = defineEmits<{
24
23
  (e: 'update:modelValue', value: T): void
25
24
  }>()
26
25
 
27
- function safeClone<T>(obj: T): T {
28
- if (!obj) return obj
29
- try {
30
- return JSON.parse(JSON.stringify(obj))
31
- } catch (e) {
32
- console.warn('Failed to clone object:', e)
33
- return obj
34
- }
35
- }
26
+ // Clone helper
27
+ const clone = <T>(obj: T): T => !obj ? obj : JSON.parse(JSON.stringify(obj))
36
28
 
29
+ // Form state
37
30
  const form = ref<HTMLFormElement>()
38
- const formData = ref<T>(safeClone(props.modelValue ?? {}) as T)
39
- const initialFormData = ref<T>(safeClone(props.modelValue ?? {}) as T)
40
-
41
- onMounted(() => {
42
- if (props.modelValue) {
43
- initialFormData.value = safeClone(props.modelValue)
31
+ const formData = ref<T>(clone(props.modelValue ?? {}) as T)
32
+ const initialFormData = ref<T>(clone(props.modelValue ?? {}) as T)
33
+ const formState = ref<'success' | 'error' | 'idle' | 'submitting'>('idle')
34
+ const schemaState = ref<'loading' | 'loaded' | 'error'>('loaded')
35
+ const resolvedSchemaData = ref<BglFormSchemaT<T>>()
36
+ const schemaRef = toRef(props, 'schema')
37
+ const resolvedSchema = computed(() => resolvedSchemaData.value)
38
+ const isDirty = computed(() => {
39
+ try {
40
+ return JSON.stringify(formData.value) !== JSON.stringify(initialFormData.value)
41
+ } catch {
42
+ return false
44
43
  }
45
44
  })
46
45
 
47
- const formState = ref<'success' | 'error' | 'idle' | 'submitting'>('idle')
46
+ // Initialize on mount
47
+ onMounted(() => {
48
+ if (props.modelValue) initialFormData.value = clone(props.modelValue)
49
+ refreshSchema()
50
+ })
48
51
 
49
- watch(() => props.modelValue, (newValue) => {
50
- if (newValue !== undefined) {
51
- formData.value = safeClone(newValue)
52
- }
52
+ // Watch for model changes
53
+ watch(() => props.modelValue, (val) => {
54
+ if (val !== undefined) formData.value = clone(val)
53
55
  }, { immediate: true, deep: true })
54
56
 
55
- const resolvedSchema = $computed<BglFormSchemaFnT<T> | undefined>(() => {
56
- if (!props.schema) return undefined
57
- return typeof props.schema === 'function' ? props.schema() : props.schema
58
- })
57
+ // Schema resolution
58
+ async function resolveSchema(schema?: BglFormSchemaFnT<T>) {
59
+ if (!schema) {
60
+ resolvedSchemaData.value = undefined
61
+ schemaState.value = 'loaded'
62
+ return
63
+ }
59
64
 
60
- const isDirty = $computed(() => {
61
65
  try {
62
- const current = JSON.stringify(formData.value)
63
- const initial = JSON.stringify(initialFormData.value)
64
- return current !== initial
65
- } catch (e) {
66
- console.warn('Failed to compare form data:', e)
67
- return false
66
+ schemaState.value = 'loading'
67
+ const isPromise = (obj: any) => obj && typeof obj.then === 'function'
68
+
69
+ let result: any
70
+ if (typeof schema === 'function') {
71
+ result = schema()
72
+ result = isPromise(result) ? await result : result
73
+ } else {
74
+ result = isPromise(schema) ? await schema : schema
75
+ }
76
+
77
+ resolvedSchemaData.value = result
78
+ schemaState.value = 'loaded'
79
+ } catch (error) {
80
+ console.error('Schema error:', error)
81
+ schemaState.value = 'error'
82
+ resolvedSchemaData.value = undefined
68
83
  }
69
- })
84
+ }
85
+
86
+ // Public refresh method
87
+ async function refreshSchema() {
88
+ await resolveSchema(props.schema)
89
+ return resolvedSchemaData.value
90
+ }
91
+
92
+ // Watch schema changes
93
+ watch(schemaRef, resolveSchema, { immediate: true, deep: true })
70
94
 
95
+ // Update form data
71
96
  function updateFormData(fieldId: string, value: any) {
72
97
  const keys = fieldId.split('.')
73
- const newData = safeClone(formData.value) as Record<string, any>
98
+ const newData = clone(formData.value) as Record<string, any>
74
99
  let current = newData
75
100
 
76
- // Traverse the object, creating nested objects as needed
77
101
  for (let i = 0; i < keys.length - 1; i++) {
78
102
  const key = keys[i]
79
- // Initialize as empty object if current[key] is null/undefined or not an object
80
- if (!current[key] || typeof current[key] !== 'object') {
81
- current[key] = {}
82
- }
103
+ if (!current[key] || typeof current[key] !== 'object') current[key] = {}
83
104
  current = current[key]
84
105
  }
85
106
 
86
- // Set the value at the final key
87
107
  current[keys[keys.length - 1]] = value
88
108
  formData.value = newData as T
89
109
  emit('update:modelValue', formData.value)
90
110
  }
91
111
 
112
+ // Form submission
92
113
  async function handleSubmit() {
93
114
  try {
94
115
  if (formState.value === 'submitting') return
95
116
  formState.value = 'submitting'
96
117
  await props.onSubmit?.(formData.value)
97
- initialFormData.value = safeClone(formData.value)
118
+ initialFormData.value = clone(formData.value)
98
119
  formState.value = 'success'
99
- // Notify parent window of successful submission
100
120
  window.parent.postMessage({ type: 'BAGEL_FORM_SUCCESS', data: JSON.stringify(formData.value) }, '*')
101
121
  } catch (error) {
102
- console.error('Form submission error:', error)
122
+ console.error('Submit error:', error)
103
123
  formState.value = 'error'
104
124
  }
105
125
  }
106
126
 
107
- function validateForm() {
108
- if (!form.value) return false
109
- return form.value.reportValidity()
110
- }
127
+ // Form validation
128
+ const validateForm = () => form.value?.reportValidity() ?? false
111
129
 
130
+ // Field rendering
112
131
  const { renderField } = useSchemaField<T>({
113
132
  mode: 'form',
114
- getRowData: () => {
115
- return {
116
- ...formData.value,
117
- // Add a special getter to handle nested paths
118
- get: (path: string) => getNestedValue(formData.value, path, '')
119
- }
120
- },
133
+ getRowData: () => ({
134
+ ...formData.value,
135
+ get: (path: string) => getNestedValue(formData.value, path, '')
136
+ }),
121
137
  onUpdate: (field, value) => {
122
- if (!field.id) return
123
- updateFormData(field.id, value)
124
- field.onUpdate?.(value, formData.value)
138
+ if (field.id) {
139
+ updateFormData(field.id, value)
140
+ field.onUpdate?.(value, formData.value)
141
+ }
125
142
  }
126
143
  })
127
144
 
128
- function renderSchemaField(field: Field): VNode | null {
129
- return renderField(field)
130
- }
145
+ const renderSchemaField = (field: Field<T>): VNode | null => renderField(field)
131
146
 
132
- defineExpose({ form, isDirty, validateForm })
147
+ defineExpose({ form, isDirty, validateForm, resolveSchema, refreshSchema })
133
148
  </script>
134
149
 
135
150
  <template>
136
151
  <template v-if="formState !== 'success' || !$slots.success">
137
152
  <form v-if="props.tag === 'form'" ref="form" :class="props.class" @submit.prevent="handleSubmit">
138
- <template v-if="resolvedSchema">
139
- <template v-for="field in (resolvedSchema as Field[])" :key="field.id">
140
- <component :is="renderSchemaField(field)" />
141
- </template>
142
- </template>
153
+ <!-- Loading state -->
154
+ <slot v-if="schemaState === 'loading'" name="loading">
155
+ <div class="flex-center h-300px">
156
+ <Loading />
157
+ </div>
158
+ </slot>
159
+
160
+ <!-- Error state -->
161
+ <slot v-else-if="schemaState === 'error'" name="schema-error">
162
+ <div class="flex-center h-300px txt-red">
163
+ Error loading form
164
+ </div>
165
+ </slot>
166
+
167
+ <!-- Render fields -->
168
+ <component
169
+ :is="renderSchemaField(field)"
170
+ v-for="field in resolvedSchema"
171
+ v-else-if="resolvedSchema"
172
+ :key="field.id"
173
+ />
174
+
175
+ <!-- Default slot -->
143
176
  <slot v-else />
144
- <slot name="submit" :submit="handleSubmit" :isDirty="isDirty" :validateForm="validateForm" :formState="formState" />
177
+
178
+ <!-- Submit slot -->
179
+ <slot
180
+ name="submit"
181
+ :submit="handleSubmit"
182
+ :isDirty="isDirty"
183
+ :validateForm="validateForm"
184
+ :formState="formState"
185
+ :schemaState="schemaState"
186
+ />
145
187
  </form>
188
+
189
+ <!-- Template mode -->
146
190
  <template v-else>
147
- <template v-for="field in (resolvedSchema as Field[])" :key="field.id">
148
- <component :is="renderSchemaField(field)" :class="props.class" />
149
- </template>
191
+ <slot v-if="schemaState === 'loading'" name="loading">
192
+ <Loading />
193
+ </slot>
194
+
195
+ <slot v-else-if="schemaState === 'error'" name="schema-error">
196
+ <div class="flex-center h-300px txt-red">
197
+ Error loading form
198
+ </div>
199
+ </slot>
200
+
201
+ <component
202
+ :is="renderSchemaField(field)"
203
+ v-for="field in resolvedSchema"
204
+ v-else-if="resolvedSchema"
205
+ :key="field.id"
206
+ :class="props.class"
207
+ />
150
208
  </template>
151
209
  </template>
210
+
211
+ <!-- Success/error slots -->
152
212
  <slot v-if="formState === 'success'" name="success" />
153
213
  <slot v-if="formState === 'error'" name="error" />
154
214
  </template>
@@ -5,11 +5,10 @@ import type {
5
5
  Attributes,
6
6
  BagelFieldOptions,
7
7
  BglFormSchemaFnT,
8
- BglFormSchemaT,
9
8
  Field,
10
9
  } from '@bagelink/vue'
11
- import { BagelForm, Btn } from '@bagelink/vue'
12
- import { useSchemaField } from '../../composables/useSchemaField'
10
+ import { BagelForm, Btn, Loading } from '@bagelink/vue'
11
+ import { ref, onMounted } from 'vue'
13
12
 
14
13
  const props = withDefaults(
15
14
  defineProps<{
@@ -40,61 +39,82 @@ const props = withDefaults(
40
39
 
41
40
  const emit = defineEmits(['update:modelValue'])
42
41
 
43
- const minimizedItems = $ref<boolean[]>([])
42
+ // State
43
+ const minimizedItems = ref<boolean[]>([])
44
+ const internalData = ref<any[]>(props.modelValue || [])
45
+ const schemaState = ref<'loading' | 'loaded' | 'error'>('loaded')
46
+ const resolvedSchemaData = ref<any[]>([])
47
+
48
+ // Resolve schema (handles sync, async, function, and direct values)
49
+ async function resolveSchema() {
50
+ if (!props.schema) {
51
+ resolvedSchemaData.value = []
52
+ schemaState.value = 'loaded'
53
+ return
54
+ }
44
55
 
45
- const data = $ref<T[]>(props.modelValue || [])
56
+ try {
57
+ schemaState.value = 'loading'
58
+ const isPromise = (obj: any) => obj && typeof obj.then === 'function'
59
+
60
+ let result: any
61
+ if (typeof props.schema === 'function') {
62
+ result = props.schema()
63
+ result = isPromise(result) ? await result : result
64
+ } else {
65
+ result = isPromise(props.schema) ? await props.schema : props.schema
66
+ }
67
+
68
+ resolvedSchemaData.value = result
69
+ schemaState.value = 'loaded'
70
+ } catch (error) {
71
+ console.error('Schema error:', error)
72
+ schemaState.value = 'error'
73
+ resolvedSchemaData.value = []
74
+ }
75
+ }
76
+
77
+ // Initialize schema on mount
78
+ onMounted(() => {
79
+ resolveSchema()
80
+ })
46
81
 
82
+ // Event handlers
47
83
  function emitValue() {
48
- emit('update:modelValue', data)
84
+ emit('update:modelValue', internalData.value)
49
85
  }
50
86
 
51
87
  function deleteItem(i: number) {
52
- data.splice(i, 1)
88
+ internalData.value.splice(i, 1)
53
89
  emitValue()
54
90
  }
55
91
 
56
92
  function addItem() {
57
- data.push({} as any)
93
+ internalData.value.push({})
58
94
  emitValue()
59
95
  }
60
96
 
61
97
  function toggleMinimized(index: number) {
62
- minimizedItems[index] = !minimizedItems[index]
98
+ minimizedItems.value[index] = !minimizedItems.value[index]
63
99
  }
64
100
 
65
- const computedField = $computed(
66
- () => ({
67
- label: props.label,
68
- placeholder: props.placeholder,
69
- children: props.children,
70
- // class: props.class,
71
- attrs: props.attrs,
72
- required: props.required,
73
- disabled: props.disabled,
74
- helptext: props.helptext,
75
- options: props.options,
76
- defaultValue: props.defaultValue,
77
- transform: props.transform,
78
- $el: props.el,
79
- }) as Field<T>
80
- ) as Field<Record<string, any>>
81
-
82
- const resolvedSchema = $computed<BglFormSchemaT<T>>(() => {
83
- if (!props.schema) return [] as BglFormSchemaT<T>
84
- return typeof props.schema === 'function' ? props.schema() : props.schema
85
- })
101
+ function updateItem(index: number, value: any) {
102
+ internalData.value[index] = value
103
+ emitValue()
104
+ }
86
105
 
87
- const { renderField } = useSchemaField<Record<string, any>>({
88
- mode: 'form',
89
- getRowData: () => data,
90
- onUpdate: (field, value) => {
91
- if (!field.id) return
92
- const index = Number.parseInt(field.id)
93
- if (Number.isNaN(index)) return
94
- data[index] = value
95
- emitValue()
96
- }
97
- })
106
+ // // Field rendering
107
+ // const { renderField } = useSchemaField<any>({
108
+ // mode: 'form',
109
+ // getRowData: () => internalData.value,
110
+ // onUpdate: (field, value) => {
111
+ // if (!field.id) return
112
+ // const index = Number.parseInt(field.id)
113
+ // if (Number.isNaN(index)) return
114
+ // internalData.value[index] = value
115
+ // emitValue()
116
+ // }
117
+ // })
98
118
  </script>
99
119
 
100
120
  <template>
@@ -102,45 +122,60 @@ const { renderField } = useSchemaField<Record<string, any>>({
102
122
  <p class="label mb-05">
103
123
  {{ label }}
104
124
  </p>
105
- <div v-if="resolvedSchema" class="ps-025 border-start mb-05">
106
- <div
107
- v-for="(_, i) in data" :key="i" outline thin
108
- class="mb-05 itemBox transition ps-05 pb-025 pt-025 radius-05 gap-05 overflow-hidden"
109
- :class="{ minimized: minimizedItems[i] }"
110
- >
111
- <p v-if="minimizedItems[i]" class="minimizedText txt14 p-025 opacity-7">
112
- {{ label }} {{ i + 1 }}
113
- </p>
114
- <BagelForm v-else v-model="data[i]" :schema="resolvedSchema" @update:model-value="emitValue" />
115
- <div class="bg-gray-80 -my-05 px-025 pt-065 pb-05 txt-center space-between flex column">
116
- <Btn
117
- v-if="resolvedSchema.length > 4"
118
- class="block rotate-180 txt10 opacity-7 p-025"
119
- flat thin icon="keyboard_arrow_down"
120
- @click="toggleMinimized(i)"
121
- />
122
- <Btn
123
- v-if="props.delete"
124
- icon="delete"
125
- class="txt10 opacity-7"
126
- thin
127
- flat
128
- @click="deleteItem(i)"
125
+ <div v-if="resolvedSchemaData.length > 0" class="ps-025 border-start mb-05">
126
+ <!-- Loading state -->
127
+ <div v-if="schemaState === 'loading'" class="flex-center h-300px">
128
+ <Loading />
129
+ </div>
130
+
131
+ <!-- Error state -->
132
+ <div v-else-if="schemaState === 'error'" class="flex-center h-300px txt-red">
133
+ Error
134
+ </div>
135
+
136
+ <!-- Render items -->
137
+ <template v-else>
138
+ <div
139
+ v-for="(item, i) in internalData" :key="i" outline thin
140
+ class="mb-05 itemBox transition ps-05 pb-025 pt-025 radius-05 gap-05 overflow-hidden"
141
+ :class="{ minimized: minimizedItems[i] }"
142
+ >
143
+ <p v-if="minimizedItems[i]" class="minimizedText txt14 p-025 opacity-7">
144
+ {{ label }} {{ i + 1 }}
145
+ </p>
146
+ <BagelForm
147
+ v-else
148
+ :model-value="item"
149
+ :schema="resolvedSchemaData"
150
+ @update:model-value="val => updateItem(i, val)"
129
151
  />
152
+ <div class="bg-gray-80 -my-05 px-025 pt-065 pb-05 txt-center space-between flex column">
153
+ <Btn
154
+ v-if="resolvedSchemaData.length > 4"
155
+ class="block rotate-180 txt10 opacity-7 p-025"
156
+ flat thin icon="keyboard_arrow_down"
157
+ @click="toggleMinimized(i)"
158
+ />
159
+ <Btn
160
+ v-if="props.delete"
161
+ icon="delete"
162
+ class="txt10 opacity-7"
163
+ thin
164
+ flat
165
+ @click="deleteItem(i)"
166
+ />
167
+ </div>
130
168
  </div>
131
- </div>
132
- <Btn v-if="add" thin icon="add" color="gray" class="txt12" @click="addItem">
133
- <p>{{ label }}</p>
134
- </Btn>
169
+ <Btn v-if="add" thin icon="add" color="gray" class="txt12" @click="addItem">
170
+ <p>{{ label }}</p>
171
+ </Btn>
172
+ </template>
135
173
  </div>
136
174
 
137
175
  <template v-else>
138
- <component
139
- :is="renderField({ ...computedField, id: String(i) })"
140
- v-for="(_, i) in data"
141
- :key="i"
142
- @update:model-value="emitValue"
143
- />
176
+ <div v-for="(_, i) in internalData" :key="i">
177
+ <p>No schema available</p>
178
+ </div>
144
179
  </template>
145
180
  </div>
146
181
  </template>
@@ -10,13 +10,23 @@ interface useBglSchemaParamsT<T> {
10
10
  data?: any[]
11
11
  }
12
12
 
13
- export function useBglSchema<T = { [key: string]: unknown }>(
13
+ export async function useBglSchema<T = { [key: string]: unknown }>(
14
14
  { schema, columns, data }: useBglSchemaParamsT<T> = {}
15
- ): BglFormSchemaT<T> {
15
+ ): Promise<BglFormSchemaT<T>> {
16
16
  let _schema = schema
17
+
18
+ // Handle async schema functions
17
19
  if (typeof _schema === 'function') {
18
- _schema = _schema()
20
+ const result = _schema()
21
+ if (result instanceof Promise) {
22
+ _schema = await result
23
+ } else {
24
+ _schema = result
25
+ }
26
+ } else if (_schema instanceof Promise) {
27
+ _schema = await _schema
19
28
  }
29
+
20
30
  if (_schema) {
21
31
  return (
22
32
  columns && columns.length > 0
@@ -41,7 +51,7 @@ export function localRef<T>(
41
51
  localStorage.setItem(key, JSON.stringify(val))
42
52
  }, { immediate: true, deep: true })
43
53
 
44
- return value
54
+ return value as any
45
55
  }
46
56
 
47
57
  export const useLocalStorage = localRef
@@ -73,4 +73,4 @@ export type BglFieldT<T = { [key: string]: any }> = Field<T>
73
73
 
74
74
  export type BglFormSchemaT<T = { [key: string]: any }> = Field<T>[]
75
75
 
76
- export type BglFormSchemaFnT<T = { [key: string]: any }> = (() => BglFormSchemaT<T>) | BglFormSchemaT<T>
76
+ export type BglFormSchemaFnT<T = { [key: string]: any }> = (() => BglFormSchemaT<T>) | BglFormSchemaT<T> | (() => Promise<BglFormSchemaT<T>>) | Promise<BglFormSchemaT<T>>