Skip to content

Recipe — Custom Form Input

Goal: Add a new input type that plugs into the schema-driven form system, so route configs can declare 'type' => 'my-input' and get your component rendered.

Time: ~10 minutes.

The Three Layers

A custom input needs three artefacts:

LayerWhatGenerated by
Vue componentThe actual input UImodularous:create:vue:input
HydrateTransforms schema config into component propsmodularous:create:input:hydrate
Registry entryTells the form system 'my-input' → VMyInputregisterInputType()

1. Scaffold the Vue component

bash
php artisan modularous:create:vue:input ColorPicker

This creates vue/src/js/components/inputs/VInputColorPicker.vue with:

  • Vue 3 Composition API setup
  • useInput(props, emit) wired in
  • useModelValue(props, emit) for v-model binding
  • Baseline v-text-field fallback — replace with your real UI

Edit the component:

vue
<script setup>
import { useInput, useModelValue } from '@/hooks'

const props = defineProps({
    modelValue: { type: String, default: null },
    schema:     { type: Object, required: true },
})

const emit = defineEmits(['update:modelValue'])

const { boundProps } = useInput(props, emit)
const { model } = useModelValue(props, emit)
</script>

<template>
    <v-text-field v-model="model" v-bind="boundProps" type="color" />
</template>

See useInput and useModelValue for the props they inject.

2. Scaffold the hydrate

bash
php artisan modularous:create:input:hydrate ColorPicker Billing

This creates modules/Billing/Hydrates/Inputs/ColorPickerHydrate.php:

php
namespace Modules\Billing\Hydrates\Inputs;

use Unusualify\Modularous\Hydrates\AbstractHydrate;

class ColorPickerHydrate extends AbstractHydrate
{
    public const HYDRATE_TYPE = 'color-picker';

    public function getOutputType(): string
    {
        return 'color-picker';
    }

    public function hydrate(array $schema): array
    {
        return array_merge($schema, [
            'type'        => $this->getOutputType(),
            'placeholder' => $schema['placeholder'] ?? '#000000',
            // add any computed/derived props here
        ]);
    }
}

See Hydrates for the hydrate contract.

3. Register the input type

In your app bootstrap (for example resources/js/app.js or a module's JS entrypoint):

js
import { registerInputType } from '@/components/inputs/registry'
import VInputColorPicker from '@/components/inputs/VInputColorPicker.vue'

registerInputType('color-picker', 'VInputColorPicker')
// Also register the component globally or locally import where used

The registry maps schema type values to component names. See Input Registry.

4. Use it in a route config

php
[
    'type'  => 'color-picker',
    'name'  => 'brand_color',
    'label' => 'Brand color',
]

The form system will:

  1. Run it through ColorPickerHydrate::hydrate()
  2. Output schema with type: 'color-picker'
  3. Map color-pickerVInputColorPicker via the registry
  4. Render with v-bind to the component

5. Verify

  1. Open the form.
  2. The colour-picker input renders in place of a plain text field.
  3. Pick a colour — it propagates through v-model to the form state.
  4. Submit — the hex string persists to the model.

Adding Validation

Validation rules live in the schema — your hydrate can push them:

php
public function hydrate(array $schema): array
{
    return array_merge($schema, [
        'type'  => $this->getOutputType(),
        'rules' => array_merge($schema['rules'] ?? [], [
            'hex-color', // your rule
        ]),
    ]);
}

Register custom rules with useValidation — see useValidation.

Adding a Props Factory

If other components want to reuse your input's props, expose a makeColorPickerProps factory:

js
import { propsFactory } from 'vuetify/util'

export const makeColorPickerProps = propsFactory({
    format:     { type: String, default: 'hex' },
    showAlpha:  Boolean,
}, 'colorPicker')

Shipping as Part of a Module

If the input belongs to a specific module:

  • Component: modules/Billing/Resources/assets/js/components/VInputColorPicker.vue
  • Hydrate: modules/Billing/Hydrates/Inputs/ColorPickerHydrate.php
  • Register in the module's JS entrypoint so it loads only when the module is active

Common Pitfalls

SymptomFix
Input renders as plain text fieldRegistry mapping missing — confirm registerInputType() runs before forms render
Schema changes aren't reflectedClear the hydrate cache: php artisan modularous:cache:clear
modelValue doesn't update on submitEmit update:modelValue (use useModelValue, don't mutate prop)
Props missing in componentHydrate isn't merging them — check hydrate() return value

Next Steps

  • Hydrates — full schema contract
  • Form Inputs — browse existing inputs for naming / prop conventions
  • Vue Hooks — every hook your input can consume