juaniquillo / input-component-action
Form input component creation and management action for Crud Assistant
Package info
github.com/juaniquillo/input-component-action
pkg:composer/juaniquillo/input-component-action
Requires
- php: ^8.3
- illuminate/support: ^12.0||^13.0
- juaniquillo/base-backend-component: ^0.1.0
- juaniquillo/crud-assistant: ^0.3.0
- juaniquillo/laravel-backend-component: ^0.12.0
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/pao: ^1.1
- laravel/pint: ^1.27
- orchestra/testbench: ^10.9
- pestphp/pest: ^4.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Turn CrudAssistant inputs into renderable Laravel Backend Components.
The action walks an input tree and produces wrapped, label, input, error and help-text components, using recipes to customize every slot.
Tip
Laravel Boost Skill
If you use Laravel Boost, install the AI skill:
php artisan boost:add-skill https://github.com/juaniquillo/input-component-action
Requirements
- PHP
^8.3 juaniquillo/crud-assistant^0.3.0juaniquillo/laravel-backend-component^0.12.0juaniquillo/base-backend-component^0.1.0- Laravel (for rendering)
Install
composer require juaniquillo/input-component-action
The package registers InputComponentActionServiceProvider automatically.
Quick start
use Juaniquillo\CrudAssistant\CrudAssistant; use Juaniquillo\CrudAssistant\Inputs\DefaultInput; use Juaniquillo\InputComponentAction\Bags\DefaultAttributeBag; use Juaniquillo\InputComponentAction\Containers\InputComponentOutput; use Juaniquillo\InputComponentAction\InputComponentAction; use Juaniquillo\InputComponentAction\Recipes\InputComponentRecipe; $name = new DefaultInput('name', 'Name'); $name->setRecipe(new InputComponentRecipe( attributeBag: (new DefaultAttributeBag)->setInputAttributes([ 'placeholder' => 'Your name', ]), helpText: 'How should we call you?', )); $crud = CrudAssistant::make([$name]); $action = InputComponentAction::make( values: ['name' => 'Alice'], errors: ['name' => 'This field is required.'], ); $output = $crud->execute($action); /** @var InputComponentOutput $output */ $component = $output->inputs->get('name');
Render it in Blade:
{{ $component }}
InputComponentOutput exposes:
inputs— one component per resolved input/collection, keyed by namemeta— aDataContainerfor action metadata
How it works
InputComponentActionruns through CrudAssistant.- Each input is resolved by its group (
DefaultInputGroupby default). - The group composes up to five slots:
- wrapper
- label
- help text
- input
- error
- Each slot is built by
SlotComposer(orInputComposerfor the input slot). - Sub-elements are built by
ChildInputResolver, which skips inputs excluded byignore()oronlyFor(). - The result is a
MainBackendComponenttree ready for Blade.
Recipes
A recipe customizes how one input is rendered. Recipes are addressed to the
action through CrudAssistant's normal setRecipe() API.
$input->setRecipe(new InputComponentRecipe( attributeBag: (new DefaultAttributeBag) ->setWrapperAttributes(['class' => 'field']) ->setInputAttributes(['type' => 'email']), themeBag: (new DefaultThemeBag) ->setLabelTheme(['color' => 'gray']) ->setInputTheme(['inputs' => 'default']), disableBag: (new DefaultDisableBag)->setDisableError(), inputValue: fn ($input, $values, $model) => $model?->email, label: 'Email address', helpText: 'We will never share it.', ));
Constructor options
InputComponentRecipe accepts:
| Option | Purpose |
|---|---|
inputGroup |
Group layout class |
themeManager |
Per-input theme manager |
componentBag |
Component classes/types per slot |
attributeBag |
Attributes per slot |
themeBag |
Themes per slot |
hookBag |
Hooks per slot |
valueBag |
Custom value manager |
errorBag |
Custom error manager |
disableBag |
Disable flags |
inputValue |
Value or value closure |
inputError |
Error or error closure |
label |
Label or label closure |
useParentValue |
Compare against the parent input value |
labelAsInputContent |
Use the label as the input content |
emptyLabel |
Render the label without content |
valueAsInputContent |
Use the value as input content |
helpText |
Help text or closure |
checkable |
Add checked when selected |
selectable |
Add selected when selected |
useParentName |
Inherit the parent input's name attribute |
useInputNameAsValue |
Fall back to the input name when no value is resolved |
Select options
SelectOptionComponentRecipe extends InputComponentRecipe and configures the
usual option defaults:
use Juaniquillo\CrudAssistant\Inputs\OptionInput; use Juaniquillo\CrudAssistant\Inputs\SelectInput; use Juaniquillo\BackendComponents\Enums\ComponentEnum; use Juaniquillo\InputComponentAction\Bags\DefaultComponentBag; use Juaniquillo\InputComponentAction\Recipes\SelectOptionComponentRecipe; $select = new SelectInput('language', 'Language'); $select->setRecipe(new InputComponentRecipe( componentBag: (new DefaultComponentBag)->setInputType(ComponentEnum::SELECT), )); $select->setSubElements(CrudAssistant::make([ (new OptionInput('php', 'PHP'))->setRecipe( new SelectOptionComponentRecipe(inputValue: 'php'), ), (new OptionInput('js', 'JavaScript'))->setRecipe( new SelectOptionComponentRecipe(inputValue: 'js'), ), ]));
When the action receives ['language' => ['php', 'js']], both options are
marked selected.
Radio and checkbox options
RadioComponentRecipe and CheckboxComponentRecipe configure a single choice
inside a group. They inherit the parent input's name, take the input name as
the value by default, and mark the option checked when it matches the parent
value:
use Juaniquillo\BackendComponents\Enums\ComponentEnum; use Juaniquillo\CrudAssistant\CrudAssistant; use Juaniquillo\CrudAssistant\Inputs\DefaultInput; use Juaniquillo\CrudAssistant\Inputs\RadioGroupInput; use Juaniquillo\InputComponentAction\Bags\DefaultComponentBag; use Juaniquillo\InputComponentAction\Recipes\InputComponentRecipe; use Juaniquillo\InputComponentAction\Recipes\RadioComponentRecipe; $group = new RadioGroupInput('choice', 'Choice'); $group->setRecipe(new InputComponentRecipe( componentBag: (new DefaultComponentBag)->setInputType(ComponentEnum::DIV), )); $group->setSubElements(CrudAssistant::make([ (new DefaultInput('a', 'A'))->setRecipe(new RadioComponentRecipe), (new DefaultInput('b', 'B'))->setRecipe(new RadioComponentRecipe), ]));
Use CheckboxComponentRecipe the same way for checkbox groups, or on its own for
a single checkbox. Both recipes only fill in defaults: a supplied component bag,
group, or disable bag is kept, so custom components (such as a switch) and
label/error groups keep working.
SelectOptionComponentRecipe, RadioComponentRecipe, and
CheckboxComponentRecipe share the ChoiceComponentRecipe base.
Bags
All slot configuration lives in five bags.
DefaultComponentBag
Component class and type per slot:
$bag = (new DefaultComponentBag) ->setWrapperType(ComponentEnum::FIELDSET) ->setLabelType(ComponentEnum::LEGEND) ->setInputType(ComponentEnum::TEXTAREA) ->setErrorType(ComponentEnum::PARAGRAPH) ->setHelpTextType(ComponentEnum::SMALL);
Custom component classes or closures:
use Juaniquillo\BaseBackendComponent\Contracts\ThemeManager; $bag->setInputComponent( fn (string|BackedEnum $type, ThemeManager $manager) => new MyInput($type, $manager), );
DefaultAttributeBag
(new DefaultAttributeBag) ->setWrapperAttributes(['class' => 'field']) ->setLabelAttributes(['class' => 'field-label']) ->setInputAttributes(['placeholder' => 'Type here']) ->setErrorAttributes(['class' => 'field-error']) ->setHelpTextAttributes(['class' => 'field-help']);
Every slot accepts an array or a closure:
->setInputAttributes(fn ($input, $type) => [ 'name' => $input->getName(), 'data-type' => $type instanceof BackedEnum ? $type->value : $type, ]);
DefaultThemeBag
(new DefaultThemeBag) ->setWrapperTheme(['forms' => 'one-column']) ->setLabelTheme(['color' => 'gray']) ->setInputTheme(['inputs' => 'default']) ->setErrorTheme(['color' => 'red']) ->setHelpTextTheme(['font' => 'sm']);
DefaultHookBag
Hooks receive the built component and can replace or mutate it:
use Juaniquillo\BaseBackendComponent\Contracts\BackendComponent; (new DefaultHookBag)->setInputHook( function (BackendComponent $component, InputInterface $input): BackendComponent { return $component->setAttribute('data-hook', 'yes'); }, );
Available hooks: input, wrapper, label, error, helpText.
DefaultDisableBag
(new DefaultDisableBag) ->setDisableWrapper() ->setDisableLabel() ->setDisableError() ->setDisableHelpText() ->setDisableInputValue() ->setDisableDefaultNameAttribute() ->setDisableDefaultIdAttribute() ->setDisableDefaultForAttribute();
Slots
Enums\Slot identifies each slot and its default component type:
| Slot | Default |
|---|---|
Slot::WRAPPER |
div |
Slot::LABEL |
label |
Slot::INPUT |
text input |
Slot::ERROR |
paragraph |
Slot::HELP_TEXT |
div |
Support::resolveType($recipe, Slot::INPUT, $bag) resolves the effective type
with this priority:
- recipe component bag
- action/default component bag
Slot::defaultType()
Groups
Set a group per input or as the action default:
use Juaniquillo\InputComponentAction\Groups\LabelHelpTextInputErrorGroup; $input->setRecipe(new InputComponentRecipe( inputGroup: new LabelHelpTextInputErrorGroup, )); $action->setDefaultInputGroup(LabelHelpTextInputErrorGroup::class);
Built-in groups:
| Group | Slots |
|---|---|
DefaultInputGroup |
label, input, error |
InputErrorGroup |
input, error |
InputLabelErrorGroup |
input, label, error |
LabelHelpTextInputErrorGroup |
label, help text, input, error |
LabelContainerGroup |
label wrapping input, error |
SoleInputGroup |
input |
NoWrapInputGroup |
label, input, error (no wrapper element) |
NoWrapSoleInputGroup |
input (no wrapper element) |
Custom groups
Extend AbstractInputGroup and declare the slot order:
use Juaniquillo\InputComponentAction\Enums\Slot; use Juaniquillo\InputComponentAction\Groups\AbstractInputGroup; final class MyGroup extends AbstractInputGroup { protected array $slots = [Slot::LABEL, Slot::INPUT, Slot::ERROR]; }
Or implement InputGroup with IsInputGroup and the Has* traits for full
control:
use Juaniquillo\BaseBackendComponent\Contracts\BackendComponent; use Juaniquillo\BaseBackendComponent\Contracts\ContentComponent; final class LabelGroup implements InputGroup { use HasLabel, HasWrapper, IsInputGroup; public function getGroup(): BackendComponent|ContentComponent { $wrapper = $this->getWrapperComponent() ?? Support::getCollectionWrapper(); return $wrapper->setContents([ $this->getLabelComponent(), $this->getInputComponent(), ]); } }
Action configuration
$action = (new InputComponentAction($values, $errors)) ->setDefaultInputGroup(LabelHelpTextInputErrorGroup::class) ->setDefaultComponentBag($componentBag) ->setDefaultThemeBag($themeBag) ->setThemeManager(DefaultThemeManager::class) ->setModel($model) ->setValueManager($valueManager) ->setErrorManager($errorManager);
setThemeManager()accepts a class string, a closure returning aThemeManager, ornull(usesLocalThemeManager).setDefaultInputGroup()accepts a class string, a closure returning anInputGroup, ornull(usesDefaultInputGroup).setModel()is used as the final value fallback.setValueManager()/setErrorManager()can override the resolvers globally.
Value resolution
Default priority:
valuesarray passed to the action- recipe
inputValue(scalar or closure) model->{name}
A recipe valueBag overrides the action value manager; the action value
manager overrides DefaultValueManager.
Error resolution
Default priority:
- recipe
inputError errors[name](the last entry when it is an array)
A recipe errorBag overrides the action error manager; the action error
manager overrides DefaultErrorManager.
Theming
The default theme manager reads package themes. Use DefaultThemeManager for
package themes or LocalThemeManager for app themes (resources/views/_themes).
$action->setThemeManager(LocalThemeManager::class);
Themes are applied per slot through DefaultThemeBag.
Testing
composer test # Pest composer analyse # PHPStan (level 8) composer format # Pint composer qa # format → analyse → test
Laravel PAO is installed as a dev dependency. Inside an AI agent it replaces the output with compact JSON; in a normal terminal the output is unchanged.
Upgrade
See UPGRADE.md for the pre-1.0 breaking changes, including the
removal of InputComponentRecipe::make() and the per-slot bag contracts.
License
MIT. See LICENSE.md.