Plugin Development Guide
Learn how to extend QuikForms with custom validators, submission handlers, and field types.
Overview
QuikForms provides a powerful plugin framework that allows you to extend form functionality with custom business logic, integrations, and field types without modifying core code.
What You Can Build
- Field Validators - Custom validation logic for fields
- Submission Handlers - Pre/post-submission business logic
- Callout Handlers - External API integrations
- Custom Field Types - New field types with custom rendering
No Core Modifications Required
Plugins are registered via Custom Metadata Types, allowing you to add/remove functionality without touching the QuikForms codebase.
Plugin Architecture
The plugin system consists of three main components:
- Plugin Interfaces - Define the contract for plugins
- Plugin Registry - Discovers and loads plugins from Custom Metadata
- Plugin Execution - Orchestrates plugin execution with error handling
Plugin Architecture
Plugin Lifecycle
- Plugin registered in
QuikForms_Plugin__mdt - Registry loads active plugins on initialization
- Plugin class instantiated and
initialize()called - Plugin executes when triggered (field validation, form submission, etc.)
- Results returned to caller
Plugin Types
Field Validators
Validate field values with custom business logic.
Use Cases
- Email domain whitelist/blacklist
- Credit card validation
- Phone number formatting
- Custom regex patterns
- Cross-field validation
- External API validation (address lookup, etc.)
Validation Modes
- Synchronous - Immediate validation (client-side)
- Asynchronous - Server-side validation with callouts
- Both - Validate on both client and server
Submission Handlers
Execute custom logic before or after form submission.
Use Cases
- Data enrichment (lookup additional data)
- External system integration
- Email notifications
- Platform Events
- Custom object creation
- Workflow triggering
Execution Points
- Before Submit - Modify field values, add computed fields, validate
- After Submit - Post-processing, notifications, integrations
Callout Handlers
Handle server-side callouts from JavaScript plugins.
Use Cases
- Address autocomplete
- Real-time data lookup
- Third-party API integration
- Dynamic field population
Custom Field Types
Create new field types with custom rendering and behavior.
Use Cases
- Signature capture
- File upload with preview
- Rich text editor
- Date range picker
- Custom visualizations
Creating Plugins
Step 1: Project Setup
Create a new Apex class in your Salesforce org:
# Using SFDX CLI
sfdx force:apex:class:create -n EmailDomainValidator -d force-app/main/default/classes
Step 2: Implement Interface
Choose the appropriate interface based on plugin type.
Base Interface (Required for All)
public interface IQuikFormsPlugin {
QuikFormsPluginInfo getPluginInfo();
void initialize(Map<String, Object> configuration);
Boolean isReady();
}
Field Validator Interface
public interface IQuikFormsFieldValidator extends IQuikFormsPlugin {
QuikFormsValidationResult validateSync(QuikFormsValidationContext context);
QuikFormsValidationResult validateAsync(QuikFormsValidationContext context);
String getValidationMode(); // 'Sync', 'Async', or 'Both'
}
Submission Handler Interface
public interface IQuikFormsSubmissionHandler extends IQuikFormsPlugin {
QuikFormsSubmissionResult onBeforeSubmit(QuikFormsSubmissionContext context);
QuikFormsSubmissionResult onAfterSubmit(QuikFormsSubmissionContext context);
}
Step 3: Register Plugin
Create a Custom Metadata record for your plugin:
- Navigate to Setup → Custom Metadata Types
- Click Manage Records next to QuikForms_Plugin
- Click New
- Configure the plugin record
| Field | Value |
|---|---|
| Label | Email Domain Validator |
| Plugin Name | Email_Domain_Validator |
| Plugin Type | FieldValidator |
| Apex Class Name | EmailDomainValidator |
| Is Active | ✓ (checked) |
| Execution Order | 10 |
| Validation Mode | Sync |
| Error Behavior | Fatal |
Step 4: Deploy
Deploy your plugin class and Custom Metadata record:
# Deploy Apex class
sfdx force:source:deploy -p force-app/main/default/classes/EmailDomainValidator.cls
# Deploy Custom Metadata
sfdx force:source:deploy -p force-app/main/default/customMetadata
Field Validator Example
Complete example of a field validator plugin:
public class EmailDomainValidator implements IQuikFormsFieldValidator {
private List<String> allowedDomains;
private List<String> blockedDomains;
// IQuikFormsPlugin methods
public QuikFormsPluginInfo getPluginInfo() {
return new QuikFormsPluginInfo(
'Email Domain Validator',
'1.0',
'Validates email addresses against allowed/blocked domain lists'
);
}
public void initialize(Map<String, Object> configuration) {
// Load configuration
this.allowedDomains = (List<String>)configuration.get('allowedDomains');
this.blockedDomains = (List<String>)configuration.get('blockedDomains');
// Default to empty lists if not configured
if (this.allowedDomains == null) {
this.allowedDomains = new List<String>();
}
if (this.blockedDomains == null) {
this.blockedDomains = new List<String>();
}
}
public Boolean isReady() {
// Plugin is ready if at least one domain list is configured
return !this.allowedDomains.isEmpty() || !this.blockedDomains.isEmpty();
}
// IQuikFormsFieldValidator methods
public String getValidationMode() {
return 'Sync'; // Client-side validation
}
public QuikFormsValidationResult validateSync(QuikFormsValidationContext context) {
QuikFormsValidationResult result = new QuikFormsValidationResult();
result.isValid = true;
// Get email value
String email = (String)context.fieldValue;
// Skip validation if empty (handled by required field validation)
if (String.isBlank(email)) {
return result;
}
// Extract domain from email
String domain = email.substringAfter('@').toLowerCase();
// Check blocked domains first
if (!this.blockedDomains.isEmpty() && this.blockedDomains.contains(domain)) {
result.isValid = false;
result.errorMessages = new List<String>{
'Email addresses from ' + domain + ' are not allowed'
};
return result;
}
// Check allowed domains
if (!this.allowedDomains.isEmpty() && !this.allowedDomains.contains(domain)) {
result.isValid = false;
result.errorMessages = new List<String>{
'Only email addresses from approved domains are permitted'
};
result.warningMessages = new List<String>{
'Allowed domains: ' + String.join(this.allowedDomains, ', ')
};
}
return result;
}
public QuikFormsValidationResult validateAsync(QuikFormsValidationContext context) {
// Not used for this validator (synchronous only)
return null;
}
}
Configuration
Configure the validator using JSON in the Custom Metadata record:
{
"allowedDomains": ["company.com", "partner.com", "example.com"],
"blockedDomains": ["tempmail.com", "throwaway.email"]
}
Submission Handler Example
Complete example of a submission handler plugin:
public class LeadEnrichmentHandler implements IQuikFormsSubmissionHandler {
private String enrichmentApiEndpoint;
private String apiKey;
// IQuikFormsPlugin methods
public QuikFormsPluginInfo getPluginInfo() {
return new QuikFormsPluginInfo(
'Lead Enrichment Handler',
'1.0',
'Enriches lead data with additional information from external API'
);
}
public void initialize(Map<String, Object> configuration) {
this.enrichmentApiEndpoint = (String)configuration.get('apiEndpoint');
this.apiKey = (String)configuration.get('apiKey');
}
public Boolean isReady() {
return String.isNotBlank(this.enrichmentApiEndpoint)
&& String.isNotBlank(this.apiKey);
}
// IQuikFormsSubmissionHandler methods
public QuikFormsSubmissionResult onBeforeSubmit(QuikFormsSubmissionContext context) {
QuikFormsSubmissionResult result = new QuikFormsSubmissionResult();
result.success = true;
result.shouldContinue = true;
try {
// Get company name from form data
String companyName = (String)context.fieldValues.get('company_field');
if (String.isNotBlank(companyName)) {
// Call enrichment API
Map<String, Object> enrichmentData = callEnrichmentAPI(companyName);
// Add enriched data to field values
if (enrichmentData != null) {
result.modifiedFieldValues = new Map<String, Object>();
result.modifiedFieldValues.put('industry_field',
enrichmentData.get('industry'));
result.modifiedFieldValues.put('employee_count_field',
enrichmentData.get('employeeCount'));
result.modifiedFieldValues.put('annual_revenue_field',
enrichmentData.get('annualRevenue'));
}
}
} catch (Exception e) {
// Log error but don't fail submission
QuikExceptionLogger.log(e, 'Lead Enrichment', context.formConfigId);
}
return result;
}
public QuikFormsSubmissionResult onAfterSubmit(QuikFormsSubmissionContext context) {
QuikFormsSubmissionResult result = new QuikFormsSubmissionResult();
result.success = true;
result.shouldContinue = true;
try {
// Send notification to sales team
sendSalesNotification(context.recordId);
// Create follow-up task
createFollowUpTask(context.recordId);
} catch (Exception e) {
QuikExceptionLogger.log(e, 'Post-Submission Handler', context.formConfigId);
}
return result;
}
// Helper methods
private Map<String, Object> callEnrichmentAPI(String companyName) {
// Make HTTP callout to enrichment API
HttpRequest req = new HttpRequest();
req.setEndpoint(this.enrichmentApiEndpoint + '?company=' +
EncodingUtil.urlEncode(companyName, 'UTF-8'));
req.setMethod('GET');
req.setHeader('Authorization', 'Bearer ' + this.apiKey);
req.setTimeout(10000);
Http http = new Http();
HttpResponse res = http.send(req);
if (res.getStatusCode() == 200) {
return (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
}
return null;
}
private void sendSalesNotification(Id recordId) {
// Send email or platform event
}
private void createFollowUpTask(Id recordId) {
// Create Task record
}
}
Best Practices
Error Handling
- Always use try-catch blocks
- Log errors using
QuikExceptionLogger - Return graceful error messages to users
- Don't fail submission for non-critical errors
Performance
- Keep validation logic lightweight
- Cache expensive computations
- Use async validation for callouts
- Implement timeouts for external calls
- Avoid SOQL queries in loops
Configuration
- Make plugins configurable via Custom Metadata
- Validate configuration in
initialize() - Provide sensible defaults
- Document configuration options in JSON schema
Security
- Validate all input data
- Sanitize user-provided values
- Use
with sharingwhen appropriate - Respect field-level security
- Don't log sensitive data
Testing Plugins
Unit Tests
Create comprehensive unit tests for your plugins:
@isTest
private class EmailDomainValidatorTest {
@isTest
static void testAllowedDomain() {
// Setup
EmailDomainValidator validator = new EmailDomainValidator();
Map<String, Object> config = new Map<String, Object>{
'allowedDomains' => new List<String>{'company.com'}
};
validator.initialize(config);
// Create context
QuikFormsValidationContext context = new QuikFormsValidationContext();
context.fieldId = 'email_field';
context.fieldValue = '[email protected]';
// Test
Test.startTest();
QuikFormsValidationResult result = validator.validateSync(context);
Test.stopTest();
// Verify
System.assert(result.isValid, 'Email from allowed domain should be valid');
}
@isTest
static void testBlockedDomain() {
// Setup
EmailDomainValidator validator = new EmailDomainValidator();
Map<String, Object> config = new Map<String, Object>{
'blockedDomains' => new List<String>{'tempmail.com'}
};
validator.initialize(config);
// Create context
QuikFormsValidationContext context = new QuikFormsValidationContext();
context.fieldId = 'email_field';
context.fieldValue = '[email protected]';
// Test
Test.startTest();
QuikFormsValidationResult result = validator.validateSync(context);
Test.stopTest();
// Verify
System.assert(!result.isValid, 'Email from blocked domain should be invalid');
System.assert(result.errorMessages.size() > 0, 'Should have error message');
}
}
Integration Tests
Test plugins in the context of form submission:
@isTest
private class LeadEnrichmentHandlerTest {
@isTest
static void testEnrichmentIntegration() {
// Setup mock HTTP callout
Test.setMock(HttpCalloutMock.class, new EnrichmentAPIMock());
// Create form configuration
FormConfigVersion__c formConfig = TestDataFactory.createFormConfig();
// Create submission context
QuikFormsSubmissionContext context = new QuikFormsSubmissionContext();
context.formConfigId = formConfig.Id;
context.fieldValues = new Map<String, Object>{
'company_field' => 'Acme Corp'
};
// Test
Test.startTest();
LeadEnrichmentHandler handler = new LeadEnrichmentHandler();
handler.initialize(getTestConfig());
QuikFormsSubmissionResult result = handler.onBeforeSubmit(context);
Test.stopTest();
// Verify
System.assert(result.success, 'Enrichment should succeed');
System.assert(result.modifiedFieldValues != null, 'Should have enriched data');
System.assert(result.modifiedFieldValues.containsKey('industry_field'));
}
}
Plugin Development Complete!
You now have everything you need to create powerful plugins for QuikForms. For more examples and detailed API documentation, see the API Reference.