Skip to content

Commit 625e1e1

Browse files
authored
Merge pull request #30 from QuickFlo/validation-aggressive-fix
fix validation in nested onof
2 parents 7f8a52b + 3875445 commit 625e1e1

8 files changed

Lines changed: 179 additions & 22 deletions

File tree

docs/guide/components.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,39 @@ const formOptions = {
573573

574574
Note: `componentDefaults.keyvalue` and `x-quasar-props` accept any native Quasar QInput props and apply them to both the key and value input fields.
575575

576+
### Type Inference
577+
578+
By default, all values are stored as strings. Enable `inferTypes` to automatically convert values to their appropriate types:
579+
580+
- `"123"``123` (number)
581+
- `"12.5"``12.5` (number)
582+
- `"true"` / `"false"``true` / `false` (boolean)
583+
- `"null"``null`
584+
- Template expressions like <code v-pre>{{ path }}</code> stay as strings (resolved at runtime)
585+
586+
**Via schema:**
587+
```typescript
588+
{
589+
type: 'object',
590+
title: 'Configuration',
591+
additionalProperties: { type: 'string' },
592+
'x-infer-types': true
593+
}
594+
```
595+
596+
**Via componentDefaults:**
597+
```typescript
598+
const options = {
599+
quickformsDefaults: {
600+
keyvalue: {
601+
inferTypes: true
602+
}
603+
}
604+
}
605+
```
606+
607+
This is useful when your backend expects typed values (numbers, booleans) rather than strings.
608+
576609
---
577610

578611
## JsonLogicBuilderField

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@quickflo/quickforms",
3-
"version": "1.14.2",
3+
"version": "1.14.8",
44
"description": "Framework-agnostic core for QuickForms - JSON Schema form generator",
55
"type": "module",
66
"main": "./dist/index.js",

packages/quasar/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@quickflo/quickforms-quasar",
3-
"version": "1.14.2",
3+
"version": "1.14.8",
44
"description": "Quasar UI components for QuickForms - JSON Schema form generator",
55
"type": "module",
66
"main": "./dist/index.js",

packages/quasar/src/components/QuasarObjectField.vue

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ const showOptionalIndicator = computed(() => {
5252
return objectDefaults?.showOptionalIndicator ?? true;
5353
});
5454
55+
// Section border style
56+
const sectionStyle = computed(() => {
57+
const objectDefaults = formContext?.componentDefaults?.object;
58+
return objectDefaults?.sectionStyle ?? 'solid';
59+
});
60+
61+
// Computed class for section style
62+
const sectionStyleClass = computed(() => {
63+
return `quickform-section-${sectionStyle.value}`;
64+
});
65+
5566
const quasarProps = computed(() => {
5667
const xQuasarProps = (props.schema as any)["x-quasar-props"] || {};
5768
const xComponentProps = (props.schema as any)["x-component-props"] || {};
@@ -72,29 +83,27 @@ const properties = computed(() => {
7283
</script>
7384

7485
<template>
75-
<div :style="{ marginBottom: fieldGap }">
86+
<div :style="{ marginBottom: fieldGap }" class="quickform-object-field" :class="sectionStyleClass">
7687
<QExpansionItem
7788
:id="fieldId"
7889
:label="label"
7990
:caption="hint"
8091
:default-opened="defaultOpened"
92+
header-class="quickform-object-header"
93+
expand-icon-class="text-grey-7"
8194
v-bind="quasarProps"
8295
>
8396
<template #header>
84-
<div class="text-subtitle1">
97+
<div class="quickform-object-header-content">
8598
{{ label }}
86-
<span v-if="required" style="color: red; margin-left: 0.25rem"
87-
>*</span
88-
>
89-
<span
90-
v-if="!required && showOptionalIndicator"
91-
style="color: #888; font-size: 0.75rem; margin-left: 0.5rem"
92-
>(optional)</span
93-
>
99+
<span v-if="required" class="quickform-required-indicator">*</span>
100+
<span v-if="!required && showOptionalIndicator" class="quickform-optional-indicator">
101+
(optional)
102+
</span>
94103
</div>
95104
</template>
96105

97-
<div style="padding: 1rem">
106+
<div class="quickform-object-content">
98107
<FieldRenderer
99108
v-for="prop in properties"
100109
:key="prop.key"
@@ -104,13 +113,70 @@ const properties = computed(() => {
104113
:readonly="readonly"
105114
/>
106115

107-
<div
108-
v-if="errorMessage"
109-
style="color: red; font-size: 0.875rem; margin-top: 0.5rem"
110-
>
116+
<div v-if="errorMessage" class="quickform-error-message">
111117
{{ errorMessage }}
112118
</div>
113119
</div>
114120
</QExpansionItem>
115121
</div>
116122
</template>
123+
124+
<style scoped>
125+
.quickform-object-field {
126+
border-radius: 4px;
127+
overflow: hidden;
128+
}
129+
130+
.quickform-object-field :deep(.quickform-object-header) {
131+
background-color: #f5f5f5;
132+
border-bottom: 1px solid #e0e0e0;
133+
font-weight: 500;
134+
}
135+
136+
/* Section style: solid (default) */
137+
.quickform-object-field.quickform-section-solid :deep(.q-expansion-item__content) {
138+
border-left: 3px solid #e0e0e0;
139+
margin-left: 8px;
140+
}
141+
142+
/* Section style: dashed */
143+
.quickform-object-field.quickform-section-dashed :deep(.q-expansion-item__content) {
144+
border-left: 2px dashed #ccc;
145+
margin-left: 8px;
146+
}
147+
148+
/* Section style: none */
149+
.quickform-object-field.quickform-section-none :deep(.q-expansion-item__content) {
150+
border-left: none;
151+
margin-left: 0;
152+
}
153+
154+
.quickform-object-header-content {
155+
font-size: 0.95rem;
156+
font-weight: 500;
157+
color: #333;
158+
}
159+
160+
.quickform-required-indicator {
161+
color: #c10015;
162+
margin-left: 0.25rem;
163+
}
164+
165+
.quickform-optional-indicator {
166+
color: #888;
167+
font-size: 0.75rem;
168+
font-weight: 400;
169+
margin-left: 0.5rem;
170+
}
171+
172+
.quickform-object-content {
173+
padding: 1rem;
174+
padding-left: 1.25rem;
175+
}
176+
177+
.quickform-error-message {
178+
color: #c10015;
179+
font-size: 0.875rem;
180+
margin-top: 0.5rem;
181+
}
182+
</style>

packages/quasar/src/components/QuasarOneOfField.vue

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const props = withDefaults(defineProps<FieldProps>(), {
2121
2222
const {
2323
value,
24+
setValue,
2425
label,
2526
hint,
2627
errorMessage,
@@ -44,16 +45,42 @@ const options = computed(() => props.schema.oneOf || props.schema.anyOf || []);
4445
// Selected option index
4546
const selectedIndex = ref(0);
4647
47-
// Try to determine initial selection based on data
48+
// Try to determine initial selection based on data and initialize defaults
4849
onMounted(() => {
49-
if (value.value) {
50+
let initialIndex = 0;
51+
52+
if (value.value && typeof value.value === 'object' && Object.keys(value.value).length > 0) {
53+
// Find matching schema based on existing data
5054
const index = options.value.findIndex((optionSchema) => {
5155
const result = schemaUtils.validate(optionSchema, value.value);
5256
return result.valid;
5357
});
5458
5559
if (index !== -1) {
56-
selectedIndex.value = index;
60+
initialIndex = index;
61+
}
62+
}
63+
64+
selectedIndex.value = initialIndex;
65+
66+
// Initialize default values for the selected schema
67+
// This ensures fields exist even if the form starts empty
68+
const initialSchema = options.value[initialIndex];
69+
if (initialSchema && initialSchema.properties) {
70+
const currentValue = (value.value && typeof value.value === 'object') ? value.value : {};
71+
const defaults = schemaUtils.getDefaultValue(initialSchema);
72+
73+
// Only proceed if defaults is a valid object
74+
if (defaults && typeof defaults === 'object') {
75+
// Merge defaults with current value
76+
const merged = { ...defaults, ...currentValue };
77+
78+
// Only update if we're adding new fields (don't overwrite existing)
79+
const hasNewFields = Object.keys(defaults).some(key => !(key in currentValue));
80+
if (hasNewFields && setValue) {
81+
// Pass false to skip validation during initialization
82+
setValue(merged, false);
83+
}
5784
}
5885
}
5986
});
@@ -177,7 +204,25 @@ const filterFn = (val: string, update: (fn: () => void) => void) => {
177204
// Handle manual switch
178205
const handleOptionChange = (newIndex: number) => {
179206
selectedIndex.value = newIndex;
180-
// Keep existing value to allow common fields to persist
207+
208+
// Initialize default values for the new schema's fields
209+
// This ensures fields exist even if user never touches them (e.g., empty string for 'value')
210+
const newSchema = options.value[newIndex];
211+
if (newSchema && newSchema.properties) {
212+
const currentValue = (value.value && typeof value.value === 'object') ? value.value : {};
213+
const defaults = schemaUtils.getDefaultValue(newSchema);
214+
215+
// Only proceed if defaults is a valid object
216+
if (defaults && typeof defaults === 'object') {
217+
// Merge defaults with current value, preferring current values for common fields
218+
const merged = { ...defaults, ...currentValue };
219+
220+
// Update form value - skip validation during option switch
221+
if (setValue) {
222+
setValue(merged, false);
223+
}
224+
}
225+
}
181226
};
182227
183228
</script>

packages/quasar/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@ interface VueComponentDefaults {
5151
defaultExpanded?: "required-only" | "all" | "none";
5252
/** Show "(optional)" indicator for optional object fields. Default: true */
5353
showOptionalIndicator?: boolean;
54+
/**
55+
* Visual style for object section borders.
56+
* 'solid' = solid left border (default)
57+
* 'dashed' = dashed left border
58+
* 'none' = no border
59+
*/
60+
sectionStyle?: "solid" | "dashed" | "none";
5461
};
5562
number?: {
5663
prefix?: string;

packages/vue/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@quickflo/quickforms-vue",
3-
"version": "1.14.2",
3+
"version": "1.14.8",
44
"description": "Vue 3 bindings for QuickForms - JSON Schema form generator",
55
"type": "module",
66
"main": "./dist/index.js",

packages/vue/src/composables/useFormField.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,12 @@ export function useFormField(
276276
}
277277
);
278278

279+
// Initialize string fields to empty string if undefined
280+
// This ensures empty text fields are saved as "" not undefined/missing
281+
if (schema.type === "string" && value.value === undefined) {
282+
setValue("", false); // Skip validation during initialization
283+
}
284+
279285
// Computed label from override or schema
280286
const label = computed(() => {
281287
if (options.label !== undefined) return options.label;

0 commit comments

Comments
 (0)