Skip to content

Commit 7f8a52b

Browse files
authored
Merge pull request #29 from QuickFlo/infer-types
infer types in keyvalue
2 parents 0ab44d1 + b82b56c commit 7f8a52b

9 files changed

Lines changed: 138 additions & 11 deletions

File tree

docs/guide/components.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,9 +658,9 @@ const options = {
658658
By default, variable references like `user.status` are converted to JSONLogic's `{ "var": "user.status" }` format. If your application uses a template engine (like Handlebars), you can enable **template syntax mode** to preserve `{{ }}` expressions as strings.
659659

660660
**When enabled:**
661-
- Values like `{{ user.status }}` are kept as strings in the JSONLogic output
661+
- Values like <code v-pre>{{ user.status }}</code> are kept as strings in the JSONLogic output
662662
- Your application is responsible for resolving templates before JSONLogic evaluation
663-
- Existing `{ "var": ... }` values are displayed as `{{ ... }}` in the UI for backwards compatibility
663+
- Existing `{ "var": ... }` values are displayed as <code v-pre>{{ ... }}</code> in the UI for backwards compatibility
664664

665665
**Via schema:**
666666
```typescript
@@ -687,11 +687,11 @@ const options = {
687687
| Mode | User types | JSONLogic output |
688688
|------|------------|------------------|
689689
| Default (`false`) | `user.status` | `{ "var": "user.status" }` |
690-
| Template (`true`) | `{{ user.status }}` | `"{{ user.status }}"` |
690+
| Template (`true`) | <code v-pre>{{ user.status }}</code> | <code v-pre>"{{ user.status }}"</code> |
691691

692692
**Loading existing data:**
693693

694-
When `useTemplateSyntax: true`, the builder automatically converts existing `{ "var": ... }` definitions to `{{ ... }}` format for display, ensuring backwards compatibility.
694+
When `useTemplateSyntax: true`, the builder automatically converts existing `{ "var": ... }` definitions to <code v-pre>{{ ... }}</code> format for display, ensuring backwards compatibility.
695695

696696
### Supported Operators
697697

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.0",
3+
"version": "1.14.2",
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.0",
3+
"version": "1.14.2",
44
"description": "Quasar UI components for QuickForms - JSON Schema form generator",
55
"type": "module",
66
"main": "./dist/index.js",

packages/quasar/src/components/QuasarKeyValueField.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ref, watch, computed } from "vue";
33
import { QInput, QBtn, QIcon } from "quasar";
44
import type { FieldProps } from "@quickflo/quickforms-vue";
55
import { useQuasarFormField } from "../composables/useQuasarFormField";
6+
import { inferType } from "../utils/type-inference";
67
78
const props = withDefaults(defineProps<FieldProps>(), {
89
disabled: false,
@@ -45,6 +46,13 @@ const quickformsFeatures = computed(() => {
4546
const valueLabel =
4647
schemaFeatures.valueLabel ?? globalDefaults.valueLabel ?? "Value";
4748
49+
// Type inference option (x-infer-types takes priority over defaults)
50+
const inferTypes =
51+
(props.schema as any)["x-infer-types"] ??
52+
schemaFeatures.inferTypes ??
53+
globalDefaults.inferTypes ??
54+
false;
55+
4856
// Merge QBtn props: defaults -> global -> schema (schema has highest priority)
4957
const addButtonDefaults = {
5058
outline: true,
@@ -82,6 +90,7 @@ const quickformsFeatures = computed(() => {
8290
showHeaders,
8391
keyLabel,
8492
valueLabel,
93+
inferTypes,
8594
};
8695
});
8796
@@ -138,10 +147,13 @@ watch(
138147
watch(
139148
pairs,
140149
(newPairs) => {
141-
const obj: Record<string, string> = {};
150+
const obj: Record<string, unknown> = {};
142151
newPairs.forEach((pair) => {
143152
if (pair.key.trim()) {
144-
obj[pair.key] = pair.value;
153+
// Optionally infer type from string value (e.g., "1" -> 1, "true" -> true)
154+
obj[pair.key] = quickformsFeatures.value.inferTypes
155+
? inferType(pair.value)
156+
: pair.value;
145157
}
146158
});
147159
isInternalUpdate.value = true;

packages/quasar/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ export {
5050
getOperatorInfo,
5151
generateConditionId,
5252
} from './utils/jsonlogic.js';
53+
54+
// Type inference utilities
55+
export { inferType, toDisplayString } from './utils/type-inference.js';
5356
export type {
5457
ComparisonOperator,
5558
OperatorInfo,

packages/quasar/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ export interface QuickFormsQuasarKeyValueFeatures {
134134
keyLabel?: string;
135135
/** Custom label for the value column. Default: 'Value' */
136136
valueLabel?: string;
137+
/**
138+
* Automatically infer types from string values.
139+
* When enabled:
140+
* - "123" becomes 123 (number)
141+
* - "true"/"false" become booleans
142+
* - "null" becomes null
143+
* - Template expressions ({{ }}) stay as strings
144+
*
145+
* Use via x-infer-types in schema or quickformsDefaults.keyvalue.inferTypes
146+
* Default: false (values stay as strings for backwards compatibility)
147+
*/
148+
inferTypes?: boolean;
137149
}
138150

139151
/**

packages/quasar/src/utils/jsonlogic.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,16 @@ function simpleConditionToJsonLogic(cond: SimpleCondition, options: ToJsonLogicO
194194
case '<=':
195195
return { '<=': [left, right] }
196196
case 'in':
197-
// Right should be an array
198-
return { in: [left, parseArrayValue(cond.right, options.useTemplateSyntax)] }
197+
// In template mode, right side could be a template resolving to an array at runtime
198+
// In non-template mode, parse as comma-separated literal values
199+
return {
200+
in: [
201+
left,
202+
options.useTemplateSyntax
203+
? parseValue(cond.right, true)
204+
: parseArrayValue(cond.right, false),
205+
],
206+
}
199207
case 'contains':
200208
// JSONLogic "in" with string checks if substring exists
201209
return { in: [right, left] }
@@ -448,6 +456,15 @@ function extractValue(val: unknown, useTemplateSyntax = false): string {
448456
}
449457

450458
if (Array.isArray(val)) {
459+
// Backwards compatibility: if template syntax is enabled and we have a single-element
460+
// array containing a template expression, unwrap it for display
461+
// (This handles data saved before the fix that stored templates as ["{{path}}"])
462+
if (useTemplateSyntax && val.length === 1 && typeof val[0] === 'string') {
463+
const item = val[0]
464+
if (item.startsWith('{{') && item.endsWith('}}')) {
465+
return item
466+
}
467+
}
451468
return JSON.stringify(val)
452469
}
453470

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Type inference utilities for form values
3+
*
4+
* When users type values in text inputs, they enter strings. This utility
5+
* infers the intended type and converts appropriately:
6+
* - "123" -> 123 (number)
7+
* - "true" / "false" -> boolean
8+
* - "null" -> null
9+
* - Template expressions like "{{path}}" stay as strings (resolved at runtime)
10+
* - Everything else stays as string
11+
*/
12+
13+
/**
14+
* Infer and convert a string value to its intended type
15+
*
16+
* @param value The string value from a text input
17+
* @returns The value converted to its inferred type
18+
*
19+
* @example
20+
* inferType("123") // => 123 (number)
21+
* inferType("12.5") // => 12.5 (number)
22+
* inferType("true") // => true (boolean)
23+
* inferType("false") // => false (boolean)
24+
* inferType("null") // => null
25+
* inferType("hello") // => "hello" (string)
26+
* inferType("{{x}}") // => "{{x}}" (string - template, resolved at runtime)
27+
* inferType("") // => "" (empty string)
28+
*/
29+
export function inferType(value: string): unknown {
30+
// Handle empty string
31+
if (value === '') {
32+
return ''
33+
}
34+
35+
// Don't convert template expressions - they're resolved at runtime
36+
if (value.includes('{{') && value.includes('}}')) {
37+
return value
38+
}
39+
40+
// Handle boolean literals (case-insensitive)
41+
const lower = value.toLowerCase()
42+
if (lower === 'true') {
43+
return true
44+
}
45+
if (lower === 'false') {
46+
return false
47+
}
48+
49+
// Handle null literal
50+
if (lower === 'null') {
51+
return null
52+
}
53+
54+
// Try to parse as number
55+
const trimmed = value.trim()
56+
if (trimmed !== '') {
57+
const num = Number(trimmed)
58+
// Only convert if it's a valid number AND the string representation matches
59+
// This prevents "123abc" from being parsed as 123
60+
if (!isNaN(num) && (trimmed === String(num) || trimmed === num.toString())) {
61+
return num
62+
}
63+
}
64+
65+
// Default: return as string
66+
return value
67+
}
68+
69+
/**
70+
* Convert a typed value back to string for display in a text input
71+
*
72+
* @param value The typed value
73+
* @returns String representation for display
74+
*/
75+
export function toDisplayString(value: unknown): string {
76+
if (value === null) {
77+
return 'null'
78+
}
79+
if (value === undefined) {
80+
return ''
81+
}
82+
return String(value)
83+
}

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.0",
3+
"version": "1.14.2",
44
"description": "Vue 3 bindings for QuickForms - JSON Schema form generator",
55
"type": "module",
66
"main": "./dist/index.js",

0 commit comments

Comments
 (0)