ffhs / filament-package_ffhs_custom_forms
Custom forms for Fernfachhochschule Schweiz (FFHS)
Requires
- php: ^8.2
- codeat3/blade-carbon-icons: ^2.18.2
- coolsam/flatpickr: ^4.0
- davidhsianturi/blade-bootstrap-icons: ^1.4
- filament/filament: ^3.0
- filament/notifications: ^3.0
- filament/spatie-laravel-translatable-plugin: ^3.2
- guava/filament-icon-picker: ^2.0
- guzzlehttp/guzzle: ^7.0.1
- laravel/framework: ^11.31
- laravel/prompts: ^0.3.1
- secondnetwork/blade-tabler-icons: ^3.26
- socialiteproviders/microsoft: ^4.2
- spatie/laravel-activitylog: ^4.7
- spatie/laravel-permission: ^6.4
- stechstudio/filament-impersonate: ^3.5
Requires (Dev)
- orchestra/testbench: ^9.0.0
- pestphp/pest: ^3.0.0
- pestphp/pest-plugin-laravel: ^3.0.0
- pestphp/pest-plugin-livewire: ^3.0.0
README
This plugin enables you to create and manage nested forms within FilamentPHP.
It provides a wide range of customizable form fields and supports behavioral rules and templates.
Features:
- β Multiple Form Types: Bind forms to different use cases (e.g. entities) through customizable form types.
- π§© General Fields: Define general fields for exports and reusability across form types.
- π Configurable Field Types: Includes various fields like Repeaters to add additional inputs dynamically.
- π€ drag & drop Use an intuitive form builder.
- π Reactive Rules: Set up rules that react to answers by changing field visibility or modifying selectable options.
- π Prioritized Select Field: Use enhanced select fields with prioritized or sorted options.
- π Template Snippets: Create reusable form templates to maximize reusability.
- π§± Custom Components: Easily embed CustomForms into your application using Filament components.
- ποΈ Multiple View Modes: Switch rendering styles based on where the form is used.
- π§ Extensible Architecture: Everything is designed to be customizable β add your own fields, rules, and templates.
- β¬οΈ Import/Export: You can import and export custom forms directly from the UI..
Screenshots
Editor
Templates
Fill Form
View Form
Rule Editor
General Fields
Installation
Install the package via composer:
composer require ffhs/filament-package_ffhs_custom_forms
Publish and run the migrations:
php artisan vendor:publish --tag="filament-package_ffhs_custom_forms-migrations"
php artisan migrate
Publish the config file with:
php artisan vendor:publish --tag="filament-package_ffhs_custom_forms-config"
The CustomForm plugin needs Icon Picker Plugin You can publish the config file with:
php artisan vendor:publish --tag="filament-icon-picker-config"
Optionally, you can publish the views using
php artisan vendor:publish --tag="filament-package_ffhs_custom_forms-views"
Usage
1. Register the Plugin
Add the plugin to your PanelProvider:
// App/Providers/Filament/xxPanelProvider ->plugins([CustomFormPlugin::make()])
2. Overview
A CustomForm is composed of two main parts:
-
CustomFormConfiguration β The blueprint that defines the forms behavior, fields, and validation rules.
-
CustomFields β Dynamic fields rendered within the form.
3. CustomFormConfiguration
Define multiple form configurations to serve different purposes.
For example:
- A registration form with specific dependencies and validations.
- A survey form with its own layout and logic.
To define a new form, extend the CustomFormConfiguration
class:
use Ffhs\FilamentPackageFfhsCustomForms\CustomForms\CustomForm\FormConfiguration\CustomFormConfiguration; class SimpleForm extends CustomFormConfiguration { public static function identifier(): string { return 'simple_form'; } public static function displayName(): string { return __('Simple Form'); } }
Key methods:
identifier()
β Uniquely links saved form entries to the corresponding configuration.displayName()
β Shown in Filamentβs form selection dropdown.
After creating a new configuration, register it in the ffhs_custom_forms
config file:
// config/ffhs_custom_forms.php 'forms' => [ SimpleForm::class, ],`
Notes:
- Add each configuration class to the
forms
array. - Run
php artisan config:clear
.
Features explained
β Form Configurations (FormTypes)
You can create different FormTypes, each with its own set of available FieldTypes and required GeneralFields. This allows you to customize forms for different application scenarios.
For example, you might have custom forms for requests and custom forms for applications. Now you can create two different FormTypes:
- One requires a general field for the study location
- The other does not include this field
This makes the form system flexible and reusable across various use cases.
CustomFormConfiguration.php
Other functions to overwrite
Method Signature | Description |
---|---|
formFieldTypes(): array |
Returns the available custom field types that can be used in the form editor. - Source: CustomFieldType::getSelectableFieldTypes() - Purpose: Determines which field types are selectable when building or editing forms. |
getRuleTriggerTypes(): array |
Returns the list of trigger types used in form rule definitions. - Source: config('ffhs_custom_forms.rule.trigger') - Example values: on_change , on_submit , etc. |
getRuleEventTypes(): array |
Returns the list of event types that define what happens when a rule is triggered. - Source: config('ffhs_custom_forms.rule.event') - Example values: show_field , hide_field , set_value , etc. |
editorFieldAdder(): array |
Returns the available field adders used in the form editor interface. - Source: config('ffhs_custom_forms.editor.field_adders') - Purpose: Defines which buttons or UI elements are available for adding fields in the editor. |
overwriteViewModes(): array |
Returns an array of custom view modes that override the default ones. - Default: An empty array, meaning no overrides by default. - Use case: Can be extended by child classes to customize how the form is rendered. |
abstract public static function identifier(): string |
An abstract method that must be implemented by each concrete FormType class. - Purpose: Returns a unique identifier string for the FormType (e.g. "application" , "registration" ). |
displayViewMode(): string |
Returns the view mode to use when displaying the form. - Default: Uses defaultViewMode() . |
displayEditMode(): string |
Returns the edit mode to use when modifying the form. - Default: Uses defaultViewMode() . |
displayCreateMode(): string |
Returns the create mode to use when creating a new instance of the form. - Default: Uses defaultViewMode() . |
defaultViewMode(): string |
Returns the default mode name, which is "default" unless overridden. |
CustomFields
A CustomField consists of:
- A FieldType β Defines the type and logic of the field (e.g.
TextType
,SelectType
, etc.). - A set of Options β Configuration values such as placeholder text, min/max values, selectable choices, etc.
- A FieldTypeView β Which can change on a different ViewMode
Predefined Field Types
π₯ Input Fields
TextType::class
EmailType::class
NumberType::class
TextAreaType::class
DateTimeType::class
DateType::class
DateRangeType::class
FileUploadType::class
π Choice Fields
SelectType::class
RadioType::class
CheckboxListType::class
ToggleButtonsType::class
β¨ Special Fields
TagsType::class
KeyValueType::class
ColorPickerType::class
IconSelectType::class
π Layout Components
SectionType::class
FieldsetType::class
GroupType::class
TitleType::class
TextLayoutType::class
DownloadType::class
ImageLayoutType::class
SpaceType::class
Create your own CustomFieldType
Create your own reusable field type by implementing a CustomFieldType and a matching ViewComponent.
- Define the Field Type Class
class MyType extends CustomFieldType { public function getTranslatedName(): string { return __('type.label'); } public static function identifier(): string { return 'text'; } public function viewModes(): array { return [ 'default' => MyTypeView::class, ]; } public function icon(): string { return 'bi-input-cursor-text'; } }
- Register Type in Config
// config/ffhs_custom_forms.php 'custom_field_types' => [ MyType::class, [..] ]
- Define the FieldTypeView
class MyTypeView implements FieldTypeView { use HasDefaultViewComponent; public function getFormComponent( CustomFieldType $type, CustomField $record, array $parameter = [] ): FormsComponent { /** @var TextInput $input */ return $input = $this->makeComponent(TextInput::class, $record); } public function getInfolistComponent( CustomFieldType $type, CustomFieldAnswer $record, array $parameter = [] ): InfolistsComponent { /** @var TextEntry $input */ return $input = $this->makeComponent(TextEntry::class, $record); } }
Other Functions
These optional methods can be overridden in your custom CustomFieldType
class to control behavior during field
editing, rendering, saving, and cloning.
Method Signature | Description |
---|---|
canBeDeactivate(): bool |
Returns true if this field type can be deactivated (Hidden), otherwise false . |
generalTypeOptions(): array |
Returns a list of general type options for this field type. |
extraTypeOptions(): array |
Returns additional, type options specific to this field type. |
beforeSaveField(CustomField $field, array $data): void |
Called before a field is saved. Use it to preprocess or validate field data before itβs persisted. |
afterSaveField(CustomField $field, array $data): void |
Called after the field has been saved. Use it to trigger post-save logic such as logging or syncing related data. |
afterCreateField(CustomField $field, array $data): void |
Called after a new field is created. Useful for generating default options or setting up linked records. |
afterDeleteField(CustomField $field): void |
Called after a field has been deleted. Can be used to clean up orphaned records or perform audits. |
mutateOnCloneField(array $data, CustomField $original): array |
Allows you to modify data when a field is cloned (on dissolve template). Example: Reset values or generate unique identifiers. |
isFullSizeField(): bool |
Returns true if the field should take the full width of the form layout. Useful for large inputs like TextArea or FileUpload . Default: false . |
getEditorFieldBadge(array $rawData): ?string |
Returns an optional badge label shown in the field editor. Return null for no badge. Used to identify general fields and templates. |
getEditorFieldTitle(array $rawData, CustomForm $form): string |
Returns the title shown for the field in the form editor. Useful for showing the question label, field name, or summary. |
fieldEditorExtraComponent(array $rawData): ?string |
Returns a reference to a custom editor UI component (e.g. Vue or Blade). Rendered in the field's editor panel. Return null to disable. |
getEditorActions(string $key, array $rawData): array |
Returns a list of editor actions (buttons, dropdowns) for the field editor. Example: Returns delete, edit, and activation actions, with visibility depending on canBeDeactivate() . |
TypeOptions
The TypeOptions are options components which can add to a field-type to modify the components.
The TypeOptions can automatically apply some effects if the
return $input = $this->makeComponent(TextInput::class, $record);
is used in the TypeView.
Predefined Type Option Classes
These classes define configurable TypeOptions used in CustomFormFields.
ActionLabelTypeOption.php
AlpineMaskOption.php
BooleanOption.php
ColumnsOption.php
ColumnSpanOption.php
DateFormatOption.php
HelperTextTypeOption.php
IconOption.php
ImaginaryTypeOption.php
InLineLabelOption.php
InlineOption.php
MaxAmountOption.php
MaxLengthOption.php
MaxSelectOption.php
MaxValueOption.php
MinAmountOption.php
MinLengthOption.php
MinSelectOption.php
MinValueOption.php
NewLineOption.php
RelatedFieldOption.php
ReorderableTypeOption.php
RequiredOption.php
ShowAsFieldsetOption.php
ShowInViewOption.php
ShowLabelOption.php
ValidationAttributeOption.php
ValidationMessageOption.php
Using TypeOptions in a CustomFieldType
You can add extra type options to your custom field by overriding extraTypeOptions()
or generalTypeOptions()
:
// MyType.php public function extraTypeOptions(): array { return [ 'alpine_mask' => AlpineMaskOption::make() ->modifyDefault(fn($oldDefault) => '...'), 'max_length' => MaxLengthOption::make() ->modifyOptionComponent(fn($component) => $component->columnStart(1)), 'min_length' => MinLengthOption::make(), ]; }
- Each key represents the option name.
- The value is an instance of a
TypeOption
, usually created via::make()
. - You can customize how the option behaves using methods like
modifyDefault()
ormodifyOptionComponent()
.
Creating a custom TypeOption
To create your own option, extend the TypeOption
class:
class MyTypeOption extends TypeOption { public function getDefaultValue(): mixed { return null; } public function getComponent(string $name): Component { return Toggle::make($name) ->label('MyTypeOption') ->columnSpanFull() ->live(); } public function modifyFormComponent(FormsComponent $component, mixed $value): FormsComponent { return $component->modify(...); } public function modifyInfolistComponent(InfolistComponent $component, mixed $value): InfolistComponent { return $component->modify(...); } }
Other optional functions
Method | Purpose |
---|---|
mutateOnFieldSave() |
Modify data before it's saved to the field |
mutateOnFieldLoad() |
Modify data when it's loaded |
beforeSaveField() |
Hook before the field is saved (via reference) |
afterSaveField() |
Hook after the field is saved (via reference) |
afterCreateField() |
Triggered right after the field is created |
beforeDeleteField() |
Hook before the field is deleted |
afterDeleteField() |
Hook after the field is deleted |
mutateOnFieldClone() |
Modify option data when cloning the field |
canBeOverwrittenByNonField() |
Whether this option can be overwritten globally or externally (default: true ) |
Use FastTypeOption
FastTypeOption allows you to quickly add custom options directly in your FieldType
and react to them in your
FieldTypeView
.
//MyType.php public function extraTypeOptions(): array { return [ 'my_option' => FastTypeOption::makeFast( default: false, Toggle::make('my_option') ) ]; }
// MyTypeView.php $myOption = $this->getOptionParameter($record, 'my_option'); /** @var TextInput $input */ $input = $this->makeComponent(TextInput::class, $record); if($myOption){ $input->modify(...); } return $input
OptionsGroups
OptionsGroups help organize multiple related TypeOptions into logical collections, making your field configuration cleaner and easier to manage.
In the image TypeOptions are marked red and the OptionGroups are marked blue.
How to use OptionGroups?
//MyType.php public function extraTypeOptions(): array { return [ TypeOptionGroup::make('MyGroup', [ 'alpine_mask' => AlpineMaskOption::make(), 'max_length' => MaxLengthOption::make(), 'min_length' => MinLengthOption::make(), ]), ValidationTypeOptionGroup::make(), LayoutOptionGroup::make() ->mergeTypeOptions([ 'my_option' => MyTypeOption::make(), ]) ->removeTypeOption('...'), ]; }
Rules: Events and Triggers
Rules can be divided into events and triggers. Triggers cause events to be executed. A rule can consist of multiple triggers that can be combined using 'AND' or 'OR' logic and can execute multiple events.
Predefined Events
HideEvent
VisibleEvent
DisabledEvent
RequiredEvent
ChangeOptionsEvent
Predefined Triggers
IsInfolistTrigger
ValueEqualsRuleTrigger
AlwaysRuleTrigger
Create own Trigger
- Create an MyTrigger class
// MyTrigger.php class MyTrigger extends FormRuleTriggerType { use HasTriggerEventFormTargets; // <= Adds an CustomField Select public static function identifier(): string { return 'my_trigger'; } public function getDisplayName(): string { return 'My Trigger'; } public function isTrigger(array $arguments, mixed &$target, RuleTrigger $rule): bool { return ...; } public function getFormSchema(): array { return [ Toggle::make('option for my trigger'), this->getTargetSelect() // <= Adds an CustomField Select ]; } }
- Add your trigger class in the config file:
// config/ffhs_custom_forms.php 'trigger' => [ MyTrigger::class ... ]
Create own Event
- Create an MyEvent class
// MyTrigger.php class MyEvent extends FormRuleEventType { use HasTriggerEventFormTargets; // <= Adds an CustomField Select public static function identifier(): string { return 'my_event'; } public function getDisplayName(): string { return 'My Event'; } public function getFormSchema(): array { return [ Toggle::make('option for my trigger'), this->getTargetSelect() // <= Adds an CustomField Select ]; } // Implement one or more handler functions below depending on your use case: // - handleAnswerLoadMutation // - handleAnswerSaveMutation // - handleBeforeRender // - handleAfterRenderForm // - handleAfterRenderInfolist }
- Add your event class in the config file:
// config/ffhs_custom_forms.php 'event' => [ MyEvent::class ... ],
Embed CustomForms
You can embed CustomForms or their answers in various parts of your application using specialized components. Below are examples of usage patterns and customization options.
Editor
Embed the form editor in your layout using CustomFormEditor
:
CustomFormEditor::make('custom_forms') ->relationship('customForm')
Form Answer Edit
Embed a form for editing its answer using EmbeddedCustomForm
.
Basic usage:
EmbeddedCustomForm::make('custom_form_answer') ->relationship('customFormAnswer')
With automatic saving:
EmbeddedCustomForm::make('custom_form_answer') ->relationship('customFormAnswer') ->autoSave()
Split layout by section type:
use Ffhs\FilamentPackageFfhsCustomForms\CustomFieldType\LayoutType\Types; EmbeddedCustomForm::make('custom_form_answer') ->relationship('customFormAnswer') ->useLayoutTypeSplit() ->layoutTypeSplit(SectionType::make())
Split layout by specific fields:
EmbeddedCustomForm::make('custom_form_answer') ->relationship('customFormAnswer') ->useFieldSplit() ->fieldSplit(CustomField....)
Split layout by pose (e.g. question number range):
EmbeddedCustomForm::make('custom_form_answer') ->relationship('customFormAnswer') ->usePoseSplit() ->poseSplitStart(5) ->poseSplitEnd(10)
View answer on an infolist
Embed a view for its answer using EmbeddedCustomForm
.
Basic usage:
EmbeddedAnswerInfolist::make('custom_form_answer') ->relationship('customFormAnswer')
Split layout by section type:
EmbeddedAnswerInfolist::make('custom_form_answer') ->relationship('customFormAnswer') ->useLayoutTypeSplit() ->layoutTypeSplit(SectionType::make())
Split layout by specific fields:
EmbeddedAnswerInfolist::make('custom_form_answer') ->relationship('customFormAnswer') ->useFieldSplit() ->fieldSplit(CustomField....)
Split layout by pose (e.g. question number range):
EmbeddedAnswerInfolist::make('custom_form_answer') ->relationship('customFormAnswer') ->usePoseSplit() ->poseSplitStart(5) ->poseSplitEnd(10)
Testing
composer install ./vendor/bin/testbench vendor:publish --tag="filament-package_ffhs_custom_forms-migrations" ./vendor/bin/testbench workbench:build ./vendor/bin/pest test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Postcards
We highly appreciate you sending us a postcard. You'll find our address on our page.
License
The MIT License (MIT). Please see License File for more information.