Creating Your Own UI Preset
While Enforma includes built-in presets for popular UI libraries like PrimeVue, Vuetify, Quasar, Reka UI and Nuxt UI, you might want to create your own preset for a different UI library or a custom component set.
This guide walks through the process of building a custom UI preset for Enforma.
An Enforma preset gives you control over:
- Input mappings: Which component to use for each type of input
- Configuration options: How forms are rendered and how they behave
Step-by-Step Guide
1. Set Up Your Project Structure
Create a dedicated file for your preset:
src/
presets/
my-ui-preset.js
components/
MyCustomField.vue // Only needed for custom field components2. Create an Input Map
First, map each field type to the appropriate component:
// src/presets/my-ui-preset.js
import {
MyTextInput,
MyPasswordInput,
MySelect,
MyCheckbox,
MyRadioGroup,
MyTextarea,
MyDatePicker,
MyButton
} from 'my-ui-library';
// Import any custom wrapper components (only if needed)
import MyCustomField from './components/MyCustomField.vue';
const myUiInputs = {
// Input components
text: MyTextInput,
password: MyPasswordInput,
email: MyTextInput, // Reuse with
number: MyTextInput, // Reuse with type="number"
select: MySelect,
checkbox: MyCheckbox,
radio: MyRadioGroup,
textarea: MyTextarea,
date: MyDatePicker,
custom: MyCustomField,
}
function setInputComponentOnFields(
fieldProps: any
) {
if (fieldProps.inputComponent && 'object' !== typeof fieldProps.inputComponent) {
// if the component is not already a Vue component
fieldProps.inputComponent = inputComponents[fieldProps.inputComponent] || InputText
} else if (!fieldProps.inputComponent) {
// default to InputText
fieldProps.inputComponent = InputText
}
return fieldProps
}3. Set Configuration Options
Add custom configuration options including button customization:
const myUiPresetConfig = {
pt: {
// Input components
wrapper: {
class: 'form-control',
},
label: {
class: 'form-label',
},
// Button configuration (no custom components needed!)
submit: {
as: MyButton, // Use your UI library's button
class: 'btn btn-primary',
content: 'Submit Form' // Custom content
},
reset: {
as: MyButton,
class: 'btn btn-secondary',
content: 'Reset Form'
},
// Repeatable button configuration
repeatable: {
wrapper: { class: 'repeatable-wrapper' },
items: { class: 'repeatable-items' },
item: { class: 'repeatable-item' },
actions: { class: 'repeatable-actions' },
itemActions: { class: 'repeatable-item-actions' },
add: {
as: MyButton,
class: 'btn btn-success',
content: '<i class="icon-plus"></i> Add Item'
},
remove: {
as: MyButton,
class: 'btn btn-danger btn-sm',
content: '<i class="icon-trash"></i>'
},
moveUp: {
as: MyButton,
class: 'btn btn-info btn-sm',
content: '↑'
},
moveDown: {
as: MyButton,
class: 'btn btn-info btn-sm',
content: '↓'
}
}
}
}4. Add Transformers
For components that expect different value formats:
const myUiPresetConfig = {
pt: {
// Default props...
},
transformers: {
field_props: [setInputComponentOnFields]
}
};5. Implement Function to Change Global Options
import {getGlobalConfig, setGlobalConfig } from '@encolajs/enforma'
export default function useMyUiPreset() {
const currentConfig = getGlobalConfig()
const mergedConfig = {
// implement here how your config options are merged
// with the currently existing global options
// if you want you can ignore the current global config
}
setGlobalConfig(mergedConfig)
}6. Register Your Preset
Use your preset when creating the Enforma instance:
// main.js
import { createApp } from 'vue'
import { EnformaPlugin } from '@encolajs/enforma'
import { useMyUiPreset } from './presets/my-ui-preset'
import App from './App.vue'
const app = createApp(App);
// Configure Enforma with your preset
app.use(EnformaPlugin, {})
// Use the preset
useMyUiPreset()
app.mount('#app')PrimeVue Preset Source Code
The best way to learn how to write a preset is to look at one of the presents inside the library. Below is the exact code from the PrimeVue preset with comments to guide you in making your own presets
import { Component } from 'vue'
import RepeatableAddButton from './primevue/RepeatableAddButton.vue'
import {
InputText,
Select,
AutoComplete,
DatePicker,
ToggleSwitch,
Button,
} from 'primevue'
import { FieldController, FieldControllerExport, FormController } from '@/types'
import {
DeepPartial,
EnformaConfig,
setGlobalConfig,
getGlobalConfig,
} from '@/utils/useConfig'
import { deepMerge } from '@/utils/configUtils'
const inputComponents: Record<string, Component> = {
input: InputText,
select: Select,
autocomplete: AutoComplete,
datepicker: DatePicker,
toggle: ToggleSwitch,
switch: ToggleSwitch,
}
/**
* Function that ensures an input field is using a PrimeVue component
* It uses the `fieldMap` to convert something like `select`
* into the Select component from PrimeVue
*/
function usePrimeVueComponent(
fieldProps: any,
field: FieldControllerExport,
formState: FormController,
config: any
) {
if (
fieldProps.inputComponent &&
'object' !== typeof fieldProps.inputComponent
) {
// if the component is not already a Vue component
fieldProps.inputComponent =
inputComponents[fieldProps.inputComponent] || InputText
} else if (!fieldProps.inputComponent) {
// default to InputText
fieldProps.inputComponent = InputText
}
return fieldProps
}
/**
* Most PrimeVue components use labelId and modelValue props
* This function adds them to the input field component
*/
function setPrimeVueSpecificProps(
fieldProps: any,
field: FieldControllerExport,
formState: FormController,
config: any
) {
fieldProps.inputProps.labelId = `label-${field.value.id}`
fieldProps.inputProps.modelValue = field.value.value
return fieldProps
}
/**
* Applies the PrimeVue preset to the global configuration
* This function modifies the global configuration
* by merging the PrimeVue preset with the existing global configuration
*/
export default function usePrimeVuePreset(
components?: Record<string, Component>
): void {
/**
* Because we don't know which PrimeVue components are used in an app
* the developer must provide the list of components after importing them
*/
if (components) {
Object.keys(components).map(
(key) => (inputComponents[key] = components[key])
)
}
const currentConfig = getGlobalConfig()
const primeVuePreset: DeepPartial<EnformaConfig> = {
components: {
// we're using the `pass-through` for most of the customization
// this component is needed because of PrimeVue uses `label` for the prop
repeatableAddButton: RepeatableAddButton,
},
/**
* Pass-Through configuration
* Props to be passed added to various components
*/
pt: {
actions: {
class: 'flex gap-2',
},
error: {
class: 'text-red-500',
},
submit: {
as: Button,
},
reset: {
as: Button,
severity: 'secondary',
},
repeatable: {
wrapper: {},
items: {},
item: {},
actions: {},
itemActions: {},
add: {
as: Button,
severity: 'secondary',
icon: 'pi pi-plus',
content: 'Add', // this is passed as `label` inside the component
type: 'button',
},
remove: {
as: Button,
severity: 'danger',
icon: 'pi pi-trash',
content: null,
type: 'button',
},
moveUp: {
as: Button,
severity: 'secondary',
icon: 'pi pi-arrow-up',
content: null,
type: 'button',
},
moveDown: {
as: Button,
severity: 'secondary',
icon: 'pi pi-arrow-down',
content: null,
type: 'button',
},
},
},
/**
* Functions to change the props of Enforma components before rendering
*/
transformers: {
field_props: [usePrimeVueComponent, setPrimeVueSpecificProps],
},
}
// Merge the current config with the PrimeVue preset
const mergedConfig = {
...currentConfig,
// we are using deep merge to preserve values from the default preset
// this would not be necessary if the primeVuePreset.pt would be complete
pt: deepMerge(currentConfig.pt, primeVuePreset.pt),
components: {
...currentConfig.components,
...primeVuePreset.components,
},
transformers: {
...currentConfig.transformers,
// this will remove existing field_props transformers
// which is not something that you want all the time
// if you have your own transformers configured
// before the preset they will be removed
field_props: [
...(currentConfig.transformers?.field_props || []),
...(primeVuePreset.transformers?.field_props || []),
],
},
}
// Set the global configuration
setGlobalConfig(mergedConfig)
}