How to Create Form Control Library
How to Create Form Control Library
Quick Navigation
- Getting Started with Form Controls
- Understanding Control Types
- Control Architecture Patterns
- Environment Setup
- Step-by-Step Control Creation
- Real-World Examples
- Styling & Theming
- Testing Controls
- Publishing & Deployment
- Integrating with FormAction Hooks
- Troubleshooting & Support
Getting Started with Form Controls
Form controls are the visual components that end users interact with when filling out forms in Atlas Forms applications. Unlike FormAction hooks which handle server-side logic, form controls are React components that:
- Render interactive UI elements (buttons, inputs, selectors, etc.)
- Handle user input and validation
- Trigger actions via FormAction hooks
- Display feedback and results to users
- Integrate seamlessly with the form designer and player
This guide will teach you how to:
- Understand control types and architecture
- Set up your development environment
- Create custom form controls
- Style controls with themes
- Integrate controls with FormAction hooks
- Test and publish your control library
- Create a button library tied to custom pipelines
📚 Prerequisites: You should be familiar with React and TypeScript. If you haven't already, consider reading the FormAction Hook Guide first.
Understanding Control Types
Form controls come in several categories. Let's explore what you can create:
Input Controls
Collect user data
- Text inputs
- Number fields
- Date pickers
- Custom selectors
Action Controls
Trigger operations
- Submit buttons
- Cancel buttons
- Workflow buttons
- Custom action buttons
Display Controls
Show information
- Status badges
- Progress indicators
- Messages & alerts
- Data displays
Layout Controls
Organize forms
- Sections/groups
- Tabs & accordions
- Grids
- Containers
Advanced Controls
Complex interactions
- File uploads
- Signature pads
- Rich editors
- Charts & graphs
Custom Controls
Domain-specific
- Payment widgets
- Map controls
- Video players
- Your ideas!
Control Architecture Patterns
Control Lifecycle
graph TD
A["Control Registered"] --> B["Control Mounted"]
B --> C["Receive Props & Context"]
C --> D["Render UI"]
D --> E["User Interaction"]
E --> F{Action Type?}
F -->|Submit| G["Validate Data"]
F -->|Update| H["Update Form State"]
F -->|Custom| I["Execute Hook Pipeline"]
G --> J["Call Callback"]
H --> J
I --> J
J --> K["Re-render"]
style A fill:#309B48,stroke:#309B48,stroke-width:2px,color:#000
style K fill:#309B48,stroke:#309B48,stroke-width:2px,color:#000
style F fill:#FF9900,stroke:#FF9900,stroke-width:2px
Control Interface
Every form control implements this interface:
export interface IFormControl {
readonly type: string; // Unique identifier
readonly version: string; // Semantic versioning
readonly displayName: string; // User-friendly name
readonly description: string; // What it does
readonly category: string; // Control category
readonly icon?: string; // SVG or data URI
readonly properties: PropertySchema; // Configurable properties
readonly defaultValue?: any; // Default state
// Create React component instance
createComponent(props: any, context: ControlContext): React.ReactElement;
// Optional lifecycle methods
validate?(value: any): ValidationResult;
serialize?(value: any): string;
deserialize?(value: string): any;
}
export interface ControlContext {
readonly formData: Record;
readonly formSchema: JSONSchema;
readonly isReadOnly: boolean;
readonly isInDesigner: boolean;
// Callbacks
onChange?(fieldName: string, value: any): void;
onSubmit?(data: Record): void;
onError?(error: string): void;
}
export interface ValidationResult {
valid: boolean;
errors?: string[];
}
Component vs Control
Understand the difference:
| Aspect | React Component | Form Control |
|---|---|---|
| Purpose | Render UI | Integrates with form framework |
| Props | Any props | Standardized ControlContext |
| Validation | Optional | Required interface method |
| Discovery | Manual imports | Auto-discovered by registry |
| Metadata | None | ID, version, description, etc. |
Environment Setup
Setting up for form control development is similar to hooks but includes React and additional tooling:
1. Verify Prerequisites
# Node.js 16+ and npm 8+
node --version
npm --version
# Install required dependencies
npm install react@18 react-dom@18 typescript --save-dev
2. Create Project Structure
mkdir my-form-controls
cd my-form-controls
mkdir -p src/{controls,hooks,tests,styles}
touch tsconfig.json package.json
3. Install Atlas Forms Packages
npm install @atlas-forms/form-action-pipeline-library-core-js
npm install @atlas-forms/player-components-react
npm install @atlas-forms/types-js
# Type definitions
npm install --save-dev @types/react @types/react-dom
4. Configure TypeScript and Build
# Create tsconfig.json
cat > tsconfig.json
5. Add npm Scripts
{
"scripts": {
"build": "webpack && tsc",
"dev": "webpack --watch",
"test": "jest",
"storybook": "storybook dev",
"prepublishOnly": "npm run build && npm run test"
}
}
Step-by-Step Control Creation
Let's create a custom "Secure Payment Button" control that integrates with hooks.
1. Create the Control Class
Create src/controls/SecurePaymentButton.ts:
import type {
IFormControl,
ControlContext,
ValidationResult
} from '@atlas-forms/player-components-react';
import type { PropertySchema } from '@atlas-forms/types-js';
export interface SecurePaymentButtonProps {
label?: string;
hookPipelineId?: string;
amount?: number;
currency?: string;
showConfirmation?: boolean;
onSuccess?: (result: any) => void;
onError?: (error: string) => void;
}
export class SecurePaymentButton implements IFormControl {
readonly type = 'secure-payment-button';
readonly version = '1.0.0';
readonly displayName = 'Secure Payment Button';
readonly description = 'Submit button for payment processing with integrated hooks';
readonly category = 'action';
readonly icon = `...`; // SVG icon
readonly properties: PropertySchema = {
label: { type: 'string', default: 'Process Payment' },
hookPipelineId: { type: 'string', default: 'payment-pipeline' },
amount: { type: 'number' },
currency: { type: 'string', default: 'USD' },
showConfirmation: { type: 'boolean', default: true }
};
createComponent(
props: SecurePaymentButtonProps,
context: ControlContext
) {
return (
);
}
validate(value: any): ValidationResult {
// Buttons don't typically have values to validate
return { valid: true };
}
}
2. Create the React Component
Add to same file or create src/components/SecurePaymentButtonComponent.tsx:
import React, { useState } from 'react';
import type {
SecurePaymentButtonProps,
ControlContext
} from './SecurePaymentButton';
export function SecurePaymentButtonComponent({
label = 'Process Payment',
hookPipelineId = 'payment-pipeline',
amount,
currency = 'USD',
showConfirmation = true,
onSuccess,
onError,
formData,
formSchema,
onChange,
...context
}: SecurePaymentButtonProps & ControlContext) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [showConfirm, setShowConfirm] = useState(false);
const handleClick = async () => {
if (showConfirmation) {
setShowConfirm(true);
return;
}
await processPayment();
};
const processPayment = async () => {
setError(null);
setIsLoading(true);
setShowConfirm(false);
try {
// Prepare payment data
const paymentData = {
...formData,
amount: amount || formData.amount,
currency,
timestamp: new Date().toISOString()
};
// Execute payment pipeline (which runs PII validation, encryption, etc.)
const result = await context.executor.executePipeline(
hookPipelineId,
{
formSchema,
formData: paymentData,
appConfig: {
payment: {
amount,
currency,
requiresEncryption: true
}
}
}
);
if (result.success) {
// Show success and call callback
if (onSuccess) {
onSuccess(result);
}
} else {
setError(result.error || 'Payment processing failed');
if (onError) {
onError(result.error || 'Payment processing failed');
}
}
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
setError(message);
if (onError) {
onError(message);
}
} finally {
setIsLoading(false);
}
};
return (
<>
{isLoading ? (
<>
Processing...
) : (
`${label}${amount ? ` - ${currency} ${amount.toFixed(2)}` : ''}`
)}
{showConfirm && (
Confirm Payment
Amount: {currency} {amount?.toFixed(2)}
Confirm Payment
setShowConfirm(false)}
className="btn-cancel"
>
Cancel
)}
{error && (
{error}
)}
);
}
3. Create Control Registry
Create src/controls/index.ts:
export { SecurePaymentButton } from './SecurePaymentButton';
export { MyCustomControl } from './MyCustomControl';
// Export additional controls
export function registerControlLibrary(registry: IControlRegistry) {
registry.register(new SecurePaymentButton());
registry.register(new MyCustomControl());
// Register additional controls
}
4. Add Styling
Create src/styles/SecurePaymentButton.css:
.secure-payment-button {
background-color: #309B48;
color: white;
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.3s, transform 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.secure-payment-button:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-2px);
}
.secure-payment-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.payment-confirmation-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 30px;
max-width: 400px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
.modal-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.error-message {
color: #d32f2f;
margin-top: 10px;
padding: 10px;
background-color: rgba(211, 47, 47, 0.1);
border-radius: 4px;
font-size: 14px;
}
5. Build and Test
# Build controls
npm run build
# Run tests
npm test
# Watch mode for development
npm run dev
Real-World Examples
Example 1: Dynamic Action Button Library
Create a library of action buttons tied to different pipelines:
// src/controls/ActionButtonLibrary.tsx
export interface ActionButtonProps {
action: 'submit' | 'approve' | 'reject' | 'save' | 'delete';
label?: string;
confirmMessage?: string;
}
export class ActionButton implements IFormControl {
readonly type = 'action-button';
readonly properties = {
action: {
type: 'enum',
options: ['submit', 'approve', 'reject', 'save', 'delete'],
required: true
},
label: { type: 'string' },
confirmMessage: { type: 'string' }
};
createComponent(props: ActionButtonProps, context: ControlContext) {
const pipelineMap = {
'submit': 'form-action-standard-submit',
'approve': 'approval-workflow-approve',
'reject': 'approval-workflow-reject',
'save': 'form-action-draft-save',
'delete': 'form-action-delete'
};
return (
);
}
}
Example 2: Conditional Visibility Control
Control that shows/hides based on form state:
// src/controls/ConditionalButton.tsx
export interface ConditionalButtonProps {
label: string;
visibilityRule?: (formData: any) => boolean;
disableRule?: (formData: any) => boolean;
pipeline?: string;
}
function ConditionalButtonComponent({
label,
visibilityRule,
disableRule,
pipeline,
formData,
...props
}: ConditionalButtonProps & ControlContext) {
const isVisible = visibilityRule ? visibilityRule(formData) : true;
const isDisabled = disableRule ? disableRule(formData) : false;
if (!isVisible) {
return null; // Don't render if not visible
}
return (
{label}
);
}
Example 3: Progress Tracker Control
Display form completion progress:
// src/controls/ProgressTracker.tsx
export function ProgressTrackerComponent({
formData,
formSchema,
...props
}: ControlContext) {
const requiredFields = formSchema.properties
? Object.entries(formSchema.properties)
.filter(([_, schema]: [string, any]) => schema.required)
.map(([name]) => name)
: [];
const completedFields = requiredFields.filter(
(field) => formData[field] !== undefined && formData[field] !== ''
);
const progress = Math.round(
(completedFields.length / requiredFields.length) * 100
);
return (
{progress}% Complete ({completedFields.length}/{requiredFields.length})
);
}
Styling & Theming
Theme Variables
Use CSS variables for easy theming:
:root {
/* Colors */
--color-primary: #309B48;
--color-secondary: #6B7879;
--color-danger: #d32f2f;
--color-warning: #FF9900;
--color-success: #4caf50;
/* Backgrounds */
--bg-light: #FFFFFF;
--bg-dark: #0F1419;
--bg-secondary: #1A1F29;
/* Text */
--text-primary: #101211;
--text-secondary: #6B7879;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Borders */
--border-radius: 6px;
--border-color: #e0e0e0;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
}
/* Dark mode override */
@media (prefers-color-scheme: dark) {
:root {
--text-primary: #FFFFFF;
--text-secondary: #B0B8C1;
--bg-light: #1A1F29;
--border-color: #2A3038;
}
}
Component Styling Best Practices
- Use CSS Modules for scoped styles
- Respect prefers-color-scheme for dark mode
- Support custom CSS classes from form schema
- Use CSS variables for easy theming
- Ensure accessibility with proper contrast ratios
// styles/SecurePaymentButton.module.css
.button {
background-color: var(--color-primary);
color: white;
padding: var(--spacing-md);
border-radius: var(--border-radius);
border: none;
font-weight: 600;
cursor: pointer;
transition: opacity 0.3s;
}
.button:hover {
opacity: 0.9;
}
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
// Use in component
import styles from './SecurePaymentButton.module.css';
export function Component() {
return Pay Now;
}
Testing Form Controls
Unit Testing with Jest and React Testing Library
// src/tests/SecurePaymentButton.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { SecurePaymentButtonComponent } from '../controls/SecurePaymentButton';
describe('SecurePaymentButton', () => {
const mockContext = {
formData: { amount: 100 },
formSchema: { type: 'object' },
onChange: jest.fn(),
onSubmit: jest.fn(),
executor: {
executePipeline: jest.fn()
}
};
it('should render button with label', () => {
render(
);
expect(screen.getByText(/Pay Now/)).toBeInTheDocument();
});
it('should show confirmation dialog when clicked', () => {
render(
);
const button = screen.getByText(/Pay Now/);
fireEvent.click(button);
expect(screen.getByText(/Confirm Payment/)).toBeInTheDocument();
});
it('should execute payment pipeline on confirmation', async () => {
mockContext.executor.executePipeline.mockResolvedValue({
success: true
});
render(
);
const button = screen.getByText(/Pay Now/);
fireEvent.click(button);
const confirmButton = screen.getByText(/Confirm Payment/);
fireEvent.click(confirmButton);
await waitFor(() => {
expect(mockContext.executor.executePipeline).toHaveBeenCalledWith(
'payment-pipeline',
expect.objectContaining({
formData: expect.objectContaining({ amount: 100 })
})
);
});
});
});
Visual Testing
Use Storybook for visual testing:
// src/stories/SecurePaymentButton.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { SecurePaymentButtonComponent } from '../controls/SecurePaymentButton';
const meta: Meta = {
component: SecurePaymentButtonComponent,
parameters: {
layout: 'centered'
}
};
export default meta;
type Story = StoryObj;
export const Default: Story = {
args: {
label: 'Pay Now',
amount: 99.99,
currency: 'USD',
formData: {},
formSchema: {}
}
};
export const Loading: Story = {
args: {
...Default.args
},
render: (args) => (
{/* Simulate loading state */}
)
};
export const WithError: Story = {
args: {
...Default.args
}
};
Publishing & Deployment
Prepare Package for Publishing
1. Update package.json
{
"name": "@your-org/form-controls-buttons",
"version": "1.0.0",
"description": "Custom button controls for Atlas Forms with pipeline integration",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md",
"LICENSE"
],
"keywords": [
"atlas-forms",
"form-control",
"buttons",
"react"
],
"peerDependencies": {
"react": "^18.0.0",
"@atlas-forms/player-components-react": "^1.0.0"
}
}
2. Create Comprehensive README
# Form Control Buttons Library
Advanced button controls for Atlas Forms with seamless FormAction hook integration.
## Features
- ✓ Secure payment button with confirmation
- ✓ Action buttons (submit, approve, reject, etc.)
- ✓ Conditional visibility & disable rules
- ✓ Progress tracking
- ✓ Full TypeScript support
- ✓ Accessible (WCAG 2.1 AA)
- ✓ Dark mode support
## Installation
```bash
npm install @your-org/form-controls-buttons
Usage
import { registerControlLibrary } from '@your-org/form-controls-buttons';
// In your form app initialization
registerControlLibrary(controlRegistry);
Controls Included
- SecurePaymentButton - Payment processing with hooks
- ActionButton - Multi-action button with pipeline selection
- ConditionalButton - Context-aware visibility and disable rules
Configuration
Each button can be configured in the form schema with custom properties.
License
MIT
**3. Build and Publish**
Build components
npm run build
Test build
npm run test
Create tarball to test locally
npm pack
Publish to npm
npm publish
Verify publication
npm view @your-org/form-controls-buttons
**Distribution Options:**
- **npm Public** - Widest reach, easiest discovery
- **GitHub Packages** - Tight GitHub integration
- **Private npm** - Enterprise distribution
## Integrating with FormAction Hooks
The real power of form controls comes from integrating them with FormAction hooks. Let's see how to build a cohesive system.
### Control + Hook Architecture
graph TB A["Form Control Library"] -->|executes| B["FormAction Hook Pipeline"] B --> C["Hook 1: Validate PII"] C --> D["Hook 2: Encrypt Data"] D --> E["Hook 3: Save to API"] E --> F["Hook 4: Send Notification"] F --> G["Return Result"] G -->|updates UI| A
style A fill:#309B48,stroke:#309B48,stroke-width:2px,color:#000 style B fill:#309B48,stroke:#309B48,stroke-width:2px,color:#000 style G fill:#309B48,stroke:#309B48,stroke-width:2px,color:#000 style C fill:#2A3038,stroke:#309B48,stroke-width:2px style D fill:#2A3038,stroke:#309B48,stroke-width:2px style E fill:#2A3038,stroke:#309B48,stroke-width:2px style F fill:#2A3038,stroke:#309B48,stroke-width:2px
### Example: Payment Button + Validation Hooks
// Define the complete pipeline in your app
const paymentPipeline: IFormActionPipeline = { id: 'payment-processing-pipeline', name: 'Process Payment', category: 'payment', hooks: [ 'form-action-validate', // Validate form 'pii-validator', // Check for PII (from custom hooks) 'payment-encryption-hook', // Encrypt sensitive data 'payment-processor-hook', // Call payment API 'form-action-notify' // Send confirmation ], errorPolicy: { onError: 'fail-fast' // Stop on first error } };
// Register the pipeline executor.registerPipeline(paymentPipeline);
// Now your button uses it
### Passing Data Between Hooks and Controls
Controls pass data to hooks via `appConfig` and receive results:
// In your control const result = await executor.executePipeline(pipelineId, { formSchema, formData, appConfig: { // Data for hooks payment: { amount, currency, metadata: { orderId, userId } }, encryption: { algorithm: 'AES-256-GCM' }, notification: { sendEmail: true, sendSMS: false } } });
// Hooks access this via context.appConfig // And return results via result.data if (result.success) { const processedData = result.finalFormData; const hookResults = result.hookResults;
// hookResults contains { hookId: { success, data } } console.log(hookResults['payment-processor-hook'].data); }
### Error Handling in Hook-Control Integration
// Control handles both validation errors and hook errors
try { const result = await executor.executePipeline(...);
if (!result.success) { // Hook failed const failedHook = result.failedHook; const hookError = result.hookResults[failedHook]?.error;
switch (failedHook) { case 'pii-validator': setError('PII validation failed. Please review sensitive data.'); break; case 'payment-processor-hook': setError('Payment processing failed. Try again later.'); break; default: setError(hookError || 'Processing failed'); } } } catch (error) { // Unexpected error setError('Unexpected error occurred'); console.error(error); }
## Troubleshooting & Support
### Common Issues
#### Issue: Control not appearing in form designer
**Solution:** Ensure control is registered before app starts:
import from './controls';
// During app initialization registerControlLibrary(controlRegistry);
// Then rebuild and restart
#### Issue: Hook not executing from control
**Solution:** Check pipeline ID and ensure pipeline is registered:
// Verify pipeline exists executor.registerPipeline(myPipeline);
// Use correct ID in control
#### Issue: Styling not applied
**Solution:** Check CSS import and specificity:
// Ensure CSS is imported import './styles/MyControl.css';
// Check for CSS conflicts // Use CSS modules for scoping: import styles from './MyControl.module.css';
#### Issue: React context errors
**Solution:** Wrap with proper providers:
### Debugging Techniques
1. **Console logging:** Add logs in component and pipeline
2. **React DevTools:** Inspect component props and state
3. **Network tab:** Watch hook API calls
4. **Storybook:** Isolate component for testing
5. **Unit tests:** Verify behavior in isolation
### Getting Help
- **Docs:** [Atlas Forms Documentation](https://docs.bizfirstai.com/atlas-forms)
- **GitHub Issues:** [Report Issues](https://github.com/bizfirstai/atlas-forms/issues)
- **Community:** [BizFirstAI Community Forum](https://community.bizfirstai.com)
- **Discord:** [Join Our Discord](https://discord.gg/bizfirstai)
- **Email:** [dev-support@bizfirstai.com](mailto:dev-support@bizfirstai.com)
**Have a question?** Don't hesitate to ask! Our community is friendly and helpful. Check if your issue has been answered before posting.
## Ready to Build Custom Controls?
Create powerful, reusable form controls and publish to the Atlas Forms ecosystem.
[Get Started with Template](https://github.com/bizfirstai/atlas-forms-control-template)
[Learn About FormAction Hooks](HowToCreateFormActionLibrary.html)