Skip to content

Custom Layouts

By default, the detail form renders all properties in a single column, top to bottom. Override getView() to create custom layouts with columns, cards, tabs, and panels.

Basic Layout

The getView() method returns a layout built with $this->fromLayout():

use System\View\Elements;
use System\View\ViewNode;

public function getView(?string $context = null): ?ViewNode
{
    return $this->fromLayout([
        'title',           // property name as string
        'description',
        'due_date',
    ]);
}

Returning null uses the default layout (all fields, one column).

Rows — Side by Side Fields

Place properties next to each other in a row using an array:

1
2
3
4
5
return $this->fromLayout([
    'title',
    ['start_date', 'end_date'],         // these two sit side by side
    'description',
]);

Columns — Proportional Widths

Use Elements::columns() with Elements::col() for proportional column layouts (12-column grid):

return $this->fromLayout([
    Elements::columns()->add(
        Elements::col(8)->add(           // 8/12 width (left)
            'title',
            'reference',
            'value'
        ),
        Elements::col(4)->add(           // 4/12 width (right)
            'type',
            'party',
            'period'
        )
    ),
    ['start_date', 'end_date'],          // full-width row below
]);

Cards — Visual Grouping

Wrap fields in a card for visual separation:

return $this->fromLayout([
    Elements::columns()->add(
        Elements::col(8)->add(
            Elements::card()->add(       // card with border
                'title',
                'reference',
                'value'
            )
        ),
        Elements::col(4)->add(
            Elements::card()->add(
                'type',
                'party'
            )
        )
    ),
]);

Tabs — Organized Sections

Use tabs to organize many fields into logical groups:

return $this->fromLayout([
    'title',
    Elements::tabs('main')
        ->add('Details',                  // tab label
            Elements::row('type', 'party', 'reference'),
            'value'
        )
        ->add('Dates',
            Elements::row('start_date', 'end_date'),
            'period'
        )
        ->add('Documents',
            'document',
            'amendments'
        ),
]);

Panels — Collapsible Sections

1
2
3
4
5
6
return $this->fromLayout([
    'subject',
    'content',
    Elements::panel('Internal Notes', key: 'notes')
        ->add('note', 'todo'),
]);

HasMany in Layouts

Reference hasMany properties by name. They render as inline lists:

1
2
3
4
5
6
return $this->fromLayout([
    'title',
    'content',
    ['reminders', 'feedbacks'],          // two hasMany lists side by side
    'attachments',
]);

Complete Example

Here's a real-world layout combining all elements:

public function getView(?string $context = null): ?ViewNode
{
    return $this->fromLayout([
        // Two-column header
        Elements::columns()->add(
            Elements::col(8)->add(
                Elements::card()->add(
                    'title',
                    'reference',
                    'value'
                )
            ),
            Elements::col(4)->add(
                Elements::card()->add(
                    'type',
                    'party',
                    'period'
                )
            )
        ),

        // Date row
        Elements::columns()->add(
            Elements::col(8)->add('start_date', 'end_date'),
        ),

        // Child records
        ['reminders', 'amendments'],

        // Bottom section
        'document',
        'todo',
    ]);
}

Tip

If getView() returns null, the framework renders all properties in declaration order. You only need a custom layout when the default order or single-column presentation isn't ideal.