Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e2b7a5e
schemas (wip)
Baspa Sep 10, 2025
94e4faa
add selecttree
Baspa Sep 10, 2025
a65a64f
add grouping
Baspa Sep 10, 2025
e6b7459
Fix styling
Baspa Sep 10, 2025
fb0f224
add fieldset schema
Baspa Sep 10, 2025
51dcc2c
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Sep 10, 2025
359e53f
Fix styling
Baspa Sep 10, 2025
2ebe975
use get key name
Baspa Sep 10, 2025
5af1ebf
new trait to render schemas with fields
Baspa Sep 10, 2025
a1041c0
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Sep 10, 2025
0b0bc41
Fix styling
Baspa Sep 10, 2025
66257c7
feat: table columns in repeater
Baspa Sep 11, 2025
852de44
wip (needs to be tested thoroughly, also in Backstage)
Baspa Sep 18, 2025
da63411
test
Baspa Sep 18, 2025
bcd799f
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Sep 18, 2025
91720fb
Merge branch 'main' of github.com:backstagephp/fields into feat/form-…
Baspa Sep 26, 2025
d130fdb
Fix styling
Baspa Sep 26, 2025
6ee4055
wip?
Baspa Oct 1, 2025
d5f6804
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Oct 10, 2025
1365c2c
Merge branch 'main' into feat/form-layout-components
Baspa Oct 10, 2025
ab1030f
Fix styling
Baspa Oct 10, 2025
c61d623
fix: phpstan issue
Baspa Oct 10, 2025
037c057
fix: tests
Baspa Oct 10, 2025
ba61ff4
wip
Baspa Oct 10, 2025
a37e651
Merge branch 'main' into feat/form-layout-components
Baspa Dec 11, 2025
5c3fdae
Update Repeater to exclusively use tableMode
Baspa Dec 11, 2025
8c82d52
repeater improvements
Baspa Dec 11, 2025
b75a1bf
styles: fix styling issues
Baspa Dec 11, 2025
e02ce68
fix phpstan issues
Baspa Dec 11, 2025
ba26403
Merge branch 'feat/form-layout-components' of github.com:backstagephp…
Baspa Dec 11, 2025
ec0cad2
styles: fix styling issues
Baspa Dec 11, 2025
4587a32
remove logs
Baspa Dec 11, 2025
7862ad2
fix: setting parent schemas
Baspa Dec 11, 2025
8f64f1a
feat: copy fields
Baspa Dec 11, 2025
cfce077
styles: fix styling issues
Baspa Dec 11, 2025
ab726f1
dont use merge to prevent changing keys
Baspa Dec 11, 2025
cab0a92
feat: dynamic mode (wip)
Baspa Dec 11, 2025
65a3eda
Merge branch 'feat/defined-values-based-on-dependant-fields' of githu…
Baspa Dec 11, 2025
c657399
styles: fix styling issues
Baspa Dec 11, 2025
f7e20da
fix: wrong type hint
Baspa Dec 11, 2025
f053b09
Merge branch 'feat/defined-values-based-on-dependant-fields' of githu…
Baspa Dec 11, 2025
a5d64e7
feat: build schema tree
Baspa Dec 11, 2025
2809a64
replicate fields
Baspa Dec 11, 2025
2db69c0
styles: fix styling issues
Baspa Dec 11, 2025
ac004f3
feat: advanced calculations and repeater default values
Baspa Dec 11, 2025
8576a20
Merge branch 'feat/defined-values-based-on-dependant-fields' of githu…
Baspa Dec 11, 2025
b60ae12
styles: fix styling issues
Baspa Dec 11, 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
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,48 @@ Combine multiple conditions with logical operators:

The visibility system works seamlessly with validation rules to create intelligent, user-friendly forms that adapt to your data and user interactions.

#### Dynamic Values (Text Fields)

Text fields support dynamic value calculation, allowing them to automatically populate based on other fields in the form.

##### Relation Prefill Mode

Automatically prefill a text field based on a selection in another field (typically a Select field).

- **Source Field**: The field to watch (e.g., a `building` select field).
- **Relation Column**: The column from the related model to fetch (e.g., `city`).

*Example*: When a user selects a "Building", the "City" text field automatically updates to show the city associated with that building record.

