Skip to content

Commit 4549d48

Browse files
committed
```
feat: add x-visible-when and x-readonly-when conditional field extensions - Add x-visible-when schema extension for conditional field visibility - Add x-readonly-when schema extension for conditional readonly state - Support 12 operators: eq, neq, in, notIn, gt, gte, lt, lte, truthy, falsy, like, ilike - Enable nested field references using dot notation (e.g., "parent.child") - Update FieldRenderer.vue with reactive condition evaluation - Add comprehensive documentation and examples for conditional fields - Include demo implementation in Quasar dev app ```
1 parent d8b273c commit 4549d48

8 files changed

Lines changed: 563 additions & 16 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ coverage
88
.vscode
99
.idea
1010
*.tsbuildinfo
11-
WARP.MD
11+
WARP.MD
12+
.envrc

docs/guide/examples/conditional-fields.md

Lines changed: 167 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,154 @@
11
# Conditional Fields Example
22

3-
Create dynamic forms where fields appear based on other field values using `oneOf`.
3+
Create dynamic forms where fields appear based on other field values.
44

5-
## Simple Conditional Example
5+
## Two Approaches
6+
7+
QuickForms offers two approaches for conditional fields:
8+
9+
| Approach | Best For |
10+
|----------|----------|
11+
| **`x-visible-when`** | Simple show/hide based on another field's value |
12+
| **`oneOf`/`anyOf`** | Complex scenarios with different validation rules per variant |
13+
14+
## Using `x-visible-when` (Recommended for Simple Cases)
15+
16+
For basic "show field X when field Y equals Z" scenarios, use `x-visible-when`:
17+
18+
```vue
19+
<script setup lang="ts">
20+
import { ref } from 'vue'
21+
import { DynamicForm } from '@quickflo/quickforms-vue'
22+
import type { JSONSchema } from '@quickflo/quickforms'
23+
24+
const schema: JSONSchema = {
25+
type: 'object',
26+
properties: {
27+
provider: {
28+
type: 'string',
29+
title: 'Cloud Provider',
30+
enum: ['aws', 'gcp', 'azure']
31+
},
32+
// Only visible when AWS is selected
33+
awsRegion: {
34+
type: 'string',
35+
title: 'AWS Region',
36+
enum: ['us-east-1', 'us-west-2', 'eu-west-1'],
37+
'x-visible-when': {
38+
field: 'provider',
39+
operator: 'eq',
40+
value: 'aws'
41+
}
42+
},
43+
// Only visible when GCP is selected
44+
gcpProject: {
45+
type: 'string',
46+
title: 'GCP Project ID',
47+
'x-visible-when': {
48+
field: 'provider',
49+
operator: 'eq',
50+
value: 'gcp'
51+
}
52+
},
53+
// Visible for multiple providers (AWS or GCP)
54+
enableEncryption: {
55+
type: 'boolean',
56+
title: 'Enable Encryption',
57+
'x-visible-when': {
58+
field: 'provider',
59+
operator: 'in',
60+
value: ['aws', 'gcp']
61+
}
62+
}
63+
}
64+
}
65+
66+
const formData = ref({ provider: 'aws' })
67+
</script>
68+
69+
<template>
70+
<DynamicForm :schema="schema" v-model="formData" />
71+
</template>
72+
```
73+
74+
### Supported Operators
75+
76+
| Operator | Aliases | Description | Example |
77+
|----------|---------|-------------|---------|
78+
| `eq` | `==`, `===` | Equals | `{ operator: 'eq', value: 'aws' }` |
79+
| `neq` | `!=`, `!==` | Not equals | `{ operator: 'neq', value: 'azure' }` |
80+
| `in` | | Value in array | `{ operator: 'in', value: ['aws', 'gcp'] }` |
81+
| `notIn` | `!in` | Value not in array | `{ operator: 'notIn', value: ['azure'] }` |
82+
| `truthy` | | Value is truthy | `{ operator: 'truthy' }` |
83+
| `falsy` | | Value is falsy | `{ operator: 'falsy' }` |
84+
| `gt` | `>` | Greater than | `{ operator: 'gt', value: 18 }` |
85+
| `gte` | `>=` | Greater than or equal | `{ operator: 'gte', value: 18 }` |
86+
| `lt` | `<` | Less than | `{ operator: 'lt', value: 100 }` |
87+
| `lte` | `<=` | Less than or equal | `{ operator: 'lte', value: 100 }` |
88+
| `like` | | Case-sensitive pattern | `{ operator: 'like', value: 'cloud-%' }` |
89+
| `ilike` | | Case-insensitive pattern | `{ operator: 'ilike', value: 'CLOUD-%' }` |
90+
91+
### Nested Field References
92+
93+
Reference fields in nested objects using dot notation:
94+
95+
```typescript
96+
{
97+
connectionConfig: {
98+
type: 'object',
99+
oneOf: [
100+
{ properties: { provider: { const: 'aws' } } },
101+
{ properties: { provider: { const: 'gcp' } } }
102+
]
103+
},
104+
// Reference nested field with dot notation
105+
awsSpecificOption: {
106+
type: 'string',
107+
'x-visible-when': {
108+
field: 'connectionConfig.provider',
109+
operator: 'eq',
110+
value: 'aws'
111+
}
112+
}
113+
}
114+
```
115+
116+
### Also: `x-readonly-when`
117+
118+
Make fields conditionally read-only using the same syntax:
119+
120+
```typescript
121+
{
122+
status: {
123+
type: 'string',
124+
enum: ['draft', 'published']
125+
},
126+
title: {
127+
type: 'string',
128+
// Readonly once published
129+
'x-readonly-when': {
130+
field: 'status',
131+
operator: 'eq',
132+
value: 'published'
133+
}
134+
}
135+
}
136+
```
137+
138+
**See:** [Schema Extensions - x-visible-when](/guide/schema-extensions#x-visible-when) for full documentation.
139+
140+
---
141+
142+
## Using `oneOf` (Advanced Approach)
143+
144+
For complex scenarios where you need:
145+
- Different validation rules per variant
146+
- Entirely different field sets with per-variant `required` arrays
147+
- Type-safe discriminated unions
148+
149+
Use `oneOf`:
150+
151+
## oneOf Example
6152

7153
```vue
8154
<script setup lang="ts">
@@ -362,8 +508,25 @@ const schema: JSONSchema = {
362508
}
363509
```
364510

511+
## Choosing the Right Approach
512+
513+
| Scenario | Recommended Approach |
514+
|----------|---------------------|
515+
| Show/hide a few fields based on a selection | `x-visible-when` |
516+
| Different required fields per variant | `oneOf` |
517+
| Same fields, just conditional visibility | `x-visible-when` |
518+
| Completely different form sections | `oneOf` |
519+
| Make fields readonly based on status | `x-readonly-when` |
520+
| Need tabs/dropdown UI for variant selection | `oneOf` with `x-oneof-style` |
521+
365522
## Tips
366523

524+
### For `x-visible-when`
525+
1. **Keep it simple**: Best for 1-3 conditional fields
526+
2. **Dot notation**: Reference nested fields with `parent.child` paths
527+
3. **Combine with `x-readonly-when`**: Show a field but make it readonly based on conditions
528+
529+
### For `oneOf`
367530
1. **Clear Labels**: Use descriptive `title` in each `oneOf` schema
368531
2. **Default Values**: Set a default for the discriminator field
369532
3. **Enum Labels**: Use `x-enum-labels` for better UX
@@ -372,6 +535,6 @@ const schema: JSONSchema = {
372535

373536
## Next Steps
374537

375-
- [Custom Validation](/guide/guide/examples/custom-validation) - Add custom validation logic
538+
- [Schema Extensions - x-visible-when](/guide/schema-extensions#x-visible-when) - Full operator reference
539+
- [Custom Validation](/guide/examples/custom-validation) - Add custom validation logic
376540
- [Complex Types](/guide/complex-types) - More about oneOf, anyOf, allOf
377-
- [Schema Extensions](/guide/schema-extensions) - Custom schema properties

docs/guide/schema-extensions.md

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,150 @@ QuickForms extends JSON Schema with custom `x-*` attributes to provide escape ha
66
All `x-*` attributes are optional. QuickForms works perfectly with standard JSON Schema—use extensions only when you need them.
77
:::
88

9+
## `x-visible-when`
10+
11+
**Purpose:** Conditionally show/hide a field based on another field's value
12+
13+
**Type:** `{ field: string, operator: string, value?: any }`
14+
15+
**Example:**
16+
```typescript
17+
{
18+
provider: {
19+
type: 'string',
20+
enum: ['aws', 'gcp', 'azure']
21+
},
22+
// Only visible when AWS is selected
23+
awsRegion: {
24+
type: 'string',
25+
title: 'AWS Region',
26+
enum: ['us-east-1', 'us-west-2', 'eu-west-1'],
27+
'x-visible-when': {
28+
field: 'provider',
29+
operator: 'eq',
30+
value: 'aws'
31+
}
32+
},
33+
// Visible for multiple providers
34+
enableEncryption: {
35+
type: 'boolean',
36+
title: 'Enable Encryption',
37+
'x-visible-when': {
38+
field: 'provider',
39+
operator: 'in',
40+
value: ['aws', 'gcp']
41+
}
42+
}
43+
}
44+
```
45+
46+
**Supported Operators:**
47+
48+
| Operator | Aliases | Description |
49+
|----------|---------|-------------|
50+
| `eq` | `==`, `===` | Equals |
51+
| `neq` | `!=`, `!==` | Not equals |
52+
| `in` | | Value is in array |
53+
| `notIn` | `!in` | Value is not in array |
54+
| `gt` | `>` | Greater than (numbers) |
55+
| `gte` | `>=` | Greater than or equal (numbers) |
56+
| `lt` | `<` | Less than (numbers) |
57+
| `lte` | `<=` | Less than or equal (numbers) |
58+
| `truthy` | | Value is truthy (no `value` needed) |
59+
| `falsy` | | Value is falsy (no `value` needed) |
60+
| `like` | | Case-sensitive pattern match (`%` = any chars, `_` = single char) |
61+
| `ilike` | | Case-insensitive pattern match |
62+
63+
**Nested Field Paths:**
64+
65+
You can reference fields nested within objects using dot notation:
66+
67+
```typescript
68+
{
69+
connectionConfig: {
70+
type: 'object',
71+
oneOf: [
72+
{ properties: { provider: { const: 'aws' }, /* ... */ } },
73+
{ properties: { provider: { const: 'gcp' }, /* ... */ } }
74+
]
75+
},
76+
// Reference the nested discriminator field
77+
cloudSpecificOption: {
78+
type: 'string',
79+
'x-visible-when': {
80+
field: 'connectionConfig.provider', // Dot notation for nested path
81+
operator: 'eq',
82+
value: 'aws'
83+
}
84+
}
85+
}
86+
```
87+
88+
**Pattern Matching with `like`/`ilike`:**
89+
90+
```typescript
91+
{
92+
// Visible for providers starting with 'cloud-'
93+
cloudFeature: {
94+
type: 'boolean',
95+
'x-visible-when': {
96+
field: 'provider',
97+
operator: 'ilike',
98+
value: 'cloud-%' // % matches any characters
99+
}
100+
}
101+
}
102+
```
103+
104+
**Use Cases:**
105+
- Show provider-specific fields
106+
- Progressive disclosure based on user choices
107+
- Simpler alternative to `oneOf` for basic conditional visibility
108+
109+
**Related:** [Conditional Fields Example](/guide/examples/conditional-fields), [`x-readonly-when`](#x-readonly-when)
110+
111+
---
112+
113+
## `x-readonly-when`
114+
115+
**Purpose:** Conditionally make a field read-only based on another field's value
116+
117+
**Type:** `{ field: string, operator: string, value?: any }`
118+
119+
**Example:**
120+
```typescript
121+
{
122+
status: {
123+
type: 'string',
124+
enum: ['draft', 'published', 'archived']
125+
},
126+
// Readonly once published or archived
127+
title: {
128+
type: 'string',
129+
title: 'Title',
130+
'x-readonly-when': {
131+
field: 'status',
132+
operator: 'in',
133+
value: ['published', 'archived']
134+
}
135+
}
136+
}
137+
```
138+
139+
**Operators:** Same as [`x-visible-when`](#x-visible-when)
140+
141+
**Use Cases:**
142+
- Lock fields after a certain status is reached
143+
- Prevent editing based on user selections
144+
- Conditional form protection
145+
146+
**Note:** `x-readonly-when` makes the field read-only when the condition is **TRUE**. This is the opposite of `x-visible-when` which shows the field when the condition is TRUE.
147+
148+
---
149+
9150
## `x-hidden`
10151

11-
**Purpose:** Completely hide a field from rendering
152+
**Purpose:** Completely hide a field from rendering (unconditionally)
12153

13154
**Type:** `boolean`
14155

@@ -27,6 +168,8 @@ All `x-*` attributes are optional. QuickForms works perfectly with standard JSON
27168
- Internal tracking IDs
28169
- Fields set programmatically
29170

171+
**See Also:** [`x-visible-when`](#x-visible-when) for conditional visibility
172+
30173
---
31174

32175
## `x-roles`

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.19.1",
3+
"version": "1.19.2",
44
"description": "Framework-agnostic core for QuickForms - JSON Schema form generator",
55
"type": "module",
66
"main": "./dist/index.js",

0 commit comments

Comments
 (0)