Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
7ba7db2
wip
Baspa Aug 1, 2025
aba2606
Fix styling
Baspa Aug 1, 2025
c981980
add many (to test) validation rules
Baspa Aug 1, 2025
5cd610d
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 1, 2025
25d511a
Fix styling
Baspa Aug 1, 2025
9894b56
wip
Baspa Aug 1, 2025
b9094fe
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 1, 2025
ba67c3c
Fix styling
Baspa Aug 1, 2025
af11b74
improve rules
Baspa Aug 2, 2025
78f1960
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 2, 2025
a4d3c51
Fix styling
Baspa Aug 2, 2025
23d4c5a
clean up code by using multiple components
Baspa Aug 2, 2025
920eb66
Fix styling
Baspa Aug 2, 2025
a99c605
visibility rules (wip)
Baspa Aug 2, 2025
5eacd38
remove unnecessary conditional logic schema
Baspa Aug 2, 2025
f27d7b6
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 2, 2025
ea6e55c
Fix styling
Baspa Aug 2, 2025
5b1f57e
improve item label
Baspa Aug 2, 2025
13e552f
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 2, 2025
156dc21
Fix styling
Baspa Aug 2, 2025
b96ab77
improve item label for the validation schema
Baspa Aug 2, 2025
2935b04
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 2, 2025
b01d49b
Fix styling
Baspa Aug 2, 2025
63a1a8c
only show relevant validation rule options
Baspa Aug 2, 2025
01c84fa
Merge branches 'feat/conditional-fields' and 'feat/conditional-fields…
Baspa Aug 2, 2025
5e04465
Fix styling
Baspa Aug 2, 2025
2bee7ef
Merge branch 'main' of github.com:backstagephp/fields into feat/condi…
Baspa Aug 11, 2025
61b31e8
Fix styling
Baspa Aug 11, 2025
d62353f
Merge branch 'main' of github.com:backstagephp/fields into feat/condi…
Baspa Aug 15, 2025
9d45054
Fix styling
Baspa Aug 15, 2025
1fb1d5c
v4
Baspa Aug 15, 2025
a5d3710
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 15, 2025
50709a0
Fix styling
Baspa Aug 15, 2025
a662637
wip
Baspa Aug 15, 2025
7a549bf
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 15, 2025
32c863b
Fix styling
Baspa Aug 15, 2025
a0b7814
fix: columns
Baspa Aug 15, 2025
f30e654
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Aug 15, 2025
626b94d
set columnSpan to full
Baspa Aug 15, 2025
0cbe588
fix rules form
Baspa Aug 15, 2025
0c54319
Merge branch 'main' into feat/conditional-fields
Baspa Sep 5, 2025
ef775ce
Fix styling
Baspa Sep 5, 2025
79e4f92
Merge branch 'main' into feat/conditional-fields
Baspa Sep 9, 2025
2bda467
Fix styling
Baspa Sep 9, 2025
c6f6a43
Merge branch 'main' of github.com:backstagephp/fields into feat/condi…
Baspa Sep 26, 2025
323f638
Refactor Tags field to handle empty strings and arrays
cursoragent Sep 26, 2025
71b8387
Fix styling
cursor[bot] Sep 26, 2025
bd5c715
disable options and inform user when no fields available
Baspa Sep 26, 2025
6bd0aec
docs: update readme
Baspa Sep 26, 2025
b60e67d
Fix: Correct conditional logic for empty and not empty operators
cursoragent Sep 26, 2025
7f40d97
Merge branch 'feat/conditional-fields' of github.com:backstagephp/fie…
Baspa Sep 26, 2025
31d1082
fix: unintended p
Baspa Sep 26, 2025
ba962d7
Fix: Add checks for field name before applying validation rules
cursoragent Sep 26, 2025
eca5b82
Merge branch 'main' of github.com:backstagephp/fields into feat/condi…
Baspa Sep 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,41 @@ class ContentResource extends Resource
}
```

### Field Configuration

#### Validation Rules

Each field supports validation rules that can be configured through the admin interface. The package includes support for all standard Laravel and Filament validation rules:

- **Basic Rules**: Required, nullable, filled
- **String Rules**: Min/max length, alpha, alphanumeric, email, URL
- **Numeric Rules**: Min/max values, integer, decimal, numeric
- **Date Rules**: Date format, before/after dates, date equals
- **Comparison Rules**: Same as field, different from field, greater/less than field
- **Conditional Rules**: Required if/unless, prohibited if/unless, required with/without
- **Pattern Rules**: Regex, starts/ends with, in/not in list
- **Database Rules**: Exists, unique

##### Field Dependencies

Validation rules can depend on other fields in the form:

- **Field Comparison**: Compare values with other fields (`same`, `different`, `greater_than`, etc.)
- **Conditional Requirements**: Make fields required based on other field values (`required_if`, `required_unless`)
- **Multi-field Dependencies**: Require fields based on multiple other fields (`required_with_all`, `required_without_all`)

When no other fields are available for dependency rules, the field selection will be disabled and show a helpful message.

#### Visibility Rules

Control when fields are shown or hidden based on conditions:

- **Conditional Display**: Show/hide fields based on other field values
- **Dynamic Forms**: Create adaptive forms that change based on user input
- **Complex Logic**: Support for multiple conditions and logical operators

The visibility system works seamlessly with validation rules to create intelligent, user-friendly forms.

### Making a resource page configurable

To make a resource page configurable, you need to add the `CanMapDynamicFields` trait to your page. For this example, we'll make a `EditContent` page configurable.
Expand All @@ -146,11 +181,7 @@ To make a resource page configurable, you need to add the `CanMapDynamicFields`

namespace Backstage\Resources\ContentResource\Pages;

use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Tabs;
use Filament\Forms\Components\Tabs\Tab;
use Filament\Forms\Form;
use Filament\Resources\Pages\EditRecord;
// ...
use Backstage\Fields\Concerns\CanMapDynamicFields;

class EditContent extends EditRecord
Expand Down
2 changes: 2 additions & 0 deletions src/Contracts/FieldContract.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public function getForm(): array;
public static function make(string $name, Field $field);

public static function getDefaultConfig(): array;

public function getFieldType(): ?string;
}
55 changes: 48 additions & 7 deletions src/Fields/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,34 @@
namespace Backstage\Fields\Fields;

use Backstage\Fields\Contracts\FieldContract;
use Backstage\Fields\Fields\FormSchemas\BasicSettingsSchema;
use Backstage\Fields\Fields\FormSchemas\ValidationRulesSchema;
use Backstage\Fields\Fields\FormSchemas\VisibilityRulesSchema;
use Backstage\Fields\Fields\Logic\ConditionalLogicApplier;
use Backstage\Fields\Fields\Logic\VisibilityLogicApplier;
use Backstage\Fields\Fields\Validation\ValidationRuleApplier;
use Backstage\Fields\Models\Field;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Support\Colors\Color;
use ReflectionObject;

abstract class Base implements FieldContract
{
public function getForm(): array
{
return $this->getBaseFormSchema();
return BasicSettingsSchema::make();
}

public function getRulesForm(): array
{
return [
...ValidationRulesSchema::make($this->getFieldType()),
...VisibilityRulesSchema::make(),
];
}

protected function getBaseFormSchema(): array
Expand Down Expand Up @@ -91,7 +106,7 @@ private function filterExcludedFields(array $schema): array

private function fieldContainsConfigKey($field, string $configKey): bool
{
$reflection = new \ReflectionObject($field);
$reflection = new ReflectionObject($field);
$propertiesToCheck = ['name', 'statePath'];

foreach ($propertiesToCheck as $propertyName) {
Expand All @@ -109,6 +124,13 @@ private function fieldContainsConfigKey($field, string $configKey): bool
return false;
}

public function getFieldType(): ?string
{
// This method should be overridden by specific field classes
// to return their field type
return null;
}

public static function getDefaultConfig(): array
{
return [
Expand All @@ -119,6 +141,12 @@ public static function getDefaultConfig(): array
'hint' => null,
'hintColor' => null,
'hintIcon' => null,
'conditionalField' => null,
'conditionalOperator' => null,
'conditionalValue' => null,
'conditionalAction' => null,
'validationRules' => [],
'visibilityRules' => [],
'defaultValue' => null,
];
}
Expand All @@ -131,25 +159,38 @@ public static function applyDefaultSettings($input, ?Field $field = null)
->hidden($field->config['hidden'] ?? self::getDefaultConfig()['hidden'])
->helperText($field->config['helperText'] ?? self::getDefaultConfig()['helperText'])
->hint($field->config['hint'] ?? self::getDefaultConfig()['hint'])
->hintIcon($field->config['hintIcon'] ?? self::getDefaultConfig()['hintIcon']);
->hintIcon($field->config['hintIcon'] ?? self::getDefaultConfig()['hintIcon'])
->live();

if (isset($field->config['hintColor']) && $field->config['hintColor']) {
$input->hintColor(Color::generateV3Palette($field->config['hintColor']));
}

$input = ConditionalLogicApplier::applyConditionalLogic($input, $field);
$input = ConditionalLogicApplier::applyConditionalValidation($input, $field);
$input = VisibilityLogicApplier::applyVisibilityLogic($input, $field);

$input = self::applyAdditionalValidation($input, $field);

if (isset($field->config['defaultValue'])) {
$input->default($field->config['defaultValue']);
}

return $input;
}

protected static function ensureArray($value, string $delimiter = ','): array
protected static function applyAdditionalValidation($input, ?Field $field = null): mixed
{
if (is_array($value)) {
return $value;
if (! $field || empty($field->config['validationRules'])) {
return $input;
}

return ! empty($value) ? explode($delimiter, $value) : [];
$rules = $field->config['validationRules'];

foreach ($rules as $rule) {
$input = ValidationRuleApplier::applyValidationRule($input, $rule, $field);
}

return $input;
}
}
10 changes: 10 additions & 0 deletions src/Fields/Checkbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

class Checkbox extends Base implements FieldContract
{
public function getFieldType(): ?string
{
return 'checkbox';
}

public static function getDefaultConfig(): array
{
return [
Expand Down Expand Up @@ -68,6 +73,11 @@ public function getForm(): array
->inline(false),
]),
]),
Tab::make('Rules')
->label(__('Rules'))
->schema([
...parent::getRulesForm(),
]),
])->columnSpanFull(),
];
}
Expand Down
10 changes: 10 additions & 0 deletions src/Fields/CheckboxList.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class CheckboxList extends Base implements FieldContract
{
use HasOptions;

public function getFieldType(): ?string
{
return 'checkbox-list';
}

public static function getDefaultConfig(): array
{
return [
Expand Down Expand Up @@ -112,6 +117,11 @@ public function getForm(): array
->visible(fn (Get $get): bool => $get('config.searchable')),
]),
]),
Tab::make('Rules')
->label(__('Rules'))
->schema([
...parent::getRulesForm(),
]),
])->columnSpanFull(),
];
}
Expand Down
10 changes: 10 additions & 0 deletions src/Fields/Color.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
*/
class Color extends Base implements FieldContract
{
public function getFieldType(): ?string
{
return 'color';
}

public static function getDefaultConfig(): array
{
return [
Expand Down Expand Up @@ -64,6 +69,11 @@ public function getForm(): array
->placeholder(__('Enter a regex pattern')),
]),
]),
Tab::make('Rules')
->label(__('Rules'))
->schema([
...parent::getRulesForm(),
]),
])->columnSpanFull(),
];
}
Expand Down
10 changes: 10 additions & 0 deletions src/Fields/DateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ class DateTime extends Base implements FieldContract
{
use HasAffixes;

public function getFieldType(): ?string
{
return 'date-time';
}

public static function getDefaultConfig(): array
{
return [
Expand Down Expand Up @@ -123,6 +128,11 @@ public function getForm(): array
]),
self::affixFormFields(),
]),
Tab::make('Rules')
->label(__('Rules'))
->schema([
...parent::getRulesForm(),
]),
])->columnSpanFull(),
];
}
Expand Down
54 changes: 54 additions & 0 deletions src/Fields/FormSchemas/BasicSettingsSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace Backstage\Fields\Fields\FormSchemas;

use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Utilities\Get;

class BasicSettingsSchema
{
public static function make(): array
{
return [
Grid::make(3)
->schema([
Toggle::make('config.required')
->label(__('Required'))
->inline(false),
Toggle::make('config.disabled')
->label(__('Disabled'))
->inline(false),
Toggle::make('config.hidden')
->label(__('Hidden'))
->inline(false),
]),
Grid::make(2)
->schema([
TextInput::make('config.helperText')
->live(onBlur: true)
->label(__('Helper text')),
TextInput::make('config.hint')
->live(onBlur: true)
->label(__('Hint')),
ColorPicker::make('config.hintColor')
->label(__('Hint color'))
->visible(function (Get $get): bool {
$hint = $get('config.hint');

return ! empty(trim($hint));
}),
TextInput::make('config.hintIcon')
->label(__('Hint icon'))
->placeholder('heroicon-m-')
->visible(function (Get $get): bool {
$hint = $get('config.hint');

return ! empty(trim($hint));
}),
]),
];
}
}
Loading
Loading