##### Calculation Mode

Calculate the field's value using a mathematical formula based on other fields.

- **Formula**: Enter a formula using field values.
- **Syntax**: Use field IDs or keys in curly braces, e.g., `{price} * {quantity}`.
- **Supported Operations**: Standard math operators (`+`, `-`, `*`, `/`, `(`, `)`).

*Example*: A "Total" field can automatically calculate `{price} * {tax_rate}`.

##### Advanced Use Cases

**Using Slugs**: You can use the field slug instead of the ULID for better readability: `"{price} * {quantity}"`.

**Conditional Logic**: The formula engine supports conditional operations similar to Excel or PHP ternary operators. This is useful for Repeater rows where you want calculations to apply only when specific criteria are met.

*Syntax*: `condition ? value_if_true : value_if_false`

*Example*:
```
"{type}" == "Premium" ? {price} * 1.2 : {price}
```

*Nested Example* (Excel-like IF/ELSE):
```
"{status}" == "Paid" ? 0 : ("{status}" == "Pending" ? {total} : null)
```
**Note**: Returning `null` effectively skips the calculation, allowing the field to be manually edited or remain empty.

### 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 Down Expand Up @@ -492,6 +534,20 @@ The package includes a powerful Rich Editor with custom plugins:

- **[Visibility Rules](docs/visibility-rules.md)** - Comprehensive guide to controlling field visibility based on conditions and record properties


## Fields

### Repeater

### Repeater

The Repeater field includes a robust implementation to handle default value hydration. It uses a custom `NormalizedRepeater` component (extending `Filament\Forms\Components\Repeater`) to ensure that data hydration from JSON strings (common in database storage) is correctly decoded and normalized into a UUID-keyed array before the component renders.

This prevents common "string given" errors in loops and ensures that fields are correctly populated even when the initial state is a flat list or single object.

- **Default Values**: Can be entered as JSON in the field configuration.
- **JSON Editor**: Use the JSON editor to enter default values.

## Testing

```bash
Expand Down
24 changes: 24 additions & 0 deletions database/migrations/add_schema_id_to_fields_table.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::table('fields', function (Blueprint $table) {
$table->ulid('schema_id')->nullable()->after('group');
$table->foreign('schema_id')->references('ulid')->on('schemas')->onDelete('set null');
});
}

public function down()
{
Schema::table('fields', function (Blueprint $table) {
$table->dropForeign(['schema_id']);
$table->dropColumn('schema_id');
});
}
};
35 changes: 35 additions & 0 deletions database/migrations/create_schemas_table.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create('schemas', function (Blueprint $table) {
$table->ulid('ulid')->primary();
$table->string('name');
$table->string('slug');
$table->string('field_type');
$table->json('config')->nullable();
$table->integer('position')->default(0);
$table->string('model_type');
$table->string('model_key');
$table->ulid('parent_ulid')->nullable();
$table->timestamps();

$table->index(['model_type', 'model_key']);
$table->index(['model_type', 'model_key', 'position']);
$table->index(['parent_ulid']);

$table->unique(['model_type', 'model_key', 'slug']);
});
}

public function down(): void
{
Schema::dropIfExists('schemas');
}
};
50 changes: 50 additions & 0 deletions src/Components/NormalizedRepeater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Backstage\Fields\Components;

use Filament\Forms\Components\Repeater;

class NormalizedRepeater extends Repeater
{
public function getRawState(): mixed
{
$state = parent::getRawState();

if (is_string($state)) {
$state = json_decode($state, true);
}

if (is_array($state)) {
// Check if wrapping is needed
$hasNonArrayItems = false;
foreach ($state as $item) {
if (! is_array($item)) {
$hasNonArrayItems = true;

break;
}
}

if ($hasNonArrayItems) {
$state = [$state];
}

// If it's a numeric list (0, 1, 2...), generate UUID keys
// Filament requires UUID keys for Repeater items to bind correctly
if (array_is_list($state)) {
$keyedState = [];
foreach ($state as $item) {
$keyedState[\Illuminate\Support\Str::uuid()->toString()] = $item;
}
$state = $keyedState;

// Persist the normalized state so keys don't rotate on every call
$this->rawState($state);
}

return $state;
}

return [];
}
}
Loading