Conditional Fields Example
Create dynamic forms where fields appear based on other field values.
Two Approaches
QuickForms offers two approaches for conditional fields:
| Approach | Best For |
|---|---|
x-visible-when | Simple show/hide based on another field's value |
oneOf/anyOf | Complex scenarios with different validation rules per variant |
Using x-visible-when (Recommended for Simple Cases)
For basic "show field X when field Y equals Z" scenarios, use x-visible-when:
vue
<script setup lang="ts">
import { ref } from 'vue'
import { DynamicForm } from '@quickflo/quickforms-vue'
import type { JSONSchema } from '@quickflo/quickforms'
const schema: JSONSchema = {
type: 'object',
properties: {
provider: {
type: 'string',
title: 'Cloud Provider',
enum: ['aws', 'gcp', 'azure']
},
// Only visible when AWS is selected
awsRegion: {
type: 'string',
title: 'AWS Region',
enum: ['us-east-1', 'us-west-2', 'eu-west-1'],
'x-visible-when': {
field: 'provider',
operator: 'eq',
value: 'aws'
}
},
// Only visible when GCP is selected
gcpProject: {
type: 'string',
title: 'GCP Project ID',
'x-visible-when': {
field: 'provider',
operator: 'eq',
value: 'gcp'
}
},
// Visible for multiple providers (AWS or GCP)
enableEncryption: {
type: 'boolean',
title: 'Enable Encryption',
'x-visible-when': {
field: 'provider',
operator: 'in',
value: ['aws', 'gcp']
}
}
}
}
const formData = ref({ provider: 'aws' })
</script>
<template>
<DynamicForm :schema="schema" v-model="formData" />
</template>Supported Operators
| Operator | Aliases | Description | Example |
|---|---|---|---|
eq | ==, === | Equals | { operator: 'eq', value: 'aws' } |
neq | !=, !== | Not equals | { operator: 'neq', value: 'azure' } |
in | Value in array | { operator: 'in', value: ['aws', 'gcp'] } | |
notIn | !in | Value not in array | { operator: 'notIn', value: ['azure'] } |
truthy | Value is truthy | { operator: 'truthy' } | |
falsy | Value is falsy | { operator: 'falsy' } | |
gt | > | Greater than | { operator: 'gt', value: 18 } |
gte | >= | Greater than or equal | { operator: 'gte', value: 18 } |
lt | < | Less than | { operator: 'lt', value: 100 } |
lte | <= | Less than or equal | { operator: 'lte', value: 100 } |
like | Case-sensitive pattern | { operator: 'like', value: 'cloud-%' } | |
ilike | Case-insensitive pattern | { operator: 'ilike', value: 'CLOUD-%' } |
Nested Field References
Reference fields in nested objects using dot notation:
typescript
{
connectionConfig: {
type: 'object',
oneOf: [
{ properties: { provider: { const: 'aws' } } },
{ properties: { provider: { const: 'gcp' } } }
]
},
// Reference nested field with dot notation
awsSpecificOption: {
type: 'string',
'x-visible-when': {
field: 'connectionConfig.provider',
operator: 'eq',
value: 'aws'
}
}
}Also: x-readonly-when
Make fields conditionally read-only using the same syntax:
typescript
{
status: {
type: 'string',
enum: ['draft', 'published']
},
title: {
type: 'string',
// Readonly once published
'x-readonly-when': {
field: 'status',
operator: 'eq',
value: 'published'
}
}
}See: Schema Extensions - x-visible-when for full documentation.
Using oneOf (Advanced Approach)
For complex scenarios where you need:
- Different validation rules per variant
- Entirely different field sets with per-variant
requiredarrays - Type-safe discriminated unions
Use oneOf:
oneOf Example
vue
<script setup lang="ts">
import { ref } from 'vue'
import { DynamicForm } from '@quickflo/quickforms-vue'
import type { JSONSchema } from '@quickflo/quickforms'
const schema: JSONSchema = {
type: 'object',
properties: {
accountType: {
type: 'string',
title: 'Account Type',
enum: ['personal', 'business'],
default: 'personal'
}
},
required: ['accountType'],
// Conditional fields based on accountType
oneOf: [
{
properties: {
accountType: { const: 'personal' },
firstName: {
type: 'string',
title: 'First Name'
},
lastName: {
type: 'string',
title: 'Last Name'
},
dateOfBirth: {
type: 'string',
format: 'date',
title: 'Date of Birth'
}
},
required: ['firstName', 'lastName']
},
{
properties: {
accountType: { const: 'business' },
companyName: {
type: 'string',
title: 'Company Name'
},
ein: {
type: 'string',
title: 'EIN',
pattern: '^\\d{2}-\\d{7}$'
},
industry: {
type: 'string',
title: 'Industry',
enum: ['tech', 'finance', 'healthcare', 'retail', 'other']
}
},
required: ['companyName', 'ein']
}
]
}
const formData = ref({ accountType: 'personal' })
</script>
<template>
<DynamicForm :schema="schema" v-model="formData" />
</template>Payment Method Selector
typescript
const schema: JSONSchema = {
type: 'object',
properties: {
paymentMethod: {
type: 'string',
title: 'Payment Method',
enum: ['credit_card', 'bank_transfer', 'paypal']
}
},
oneOf: [
{
title: 'Credit Card',
properties: {
paymentMethod: { const: 'credit_card' },
cardNumber: {
type: 'string',
title: 'Card Number',
pattern: '^\\d{16}$'
},
expiryDate: {
type: 'string',
title: 'Expiry (MM/YY)',
pattern: '^\\d{2}/\\d{2}$'
},
cvv: {
type: 'string',
title: 'CVV',
pattern: '^\\d{3,4}$'
}
}
},
{
title: 'Bank Transfer',
properties: {
paymentMethod: { const: 'bank_transfer' },
accountNumber: {
type: 'string',
title: 'Account Number'
},
routingNumber: {
type: 'string',
title: 'Routing Number',
pattern: '^\\d{9}$'
}
}
},
{
title: 'PayPal',
properties: {
paymentMethod: { const: 'paypal' },
email: {
type: 'string',
format: 'email',
title: 'PayPal Email'
}
}
}
]
}Shipping Options
typescript
const schema: JSONSchema = {
type: 'object',
properties: {
shippingMethod: {
type: 'string',
title: 'Shipping Method',
enum: ['standard', 'express', 'pickup'],
'x-enum-labels': {
standard: 'Standard Shipping (5-7 days)',
express: 'Express Shipping (1-2 days)',
pickup: 'In-Store Pickup'
}
}
},
oneOf: [
{
properties: {
shippingMethod: { const: 'standard' },
address: {
type: 'object',
title: 'Shipping Address',
properties: {
street: { type: 'string', title: 'Street' },
city: { type: 'string', title: 'City' },
state: { type: 'string', title: 'State' },
zip: { type: 'string', title: 'ZIP' }
}
}
}
},
{
properties: {
shippingMethod: { const: 'express' },
address: {
type: 'object',
title: 'Shipping Address',
properties: {
street: { type: 'string', title: 'Street' },
city: { type: 'string', title: 'City' },
state: { type: 'string', title: 'State' },
zip: { type: 'string', title: 'ZIP' }
}
},
phoneNumber: {
type: 'string',
title: 'Phone Number',
description: 'Required for express delivery'
}
}
},
{
properties: {
shippingMethod: { const: 'pickup' },
store: {
type: 'string',
title: 'Pickup Location',
enum: ['store1', 'store2', 'store3'],
'x-enum-labels': {
store1: 'Downtown Location',
store2: 'Westside Mall',
store3: 'North Plaza'
}
},
pickupDate: {
type: 'string',
format: 'date',
title: 'Pickup Date'
}
}
}
]
}How It Works
The Discriminator Field
The first field acts as the "discriminator" that controls which schema is active:
typescript
{
accountType: {
type: 'string',
enum: ['personal', 'business']
}
}Conditional Schemas
Each schema in oneOf uses const to match the discriminator value:
typescript
oneOf: [
{
properties: {
accountType: { const: 'personal' }, // Match this value
// ... personal fields
}
},
{
properties: {
accountType: { const: 'business' }, // Match this value
// ... business fields
}
}
]Form Behavior
- User selects a value in the discriminator field
- QuickForms finds matching
oneOfschema - Only fields from that schema are rendered
- Validation applies only to visible fields
Complex Example: Survey Form
typescript
const schema: JSONSchema = {
type: 'object',
properties: {
employmentStatus: {
type: 'string',
title: 'Employment Status',
enum: ['employed', 'self-employed', 'unemployed', 'student', 'retired']
}
},
oneOf: [
{
title: 'Employed',
properties: {
employmentStatus: { const: 'employed' },
employer: { type: 'string', title: 'Employer' },
jobTitle: { type: 'string', title: 'Job Title' },
yearsEmployed: { type: 'number', title: 'Years at Current Job', minimum: 0 },
annualIncome: {
type: 'number',
title: 'Annual Income',
minimum: 0,
'x-component-props': {
prefix: '$'
}
}
}
},
{
title: 'Self-Employed',
properties: {
employmentStatus: { const: 'self-employed' },
businessName: { type: 'string', title: 'Business Name' },
businessType: {
type: 'string',
title: 'Business Type',
enum: ['sole-proprietor', 'llc', 'corporation', 'partnership']
},
yearsInBusiness: { type: 'number', title: 'Years in Business', minimum: 0 },
estimatedIncome: {
type: 'number',
title: 'Estimated Annual Income',
minimum: 0
}
}
},
{
title: 'Unemployed',
properties: {
employmentStatus: { const: 'unemployed' },
lastEmployer: { type: 'string', title: 'Last Employer' },
unemployedSince: {
type: 'string',
format: 'date',
title: 'Unemployed Since'
},
seekingWork: {
type: 'boolean',
title: 'Actively Seeking Work',
default: true
}
}
},
{
title: 'Student',
properties: {
employmentStatus: { const: 'student' },
school: { type: 'string', title: 'School/University' },
degreeProgram: { type: 'string', title: 'Degree Program' },
expectedGraduation: {
type: 'string',
format: 'date',
title: 'Expected Graduation'
},
partTimeWork: {
type: 'boolean',
title: 'Work Part-Time',
default: false
}
}
},
{
title: 'Retired',
properties: {
employmentStatus: { const: 'retired' },
retirementYear: {
type: 'number',
title: 'Year of Retirement',
minimum: 1950,
maximum: new Date().getFullYear()
},
lastOccupation: { type: 'string', title: 'Last Occupation' },
pensionIncome: {
type: 'number',
title: 'Annual Pension/Retirement Income',
minimum: 0
}
}
}
]
}Choosing the Right Approach
| Scenario | Recommended Approach |
|---|---|
| Show/hide a few fields based on a selection | x-visible-when |
| Different required fields per variant | oneOf |
| Same fields, just conditional visibility | x-visible-when |
| Completely different form sections | oneOf |
| Make fields readonly based on status | x-readonly-when |
| Need tabs/dropdown UI for variant selection | oneOf with x-oneof-style |
Tips
For x-visible-when
- Keep it simple: Best for 1-3 conditional fields
- Dot notation: Reference nested fields with
parent.childpaths - Combine with
x-readonly-when: Show a field but make it readonly based on conditions - Values auto-clear: When a field becomes hidden, its value is automatically cleared. Use
'x-clear-on-hide': falseto preserve values.
For oneOf
- Clear Labels: Use descriptive
titlein eachoneOfschema - Default Values: Set a default for the discriminator field
- Enum Labels: Use
x-enum-labelsfor better UX - Validation: Each
oneOfschema can have its ownrequiredarray - Nested Objects: Conditional schemas can include nested objects and arrays
Next Steps
- Schema Extensions - x-visible-when - Full operator reference
- Custom Validation - Add custom validation logic
- Complex Types - More about oneOf, anyOf, allOf