diff --git a/docs/guide/components.md b/docs/guide/components.md index 68f887d..55e70ee 100644 --- a/docs/guide/components.md +++ b/docs/guide/components.md @@ -404,6 +404,104 @@ const formOptions = { --- +## KeyValueField + +Renders a dynamic key-value pair editor for record types. + +### Handles + +- `type: 'object'` with typed `additionalProperties` (e.g., `{ type: 'string' }`) but no defined `properties` + +### Example Schema + +**Basic record type:** +```typescript +{ + type: 'object', + title: 'Additional Parameters', + description: 'Dynamic key-value pairs', + additionalProperties: { + type: 'string' + } +} +``` + +**OAuth headers example:** +```typescript +{ + type: 'object', + title: 'Custom OAuth Headers', + description: 'Add custom headers for OAuth requests', + additionalProperties: { + type: 'string' + } +} +``` + +### Features + +- **Dynamic rows**: Add/remove key-value pairs +- **Type-safe**: Values are typed based on `additionalProperties.type` +- **Clean UI**: Grid layout with aligned inputs +- **Theme inheritance**: Inherits from parent Quasar theme (no hardcoded colors) +- **Validation**: Empty keys are automatically filtered out +- **Button customization**: Customize add/remove buttons via `x-quickforms-quasar` + +### UI Behavior + +- **Add button**: Creates a new empty key-value pair +- **Remove button**: Deletes a specific pair +- **Auto-cleanup**: Pairs with empty keys are not saved to form data + +### Rendering + +- **Plain Vue**: Standard HTML inputs with responsive grid layout +- **Quasar**: `QInput` components with Quasar styling and icons + +### Button Customization (Quasar) + +**Per-field customization:** +```typescript +{ + type: 'object', + title: 'Environment Variables', + additionalProperties: { type: 'string' }, + 'x-quickforms-quasar': { + addButton: { + label: 'Add Variable', + icon: 'add_circle', + color: 'secondary', + outline: false + }, + removeButton: { + icon: 'delete', + color: 'negative' + } + } +} +``` + +**Global defaults:** +```typescript +const formOptions = { + quickformsDefaults: { + keyvalue: { + addButton: { + label: 'Add Item', + icon: 'add', + color: 'primary' + }, + removeButton: { + icon: 'close', + color: 'negative' + } + } + } +} +``` + +--- + ## ArrayField Renders dynamic array fields with add/remove buttons. diff --git a/packages/core/package.json b/packages/core/package.json index 1a9d4b7..6b2a2db 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms", - "version": "1.2.0", + "version": "1.3.0", "description": "Framework-agnostic core for QuickForms - JSON Schema form generator", "type": "module", "main": "./dist/index.js", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9935af3..45107f7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,11 +20,12 @@ export { isNumberType, isIntegerType, isBooleanType, + isEnumType, isObjectType, isArrayType, - isNullType, - isEnumType, + isRecordType, isJsonType, + isNullType, hasConst, hasFormat, isEmailFormat, diff --git a/packages/core/src/testers.ts b/packages/core/src/testers.ts index 96152e0..7af241b 100644 --- a/packages/core/src/testers.ts +++ b/packages/core/src/testers.ts @@ -42,9 +42,23 @@ export const isArrayType = (schema: JSONSchema): boolean => { return schema.type === 'array'; }; +/** + * Record type tester - for dynamic key-value pairs + * Matches objects with typed additionalProperties (e.g., Record) + */ +export const isRecordType = (schema: JSONSchema): boolean => { + return ( + schema.type === 'object' && + schema.additionalProperties !== undefined && + typeof schema.additionalProperties === 'object' && + Object.keys(schema.additionalProperties).length > 0 && // Has typed additionalProperties + (!schema.properties || Object.keys(schema.properties).length === 0) + ); +}; + /** * JSON object tester - for freeform JSON editing - * Matches objects with additionalProperties but no defined properties, + * Matches objects with empty additionalProperties (freeform), * or any field with x-render: "jsoneditor" */ export const isJsonType = (schema: JSONSchema): boolean => { @@ -53,10 +67,12 @@ export const isJsonType = (schema: JSONSchema): boolean => { return true; } - // Automatic detection: object with additionalProperties but no defined properties + // Automatic detection: object with additionalProperties={} (empty, freeform) return ( schema.type === 'object' && schema.additionalProperties !== undefined && + typeof schema.additionalProperties === 'object' && + Object.keys(schema.additionalProperties).length === 0 && // Empty = freeform (!schema.properties || Object.keys(schema.properties).length === 0) ); }; diff --git a/packages/quasar/dev/App.vue b/packages/quasar/dev/App.vue index 6dcc21c..422f258 100644 --- a/packages/quasar/dev/App.vue +++ b/packages/quasar/dev/App.vue @@ -379,6 +379,26 @@ const schema: JSONSchema = { }, }, + // === KEY-VALUE EDITOR (RECORD TYPE) === + additionalParams: { + type: "object", + title: "Additional Parameters", + description: "Dynamic key-value pairs (Record)", + additionalProperties: { + type: "string", + }, + }, + + // === KEY-VALUE EDITOR (OAUTH EXAMPLE) === + oauthHeaders: { + type: "object", + title: "Custom OAuth Headers", + description: "Add custom headers for OAuth requests", + additionalProperties: { + type: "string", + }, + }, + // === URL FIELD === website: { type: "string", diff --git a/packages/quasar/package.json b/packages/quasar/package.json index 4661811..6dfa8cc 100644 --- a/packages/quasar/package.json +++ b/packages/quasar/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms-quasar", - "version": "1.2.0", + "version": "1.3.0", "description": "Quasar UI components for QuickForms - JSON Schema form generator", "type": "module", "main": "./dist/index.js", diff --git a/packages/quasar/src/components/QuasarKeyValueField.vue b/packages/quasar/src/components/QuasarKeyValueField.vue new file mode 100644 index 0000000..a5ea3b9 --- /dev/null +++ b/packages/quasar/src/components/QuasarKeyValueField.vue @@ -0,0 +1,185 @@ + + + diff --git a/packages/quasar/src/index.ts b/packages/quasar/src/index.ts index fef949d..cf74c2a 100644 --- a/packages/quasar/src/index.ts +++ b/packages/quasar/src/index.ts @@ -11,6 +11,7 @@ export { default as QuasarTimeField } from './components/QuasarTimeField.vue'; export { default as QuasarDateTimeField } from './components/QuasarDateTimeField.vue'; export { default as QuasarObjectField } from './components/QuasarObjectField.vue'; export { default as QuasarArrayField } from './components/QuasarArrayField.vue'; +export { default as QuasarKeyValueField } from './components/QuasarKeyValueField.vue'; export { default as QuasarJsonField } from './components/QuasarJsonField.vue'; export { default as QuasarMultiEnumField } from './components/QuasarMultiEnumField.vue'; export { default as QuasarOneOfField } from './components/QuasarOneOfField.vue'; @@ -52,6 +53,7 @@ export { isEnumType, isObjectType, isArrayType, + isRecordType, isJsonType, isNullType, // Format testers diff --git a/packages/quasar/src/registry.ts b/packages/quasar/src/registry.ts index f914c7a..6931292 100644 --- a/packages/quasar/src/registry.ts +++ b/packages/quasar/src/registry.ts @@ -10,6 +10,7 @@ import { isDateTimeFormat, isObjectType, isArrayType, + isRecordType, isJsonType, hasOneOf, hasAnyOf, @@ -26,6 +27,7 @@ import QuasarTimeField from './components/QuasarTimeField.vue'; import QuasarDateTimeField from './components/QuasarDateTimeField.vue'; import QuasarObjectField from './components/QuasarObjectField.vue'; import QuasarArrayField from './components/QuasarArrayField.vue'; +import QuasarKeyValueField from './components/QuasarKeyValueField.vue'; import QuasarJsonField from './components/QuasarJsonField.vue'; import QuasarMultiEnumField from './components/QuasarMultiEnumField.vue'; import QuasarOneOfField from './components/QuasarOneOfField.vue'; @@ -100,7 +102,12 @@ export function createQuasarRegistry(): ComponentRegistry { rankWith(3, isDateTimeFormat(schema)) ); - // Register JSON field (priority: 5, higher than object since it's more specific) + // Register key-value field (priority: 6, for record types with typed additionalProperties) + registry.register('keyvalue', QuasarKeyValueField, (schema) => + rankWith(6, isRecordType(schema)) + ); + + // Register JSON field (priority: 5, for freeform objects) registry.register('json', QuasarJsonField, (schema) => rankWith(5, isJsonType(schema)) ); diff --git a/packages/vue/dev/App.vue b/packages/vue/dev/App.vue index 75f4f6d..2b7e91c 100644 --- a/packages/vue/dev/App.vue +++ b/packages/vue/dev/App.vue @@ -243,6 +243,14 @@ const fullTestSchema: JSONSchema = { "x-render": "jsoneditor", "x-rows": 6, }, + environmentVars: { + type: "object", + title: "Environment Variables", + description: "Key-value pairs (Record)", + additionalProperties: { + type: "string", + }, + }, paymentMethod: { type: "object", title: "Payment Method", diff --git a/packages/vue/package.json b/packages/vue/package.json index 384cf49..962e240 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms-vue", - "version": "1.2.0", + "version": "1.3.0", "description": "Vue 3 bindings for QuickForms - JSON Schema form generator", "type": "module", "main": "./dist/index.js", diff --git a/packages/vue/src/components/DynamicForm.vue b/packages/vue/src/components/DynamicForm.vue index 639a8f1..3b73945 100644 --- a/packages/vue/src/components/DynamicForm.vue +++ b/packages/vue/src/components/DynamicForm.vue @@ -152,6 +152,11 @@ const onSubmit = handleSubmit((submittedValues) => { } }); +// Check if schema is a single field (not a form with multiple properties) +const isSingleField = computed(() => { + return props.schema.type === "object" && !props.schema.properties; +}); + // Get all top-level properties from schema const properties = computed(() => { if (props.schema.type !== "object" || !props.schema.properties) { @@ -168,7 +173,17 @@ const properties = computed(() => {