Skip to content

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:

  1. Input mappings: Which component to use for each type of input
  2. 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 components

2. Create an Input Map

First, map each field type to the appropriate component:

js
// 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:

js
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:

js
const myUiPresetConfig = {
  pt: {
    // Default props...
  },
    
  transformers: {
    field_props: [setInputComponentOnFields]
  }
};

5. Implement Function to Change Global Options

js
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:

js
// 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

ts
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)
}

Released under the MIT License