Custom Functions
Extend your forms with custom JavaScript/TypeScript logic for complex operations that go beyond JSON configuration.
What Are Custom Functions?
Custom functions are JavaScript/TypeScript functions you write for complex logic that's too hard to express in JSON. They have full access to the form context and can perform any operation - API calls, data transformations, complex calculations, and more.
When to Use
✅Use Custom Functions For:
- • Complex API calls with error handling
- • Data transformations (arrays, objects)
- • Complex conditionals
- • Date/time manipulation
- • Multiple steps of logic
- • Validation against external systems
💡Use Computed Fields For:
- • Simple math (price × quantity)
- • Percentages
- • String concatenation
- • Rounding numbers
- • Simple conditionals
Basic Setup
Step 1: Define Your Function
import { DynamicContext } from '@seanblock/form-builder';
const customFunctions = {
myFunction: async (context: DynamicContext) => {
// Your logic here
// Read from context
const value = context.myField;
// Do something
const result = await fetch('/api/endpoint');
const data = await result.json();
// Update context
context.myField = data.value;
// Return data (optional)
return data;
}
};Step 2: Pass to FormBuilderV3
<FormBuilderV3
config={myConfig}
context={{}}
customFunctions={customFunctions}
/>Step 3: Register as a Service
{
"services": {
"myFunction": {
"type": "function",
"function": "myFunction"
}
}
}Step 4: Call from Actions
{
"actions": [
{
"type": "callApi",
"service": "myFunction"
}
]
}Complete Example: Validate Discount Code
The Function (TypeScript)
const customFunctions = {
validateDiscountCode: async (context: DynamicContext) => {
const code = context.discount?.code?.toUpperCase();
// Call your API
const response = await fetch('/api/validate-discount', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code })
});
const data = await response.json();
// Update context based on response
if (data.valid) {
context.discount.percent = data.percent;
context.discount.isValid = true;
context.discount.errorMessage = '';
} else {
context.discount.percent = 0;
context.discount.isValid = false;
context.discount.errorMessage = 'Invalid discount code';
}
return data;
}
};The Configuration (JSON)
{
"context": {
"discount": {
"code": "",
"percent": 0,
"isValid": false,
"errorMessage": ""
}
},
"services": {
"validateDiscountCode": {
"type": "function",
"function": "validateDiscountCode"
}
},
"blocks": [
{
"id": "discount-input",
"blockType": "InputBlock",
"metadata": {
"label": "Discount Code",
"contextPath": "discount.code"
}
},
{
"id": "validate-btn",
"blockType": "ButtonBlock",
"metadata": {
"label": "Validate Code"
},
"actions": [
{
"type": "callApi",
"service": "validateDiscountCode"
}
]
},
{
"id": "error-message",
"blockType": "ParagraphBlock",
"className": "text-red-600",
"displayConditions": [
{
"matchType": "exact",
"field": "discount.isValid",
"value": false
}
],
"metadata": {
"text": "{{context:discount.errorMessage}}"
}
}
]
}Working with Context
Reading Values
async (context: DynamicContext) => {
// Simple values
const name = context.user.name;
const email = context.user.email;
// Nested values
const street = context.address?.street || '';
// Arrays
const items = context.cart.items || [];
// Numbers
const quantity = context.order.quantity || 1;
}Updating Values
async (context: DynamicContext) => {
// Simple assignment
context.user.name = "John Doe";
// Nested object
context.address = {
street: "123 Main St",
city: "New York",
state: "NY"
};
// Update existing object
context.cart.total = calculateTotal(context.cart.items);
// Boolean flags
context.form.isValid = true;
context.form.isSubmitting = false;
}Common Patterns
Pattern 1: API Call with Error Handling
const customFunctions = {
fetchUserData: async (context: DynamicContext) => {
try {
context.loading = true;
const response = await fetch(`/api/users/${context.userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user data');
}
const data = await response.json();
context.user = data;
context.loading = false;
context.error = null;
return { success: true, data };
} catch (error) {
context.loading = false;
context.error = error.message;
return { success: false, error: error.message };
}
}
};Pattern 2: Data Transformation
const customFunctions = {
calculateShipping: async (context: DynamicContext) => {
const items = context.cart.items || [];
// Calculate total weight
const totalWeight = items.reduce((sum, item) => {
return sum + (item.weight * item.quantity);
}, 0);
// Get shipping zones
const response = await fetch('/api/shipping/calculate', {
method: 'POST',
body: JSON.stringify({
weight: totalWeight,
zipCode: context.address.zipCode
})
});
const data = await response.json();
context.cart.shipping = data.cost;
context.cart.shippingMethod = data.method;
context.cart.estimatedDays = data.estimatedDays;
return data;
}
};Pattern 3: Complex Validation
const customFunctions = {
validateForm: async (context: DynamicContext) => {
const errors: string[] = [];
// Complex business logic
if (context.user.age < 18 && !context.guardian.present) {
errors.push('Guardian consent required for minors');
}
if (context.payment.method === 'credit' && !context.payment.verified) {
errors.push('Credit card verification required');
}
// Check against external system
const response = await fetch('/api/validate-order', {
method: 'POST',
body: JSON.stringify(context.order)
});
const validation = await response.json();
if (!validation.valid) {
errors.push(...validation.errors);
}
context.form.errors = errors;
context.form.isValid = errors.length === 0;
return {
valid: errors.length === 0,
errors
};
}
};Pattern 4: Date/Time Operations
const customFunctions = {
calculateAvailability: async (context: DynamicContext) => {
const selectedDate = new Date(context.appointment.date);
const today = new Date();
// Check if date is in the past
if (selectedDate < today) {
context.appointment.error = 'Cannot book past dates';
return { available: false };
}
// Check availability
const response = await fetch(`/api/availability?date=${context.appointment.date}`);
const data = await response.json();
context.appointment.availableSlots = data.slots;
context.appointment.nextAvailable = data.nextAvailable;
return data;
}
};Combining All Features
Custom functions work seamlessly with computed fields and lifecycle hooks:
// Custom function
const customFunctions = {
lookupTaxRate: async (context: DynamicContext) => {
const response = await fetch(`/api/tax-rate/${context.location.zipCode}`);
const data = await response.json();
context.location.taxRate = data.rate;
context.location.city = data.city;
context.location.state = data.state;
return data;
}
};{
"lifecycle": {
"onContextChange": [
{
"watch": ["location.zipCode"],
"actions": [
{
"type": "callApi",
"service": "lookupTaxRate"
}
]
}
]
},
"computedFields": {
"tax": {
"targetPath": "order.tax",
"expression": "context.order.subtotal * context.location.taxRate",
"dependencies": ["order.subtotal", "location.taxRate"]
}
}
}🎯 The Flow:
- 1. User enters ZIP code
- 2. Lifecycle hook triggers
- 3. Custom function calls API and updates
taxRate - 4. Computed field detects dependency change
- 5. Tax recalculates automatically!
TypeScript Types
import { DynamicContext } from '@seanblock/form-builder';
// Custom function signature
type CustomFunction = (context: DynamicContext) => Promise<any>;
// Your functions object
const customFunctions: Record<string, CustomFunction> = {
myFunction: async (context) => {
// TypeScript will provide autocomplete for common context patterns
return { success: true };
}
};Best Practices
✅ Always handle errors
Use try/catch and set error flags in context
✅ Set loading states
Update context.loading before/after async operations
✅ Return meaningful data
Return objects with success/error info for action chaining
✅ Provide defaults
Use || [] and || for safety
❌ Don't mutate arrays directly
Create new arrays: context.items = [...oldItems, newItem]
❌ Don't forget async/await
Always use async and await for promises
Debugging
const customFunctions = {
myFunction: async (context: DynamicContext) => {
// Log current context
console.log('Current context:', context);
// Log specific values
console.log('User ID:', context.userId);
// Log API responses
const response = await fetch('/api/endpoint');
const data = await response.json();
console.log('API response:', data);
return data;
}
};Live Example
🎯 Try It Out
Check out working examples with custom functions: