Description

A Tabs Page is a controller that assembles Components into a Bootstrap tab interface. Each tab has an id, a visible label, and its own grid of components. Everything else — authorization, CSRF, GET/POST dispatch, <head> / <foot> collection — works identically to a Grid Page.

Create a Tabs Page by extending BaseTabsComponent and declaring a $tabs array.


Minimal example

use Fabiom\UDDemo\Components\BaseTabsComponent;

class ArticlesPage extends BaseTabsComponent {

    const CONTROLLER_NAME = 'articles-page';

    protected array $tabs = [
        [
            'id'     => 'tab-list',
            'label'  => 'All Articles',
            'panels' => [
                ['cssclass' => 'col-12', 'component' => ArticlesList::class],
            ],
        ],
        [
            'id'     => 'tab-new',
            'label'  => 'New Article',
            'panels' => [
                ['cssclass' => 'col-md-8 offset-md-2', 'component' => ArticleNew::class],
            ],
        ],
    ];

}

The CONTROLLER_NAME constant registers the page with the router. A request to /articles-page.html renders the tab interface, with the first tab active by default.


The $tabs array

Each entry in $tabs is a tab descriptor with three required keys.

Key Type Description
id string Unique HTML id for the tab panel. Must be valid as an HTML id attribute.
label string Text shown on the tab button.
panels array A list of layout nodes, identical to the $panels array of a Grid Page.

Panel nodes inside a tab

The panels array inside each tab supports the same three node types as a Grid Page.

Component node

['cssclass' => 'col-12', 'component' => ArticlesList::class]

Container node

['cssclass' => 'row', 'panels' => [
    ['cssclass' => 'col-md-6', 'component' => ArticlesList::class],
    ['cssclass' => 'col-md-6', 'component' => ArticleSummary::class],
]]

Nested tabs node

Tabs can be nested — a tab panel can itself contain a tabs node.

['cssclass' => 'col-12', 'tabs' => [
    ['id' => 'tab-published', 'label' => 'Published', 'panels' => [
        ['cssclass' => 'col-12', 'component' => PublishedArticlesList::class],
    ]],
    ['id' => 'tab-drafts', 'label' => 'Drafts', 'panels' => [
        ['cssclass' => 'col-12', 'component' => DraftArticlesList::class],
    ]],
]]

POST dispatch

POST handling works the same as in a Grid Page. Each component form must include a hidden _component field so the page can route the submission to the correct component:

<form method="post">
    <input type="hidden" name="_component" value="<?= self::class ?>">
    <!-- form fields -->
    <button type="submit" class="btn btn-primary">Save</button>
</form>

After a successful POST the page redirects back to the referring page by default. Override onPostSuccess() to change this:

protected function onPostSuccess(): void {
    $this->redirectToPage(url_for('articles-page'));
}

Authorization

Override check_authorization_get_request() and check_authorization_post_request() to guard the whole page.

protected function check_authorization_get_request(): bool {
    return isset($_SESSION['group']) && in_array($_SESSION['group'], ['editor', 'admin']);
}

protected function check_authorization_post_request(): bool {
    return isset($_SESSION['group']) && in_array($_SESSION['group'], ['editor', 'admin']);
}

Individual components inside any tab can still declare their own check_authorization_resource_request() to hide themselves for certain users.


Complete example

use Fabiom\UDDemo\Components\BaseTabsComponent;
use Fabiom\UDDemo\Chapters\Articles\Components\ArticlesList;
use Fabiom\UDDemo\Chapters\Articles\Components\ArticleNew;
use Fabiom\UDDemo\Chapters\Articles\Components\ArticleSearch;
use Fabiom\UDDemo\Chapters\Articles\Components\ArticleStats;

class ArticlesPage extends BaseTabsComponent {

    const CONTROLLER_NAME = 'articles-page';

    protected array $tabs = [
        [
            'id'     => 'tab-list',
            'label'  => 'Articles',
            'panels' => [
                ['cssclass' => 'col-12 mb-3', 'component' => ArticleSearch::class],
                ['cssclass' => 'col-12',       'component' => ArticlesList::class],
            ],
        ],
        [
            'id'     => 'tab-new',
            'label'  => 'New Article',
            'panels' => [
                ['cssclass' => 'row', 'panels' => [
                    ['cssclass' => 'col-md-8 offset-md-2', 'component' => ArticleNew::class],
                ]],
            ],
        ],
        [
            'id'     => 'tab-stats',
            'label'  => 'Statistics',
            'panels' => [
                ['cssclass' => 'col-12', 'component' => ArticleStats::class],
            ],
        ],
    ];

    protected function check_authorization_get_request(): bool {
        return isset($_SESSION['group']);
    }

    protected function check_authorization_post_request(): bool {
        return isset($_SESSION['group']);
    }

}

This page renders three tabs. The first tab shows a search bar above the articles list. The second tab shows a centred new-article form. The third tab shows statistics. POST submissions from ArticleNew are routed automatically via the _component field.