Conditions System
Create dynamic forms and workflows with Rilay's powerful conditions system.
The Rilay conditions system provides a powerful, type-safe way to create dynamic forms and workflows that adapt based on user input. Conditions are evaluated in real-time and can control field visibility, requirement status, disabled state, and step navigation in workflows.
The when()
Function
The system centers around the when()
function which returns a ConditionBuilder
instance:
import { when } from '@rilaykit/core';
// Basic condition
const condition = when('fieldName').equals('value');
// Evaluate condition
const result = condition.evaluate({ fieldName: 'value' }); // true
Available Operators
Equality Operators
// Exact equality
when('status').equals('active')
when('age').equals(25)
when('isVip').equals(true)
// Not equal
when('status').notEquals('inactive')
when('role').notEquals('admin')
Numeric Comparisons
// Age must be greater than 18
when('age').greaterThan(18)
// Price must be less than 100
when('price').lessThan(100)
// Score must be at least 70
when('score').greaterThanOrEqual(70)
// Discount cannot exceed 50%
when('discount').lessThanOrEqual(50)
String Operations
// Name contains "John"
when('name').contains('John')
// Email doesn't contain "test"
when('email').notContains('test')
// Starts with prefix
when('code').startsWith('PREFIX_')
// Ends with suffix
when('email').endsWith('@company.com')
// Regex pattern matching
when('phone').matches(/^\d{3}-\d{3}-\d{4}$/)
Array Operations
// Check if array contains a specific value
when('selectedProducts').contains('provident')
when('userRoles').contains('admin')
// Check if array doesn't contain a specific value
when('excludedItems').notContains('premium')
// Check if a single value is in a list of options
when('status').in(['active', 'pending', 'approved'])
// Check if a single value is not in a list of options
when('role').notIn(['admin', 'super-admin'])
Existence Checks
// Field has a value (not null/undefined/empty)
when('companyName').isNotEmpty()
// Field is empty, null, or undefined
when('optionalField').isEmpty()
Logical Operators
AND Operations
Combine multiple conditions where all must be true:
// Multiple conditions must be true
when('age').greaterThan(18)
.and(when('status').equals('active'))
.and(when('country').equals('US'))
// Complex nested example
when('userType').equals('premium')
.and(
when('subscription.plan').equals('pro')
.or(when('subscription.legacy').equals(true))
)
OR Operations
Combine conditions where any can be true:
// Any condition can be true
when('type').equals('premium')
.or(when('age').greaterThan(65))
.or(when('vipStatus').equals(true))
Field Path Resolution
The condition system supports dot notation for nested object access:
// Access nested properties
when('user.profile.age').greaterThan(18)
when('company.address.country').equals('US')
when('settings.notifications.email').equals(true)
// Example data structure
const data = {
user: {
profile: {
age: 25,
name: 'John Doe'
}
},
company: {
address: {
country: 'US',
city: 'New York'
}
}
};
// This condition evaluates to true
when('user.profile.age').greaterThan(18).evaluate(data); // true
Field-Level Conditions
Control individual form field behavior with four types of conditions:
// Show/hide fields dynamically
factory.form()
.add({
id: 'phoneNumber',
type: 'text',
props: { label: 'Phone Number' },
conditions: {
visible: when('contactMethod').equals('phone')
}
})
.add({
id: 'companyDetails',
type: 'text',
props: { label: 'Company Details' },
conditions: {
visible: when('userType').equals('business')
.and(when('hasCompany').equals(true))
}
});
// Make fields required conditionally
factory.form()
.add({
id: 'businessLicense',
type: 'text',
props: { label: 'Business License' },
conditions: {
required: when('businessType').equals('corporation')
.and(when('state').in(['NY', 'CA', 'TX']))
}
})
.add({
id: 'taxId',
type: 'text',
props: { label: 'Tax ID' },
conditions: {
required: when('orderTotal').greaterThan(1000)
}
});
// Disable fields based on conditions
factory.form()
.add({
id: 'email',
type: 'email',
props: { label: 'Email Address' },
conditions: {
disabled: when('emailVerified').equals(true)
}
})
.add({
id: 'discount',
type: 'number',
props: { label: 'Discount %' },
conditions: {
disabled: when('userRole').notEquals('admin')
}
});
// Make fields read-only conditionally
factory.form()
.add({
id: 'accountNumber',
type: 'text',
props: { label: 'Account Number' },
conditions: {
readonly: when('accountLocked').equals(true)
}
})
.add({
id: 'finalScore',
type: 'number',
props: { label: 'Final Score' },
conditions: {
readonly: when('testSubmitted').equals(true)
}
});
Step-Level Conditions (Workflows)
Control workflow step navigation with visibility and skip conditions:
const workflow = factory.flow('user-onboarding')
.addStep({
id: 'personal-info',
title: 'Personal Information',
formConfig: personalInfoForm
})
.addStep({
id: 'company-info',
title: 'Company Information',
conditions: {
// Only show for business users
visible: when('personal-info.userType').equals('business'),
// Allow skipping if company data already exists
skippable: when('personal-info.hasCompanyData').equals(true)
},
formConfig: companyInfoForm
})
.addStep({
id: 'payment-info',
title: 'Payment Information',
conditions: {
// Show for premium users or large companies
visible: when('personal-info.planType').equals('premium')
.or(when('company-info.employees').greaterThan(50)),
// Cannot be skipped for premium users
skippable: when('personal-info.planType').notEquals('premium')
},
formConfig: paymentForm
});
Cross-Step References
Reference data from previous workflow steps using "stepId.fieldId"
format:
// Reference specific step data
when('personal-info.age').greaterThan(18)
when('contact-details.email').contains('@company.com')
when('preferences.notifications').equals(true)
// Real-world example
const workflow = factory.flow('loan-application')
.addStep({
id: 'applicant-info',
formConfig: applicantForm
})
.addStep({
id: 'employment-details',
conditions: {
visible: when('applicant-info.age').greaterThan(18)
.and(when('applicant-info.citizenship').equals('US'))
}
})
.addStep({
id: 'co-signer',
conditions: {
visible: when('applicant-info.creditScore').lessThan(650)
.or(when('employment-details.income').lessThan(50000))
}
});
Real-World Examples
E-commerce Checkout Form
const checkoutForm = factory.form('checkout')
.add({
id: 'billingAddress',
type: 'address',
props: { label: 'Billing Address' },
conditions: {
visible: when('sameAsShipping').equals(false)
}
})
.add({
id: 'creditCardNumber',
type: 'text',
props: { label: 'Credit Card Number' },
conditions: {
visible: when('paymentMethod').equals('credit_card'),
required: when('paymentMethod').equals('credit_card')
}
})
.add({
id: 'paypalEmail',
type: 'email',
props: { label: 'PayPal Email' },
conditions: {
visible: when('paymentMethod').equals('paypal'),
required: when('paymentMethod').equals('paypal')
}
})
.add({
id: 'companyTaxId',
type: 'text',
props: { label: 'Company Tax ID' },
conditions: {
visible: when('customerType').equals('business')
.and(when('country').in(['US', 'CA', 'UK'])),
required: when('customerType').equals('business')
.and(when('orderTotal').greaterThan(1000))
}
});
Survey with Skip Logic
const surveyForm = factory.form('customer-survey')
.add({
id: 'satisfaction',
type: 'select',
props: {
label: 'How satisfied are you?',
options: [
{ value: 'very-satisfied', label: 'Very Satisfied' },
{ value: 'satisfied', label: 'Satisfied' },
{ value: 'neutral', label: 'Neutral' },
{ value: 'dissatisfied', label: 'Dissatisfied' },
{ value: 'very-dissatisfied', label: 'Very Dissatisfied' }
]
}
})
.add({
id: 'positiveReason',
type: 'textarea',
props: { label: 'What did you like most?' },
conditions: {
visible: when('satisfaction').in(['very-satisfied', 'satisfied'])
}
})
.add({
id: 'improvementSuggestions',
type: 'textarea',
props: { label: 'How can we improve?' },
conditions: {
visible: when('satisfaction').in(['neutral', 'dissatisfied', 'very-dissatisfied']),
required: when('satisfaction').in(['dissatisfied', 'very-dissatisfied'])
}
})
.add({
id: 'recommendToFriend',
type: 'select',
props: {
label: 'Would you recommend us to a friend?',
options: [
{ value: 'yes', label: 'Yes' },
{ value: 'no', label: 'No' },
{ value: 'maybe', label: 'Maybe' }
]
},
conditions: {
visible: when('satisfaction').in(['very-satisfied', 'satisfied', 'neutral'])
}
});
Job Application Workflow
const jobApplicationFlow = factory.flow('job-application')
.addStep({
id: 'basic-info',
title: 'Basic Information',
formConfig: basicInfoForm
})
.addStep({
id: 'experience',
title: 'Work Experience',
conditions: {
skippable: when('basic-info.isRecentGraduate').equals(true)
.and(when('basic-info.degree').in(['bachelor', 'master', 'phd']))
},
formConfig: experienceForm
})
.addStep({
id: 'portfolio',
title: 'Portfolio',
conditions: {
visible: when('basic-info.position').in(['designer', 'developer', 'architect'])
.or(when('experience.yearsExperience').greaterThan(3))
},
formConfig: portfolioForm
})
.addStep({
id: 'references',
title: 'References',
conditions: {
visible: when('basic-info.position').in(['manager', 'director', 'vp'])
.or(when('experience.yearsExperience').greaterThan(5)),
skippable: when('basic-info.isInternalCandidate').equals(true)
},
formConfig: referencesForm
});
Advanced Patterns
Progressive Disclosure
Reveal fields progressively based on user choices:
factory.form('progressive-form')
.add({
id: 'experience',
type: 'select',
props: {
label: 'Experience Level',
options: [
{ value: 'beginner', label: 'Beginner' },
{ value: 'intermediate', label: 'Intermediate' },
{ value: 'advanced', label: 'Advanced' }
]
}
})
.add({
id: 'yearsExperience',
type: 'number',
props: { label: 'Years of Experience' },
conditions: {
visible: when('experience').in(['intermediate', 'advanced'])
}
})
.add({
id: 'specializations',
type: 'multiselect',
props: { label: 'Specializations' },
conditions: {
visible: when('experience').equals('advanced')
.and(when('yearsExperience').greaterThan(5))
}
})
.add({
id: 'certifications',
type: 'textarea',
props: { label: 'Professional Certifications' },
conditions: {
visible: when('experience').equals('advanced')
.and(when('yearsExperience').greaterThan(3)),
required: when('specializations').contains('security')
.or(when('specializations').contains('architecture'))
}
});
Dynamic Validation
Combine conditions with validation for smart form behavior:
factory.form('smart-validation')
.add({
id: 'email',
type: 'email',
props: { label: 'Email Address' },
validation: {
validators: [
required('Email is required'),
email('Please enter a valid email')
]
},
conditions: {
required: when('contactMethod').equals('email'),
readonly: when('emailVerified').equals(true)
}
})
.add({
id: 'phone',
type: 'text',
props: { label: 'Phone Number' },
validation: {
validators: [
validateWhen(
(data) => data.contactMethod === 'phone',
required('Phone is required when contact method is phone')
),
pattern(/^\d{3}-\d{3}-\d{4}$/, 'Please enter a valid phone number')
]
},
conditions: {
visible: when('contactMethod').in(['phone', 'both']),
required: when('contactMethod').equals('phone')
}
});
Performance Note: Conditions are evaluated efficiently with short-circuit logic. OR conditions stop evaluating once a true condition is found, and field paths are resolved using optimized object traversal.
Integration with Validation
When a field is not visible due to conditions, its validation is automatically skipped, ensuring the form can be submitted without errors from hidden fields.
// This field's validation only runs when visible
factory.form()
.add({
id: 'businessTaxId',
type: 'text',
validation: {
validators: [
required('Tax ID is required for business accounts'),
pattern(/^\d{2}-\d{7}$/, 'Tax ID must be in format XX-XXXXXXX')
]
},
conditions: {
visible: when('accountType').equals('business'),
required: when('accountType').equals('business')
}
});
Debugging Conditions
Test conditions directly for debugging:
// Test conditions with sample data
const condition = when('age').greaterThan(18);
console.log(condition.evaluate({ age: 25 })); // true
console.log(condition.evaluate({ age: 16 })); // false
// Complex condition testing
const complexCondition = when('type').equals('premium')
.or(when('age').greaterThan(65));
console.log(complexCondition.evaluate({
type: 'basic',
age: 70
})); // true (age > 65)
// Debug helper function
const debugCondition = (condition, data, label) => {
const result = condition.evaluate(data);
console.log(`${label}:`, result, {
condition: condition.build(),
data
});
return result;
};
Best Practices
Keep conditions simple: Prefer multiple simple conditions over complex nested ones for better maintainability.
Use meaningful field names: Clear field names make conditions easier to read and debug.
Test edge cases: Always test conditions with empty, null, and undefined values.
Avoid circular dependencies: Don't create conditions where fields depend on each other in a circular manner.
The conditions system makes Rilay forms and workflows incredibly flexible, allowing you to create sophisticated user experiences that adapt dynamically to user input while maintaining type safety and performance.