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:
| Layer | What | Generated by |
|---|---|---|
| Vue component | The actual input UI | modularous:create:vue:input |
| Hydrate | Transforms schema config into component props | modularous:create:input:hydrate |
| Registry entry | Tells the form system 'my-input' → VMyInput | registerInputType() |
1. Scaffold the Vue component
php artisan modularous:create:vue:input ColorPickerThis creates vue/src/js/components/inputs/VInputColorPicker.vue with:
- Vue 3 Composition API setup
useInput(props, emit)wired inuseModelValue(props, emit)forv-modelbinding- Baseline
v-text-fieldfallback — replace with your real UI
Edit the component:
<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
php artisan modularous:create:input:hydrate ColorPicker BillingThis creates modules/Billing/Hydrates/Inputs/ColorPickerHydrate.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):
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 usedThe registry maps schema type values to component names. See Input Registry.
4. Use it in a route config
[
'type' => 'color-picker',
'name' => 'brand_color',
'label' => 'Brand color',
]The form system will:
- Run it through
ColorPickerHydrate::hydrate() - Output schema with
type: 'color-picker' - Map
color-picker→VInputColorPickervia the registry - Render with
v-bindto the component
5. Verify
- Open the form.
- The colour-picker input renders in place of a plain text field.
- Pick a colour — it propagates through
v-modelto the form state. - Submit — the hex string persists to the model.
Adding Validation
Validation rules live in the schema — your hydrate can push them:
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:
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
| Symptom | Fix |
|---|---|
| Input renders as plain text field | Registry mapping missing — confirm registerInputType() runs before forms render |
| Schema changes aren't reflected | Clear the hydrate cache: php artisan modularous:cache:clear |
modelValue doesn't update on submit | Emit update:modelValue (use useModelValue, don't mutate prop) |
| Props missing in component | Hydrate 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