diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 6718ad3..058f22f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -53,7 +53,10 @@ export default defineConfig({ text: "API Reference", items: [ { text: "Form Options", link: "/guide/form-options" }, - { text: "Schema Extensions", link: "/guide/schema-extensions" }, + { + text: "Schema Extensions (x-*)", + link: "/guide/schema-extensions", + }, { text: "Components", link: "/guide/components" }, { text: "Composables", link: "/guide/composables" }, { text: "Testers & Registry", link: "/guide/testers-registry" }, diff --git a/docs/guide/components.md b/docs/guide/components.md index 542b862..68f887d 100644 --- a/docs/guide/components.md +++ b/docs/guide/components.md @@ -305,6 +305,105 @@ Renders nested object fields. --- +## JsonField + +Renders a JSON editor for freeform object configuration. + +### Handles + +- `type: 'object'` with `additionalProperties` but no defined `properties` +- Any field with `x-render: 'jsoneditor'` + +### Example Schema + +**Auto-detected (freeform object):** +```typescript +{ + type: 'object', + title: 'Custom Configuration', + description: 'Freeform JSON object', + additionalProperties: {} +} +``` + +**Explicit via x-render:** +```typescript +{ + type: 'object', + title: 'API Settings', + description: 'Configuration in JSON format', + 'x-render': 'jsoneditor', + 'x-rows': 10 // Control textarea height +} +``` + +**With custom props (Quasar):** +```typescript +{ + type: 'object', + title: 'Metadata', + 'x-render': 'jsoneditor', + 'x-rows': 8, + 'x-quasar-props': { + dense: false, + color: 'secondary' + }, + 'x-quickforms-quasar': { + prependIcon: 'settings', + iconColor: 'primary', + showFormatHint: false // Hide info icon + } +} +``` + +**Hide format hint (Vue):** +```typescript +{ + type: 'object', + 'x-render': 'jsoneditor', + 'x-show-format-hint': false // Hide info icon +} +``` + +### Features + +- **Tab indentation**: Press Tab to insert 2 spaces +- **Format shortcut**: Press Ctrl+Space to auto-format JSON +- **Real-time validation**: Shows parse errors as you type +- **Monospace font**: Better readability for JSON +- **Info icon**: Hover tooltip showing format shortcut (can be hidden) +- **No auto-formatting**: Only formats on initial load or manual trigger + +### Keyboard Shortcuts + +- **Ctrl+Space**: Format JSON with proper indentation +- **Tab**: Insert 2 spaces for indentation +- **Enter**: Insert new line (does not submit form) + +### Configuration + +**Global defaults (Quasar):** +```typescript +const formOptions = { + componentDefaults: { + jsoneditor: { + dense: true, + color: 'primary', + rows: 12 + } + }, + quickformsDefaults: { + jsoneditor: { + prependIcon: 'code', + iconColor: 'grey-7', + showFormatHint: true // Default: true + } + } +} +``` + +--- + ## ArrayField Renders dynamic array fields with add/remove buttons. diff --git a/docs/guide/schema-extensions.md b/docs/guide/schema-extensions.md index 5e9b99a..33f0f34 100644 --- a/docs/guide/schema-extensions.md +++ b/docs/guide/schema-extensions.md @@ -330,6 +330,82 @@ All `x-*` attributes are optional. QuickForms works perfectly with standard JSON --- +## `x-render` + +**Purpose:** Force a specific renderer for a field + +**Type:** `string` + +**Example:** +```typescript +{ + apiSettings: { + type: 'object', + title: 'API Settings', + 'x-render': 'jsoneditor' // Force JSON editor + } +} +``` + +**Available Renderers:** +- `'jsoneditor'` - JSON textarea editor with formatting support + +**Use Cases:** +- Force JSON editor for object fields that would normally render as nested fields +- Override automatic component selection + +**Related:** [JsonField Component](/guide/components#jsonfield) + +--- + +## `x-rows` + +**Purpose:** Control textarea height in rows + +**Type:** `number` + +**Default:** `8` (for JSON editor), varies by field type + +**Example:** +```typescript +{ + config: { + type: 'object', + 'x-render': 'jsoneditor', + 'x-rows': 12 // Taller editor + } +} +``` + +**Applies to:** +- `format: 'textarea'` - String textarea fields +- `x-render: 'jsoneditor'` - JSON editor fields + +--- + +## `x-show-format-hint` + +**Purpose:** Show/hide the format hint icon in JSON editor (Vue package) + +**Type:** `boolean` + +**Default:** `true` + +**Example:** +```typescript +{ + config: { + type: 'object', + 'x-render': 'jsoneditor', + 'x-show-format-hint': false // Hide the ⓘ icon + } +} +``` + +**Note:** For Quasar, use `x-quickforms-quasar: { showFormatHint: false }` instead. + +--- + ## Combining Extensions Multiple extensions can be used together: diff --git a/packages/core/package.json b/packages/core/package.json index 499214d..1a9d4b7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms", - "version": "1.1.0", + "version": "1.2.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 83c9547..9935af3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -24,6 +24,7 @@ export { isArrayType, isNullType, isEnumType, + isJsonType, hasConst, hasFormat, isEmailFormat, diff --git a/packages/core/src/testers.ts b/packages/core/src/testers.ts index 29e4c77..96152e0 100644 --- a/packages/core/src/testers.ts +++ b/packages/core/src/testers.ts @@ -42,6 +42,25 @@ export const isArrayType = (schema: JSONSchema): boolean => { return schema.type === 'array'; }; +/** + * JSON object tester - for freeform JSON editing + * Matches objects with additionalProperties but no defined properties, + * or any field with x-render: "jsoneditor" + */ +export const isJsonType = (schema: JSONSchema): boolean => { + // Explicit opt-in via x-render extension + if ((schema as any)['x-render'] === 'jsoneditor') { + return true; + } + + // Automatic detection: object with additionalProperties but no defined properties + return ( + schema.type === 'object' && + schema.additionalProperties !== undefined && + (!schema.properties || Object.keys(schema.properties).length === 0) + ); +}; + export const isNullType = (schema: JSONSchema): boolean => { return schema.type === 'null'; }; diff --git a/packages/quasar/dev/App.vue b/packages/quasar/dev/App.vue index f8d526f..6dcc21c 100644 --- a/packages/quasar/dev/App.vue +++ b/packages/quasar/dev/App.vue @@ -353,6 +353,32 @@ const schema: JSONSchema = { }, }, + // === JSON EDITOR (AUTO-DETECTED) === + customConfig: { + type: "object", + title: "Custom Configuration", + description: "Freeform JSON object (auto-detected via additionalProperties)", + additionalProperties: {}, + "x-rows": 8, + }, + + // === JSON EDITOR (EXPLICIT WITH CUSTOMIZATION) === + metadata: { + type: "object", + title: "Metadata", + description: "Explicit JSON editor via x-render extension with custom props", + "x-render": "jsoneditor", + "x-rows": 6, + "x-quasar-props": { + dense: false, + color: "secondary", + }, + "x-quickforms-quasar": { + prependIcon: "settings", + iconColor: "primary", + }, + }, + // === URL FIELD === website: { type: "string", diff --git a/packages/quasar/package.json b/packages/quasar/package.json index 44182ea..4661811 100644 --- a/packages/quasar/package.json +++ b/packages/quasar/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms-quasar", - "version": "1.1.0", + "version": "1.2.0", "description": "Quasar UI components for QuickForms - JSON Schema form generator", "type": "module", "main": "./dist/index.js", diff --git a/packages/quasar/src/components/QuasarJsonField.vue b/packages/quasar/src/components/QuasarJsonField.vue new file mode 100644 index 0000000..b315577 --- /dev/null +++ b/packages/quasar/src/components/QuasarJsonField.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/packages/quasar/src/index.ts b/packages/quasar/src/index.ts index 2e79867..fef949d 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 QuasarJsonField } from './components/QuasarJsonField.vue'; export { default as QuasarMultiEnumField } from './components/QuasarMultiEnumField.vue'; export { default as QuasarOneOfField } from './components/QuasarOneOfField.vue'; export { default as QuasarAllOfField } from './components/QuasarAllOfField.vue'; @@ -51,6 +52,7 @@ export { isEnumType, isObjectType, isArrayType, + isJsonType, isNullType, // Format testers isEmailFormat, diff --git a/packages/quasar/src/registry.ts b/packages/quasar/src/registry.ts index cba0490..f914c7a 100644 --- a/packages/quasar/src/registry.ts +++ b/packages/quasar/src/registry.ts @@ -10,6 +10,7 @@ import { isDateTimeFormat, isObjectType, isArrayType, + isJsonType, hasOneOf, hasAnyOf, hasAllOf, @@ -25,6 +26,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 QuasarJsonField from './components/QuasarJsonField.vue'; import QuasarMultiEnumField from './components/QuasarMultiEnumField.vue'; import QuasarOneOfField from './components/QuasarOneOfField.vue'; import QuasarAllOfField from './components/QuasarAllOfField.vue'; @@ -98,6 +100,11 @@ export function createQuasarRegistry(): ComponentRegistry { rankWith(3, isDateTimeFormat(schema)) ); + // Register JSON field (priority: 5, higher than object since it's more specific) + registry.register('json', QuasarJsonField, (schema) => + rankWith(5, isJsonType(schema)) + ); + // Register object field (priority: 1) registry.register('object', QuasarObjectField, (schema) => rankWith(1, isObjectType(schema)) diff --git a/packages/quasar/src/types.ts b/packages/quasar/src/types.ts index c727eb8..a23bfc4 100644 --- a/packages/quasar/src/types.ts +++ b/packages/quasar/src/types.ts @@ -116,6 +116,9 @@ export interface QuasarComponentDefaults extends VueComponentDefaults { /** QExpansionItem-specific defaults (for objects) */ expansion?: Partial; + + /** QInput textarea-specific defaults (for JSON editor) */ + jsoneditor?: Partial; } /** @@ -134,6 +137,11 @@ export interface QuickFormsQuasarDefaults { datetime?: QuickFormsQuasarFeatures; /** Array-specific QuickForms features */ array?: QuickFormsQuasarArrayFeatures; + /** JSON editor-specific QuickForms features */ + jsoneditor?: QuickFormsQuasarFeatures & { + /** Show the info icon with format shortcut hint. Default: true */ + showFormatHint?: boolean; + }; } /** diff --git a/packages/vue/dev/App.vue b/packages/vue/dev/App.vue index 8cab2e3..75f4f6d 100644 --- a/packages/vue/dev/App.vue +++ b/packages/vue/dev/App.vue @@ -230,6 +230,19 @@ const fullTestSchema: JSONSchema = { required: ["company", "position"], }, }, + customConfig: { + type: "object", + title: "Custom Configuration", + description: "Freeform JSON object (auto-detected via additionalProperties)", + additionalProperties: {}, + }, + apiSettings: { + type: "object", + title: "API Settings", + description: "Explicit JSON editor via x-render extension", + "x-render": "jsoneditor", + "x-rows": 6, + }, paymentMethod: { type: "object", title: "Payment Method", diff --git a/packages/vue/package.json b/packages/vue/package.json index 9ec573a..384cf49 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms-vue", - "version": "1.1.0", + "version": "1.2.0", "description": "Vue 3 bindings for QuickForms - JSON Schema form generator", "type": "module", "main": "./dist/index.js", diff --git a/packages/vue/src/components/JsonField.vue b/packages/vue/src/components/JsonField.vue new file mode 100644 index 0000000..d6940fd --- /dev/null +++ b/packages/vue/src/components/JsonField.vue @@ -0,0 +1,244 @@ + + +