Trongate Way Docs

The Manage Page

The manage page is the centerpiece of any Trongate admin CRUD module. It displays a paginated table of records, provides search and per-page controls, and gives quick access to view, edit, and delete actions for each record.

Page Layout

Here is the high-level layout of the manage page. The page is rendered inside the admin template, which provides the header, sidebar, and footer. What we build is the content area:

admin template header + sidebar
h1 → Manage Countries
flashdata message (appears only after create/update/delete)
[ + Create New Country Record ]
[ Search ]
pagination bar ← 1 2 3 … 5 →   Showing 1 to 20 of 23
Country Title Country Code Actions
Australia AU [👁] [✏] [🗑]
Belgium BE [👁] [✏] [🗑]
Canada CA [👁] [✏] [🗑]
pagination bar   Records Per Page: [20 ▼]
admin template footer

Two important differences from simpler CRUD examples: the table shows both the country title and the country code as visible columns, and the country title links open an MX record preview modal rather than navigating away from the list.

Controller Method

The manage() method in Countries.php gathers all the data needed by the view:

PHP
/**
 * Display paginated list of countries.
 *
 * Shows records in a table with pagination controls. Includes
 * dropdown for selecting number of records per page.
 *
 * @return void
 */
public function manage(): void {
    $this->trongate_security->make_sure_allowed();

    $search_query = $_GET['search_query'] ?? '';
    $search_column = $_GET['search_column'] ?? '';

    // Validate column against known searchable columns.
    $allowed_columns = $this->model->get_searchable_columns();
    if ($search_column !== '' && !in_array($search_column, $allowed_columns, true)) {
        $search_column = '';
    }

    $search_active = ($search_query !== '');

    if ($search_active) {
        $total_rows = $this->model->count_search_results($search_query, $search_column);
    } else {
        $total_rows = $this->model->count_all();
    }
    
    $limit = $this->get_limit();
    $offset = $this->get_offset();

    if ($search_active) {
        $rows = $this->model->search_records($search_query, $search_column, $limit, $offset);
    } else {
        $rows = $this->model->fetch_records($limit, $offset);
    }
    $rows = $this->model->prepare_records_for_display($rows);

    $data = [
        'rows' => $rows,
        'pagination_data' => $this->get_pagination_data($total_rows, $limit, $search_query, $search_column),
        'view_module' => 'countries',
        'view_file' => 'manage',
        'per_page_options' => $this->per_page_options,
        'selected_per_page' => $this->get_selected_per_page()
    ];

    $data['search_query'] = $search_query;
    $data['search_column'] = $search_column;
    $data['search_active'] = $search_active;
    $this->templates->admin($data);
}

Let us examine each piece.

Authentication

The $this->trongate_security->make_sure_allowed() call ensures only logged-in administrators can access this page. This is the standard v2 approach - it delegates to the trongate_administrators module which checks the user's token level. In dev mode, it automatically creates a temporary admin session so you don't need to log in repeatedly during development.

Search Parameters

The search query and column are read from the URL's query string (?search_query=...&search_column=...). Before using them, the controller validates the column name against the model's get_searchable_columns() whitelist - this prevents SQL injection through the column parameter.

When a search is active, the controller uses count_search_results() and search_records() instead of count_all() and fetch_records(). The search query string is also passed to the pagination helper so that pagination links preserve the search context.

Pagination Helpers

The get_limit() helper reads the user's preferred page size from the session (defaulting to 20). The get_offset() helper calculates how many records to skip based on the current page number, which is extracted from URL segment 3 via segment(3, 'int').

Unlike older Trongate patterns, these helpers are declared as private methods. This means they cannot be invoked via URL, so there is no need for block_url().

Pagination Data

The pagination data array tells the pagination module how to render its controls:

PHP
/**
 * Generate pagination configuration data.
 *
 * @param int    $total_rows    Total number of records
 * @param int    $limit         Number of records per page
 * @param string $search_query  The search query string
 * @param string $search_column The column being searched
 * @return array Pagination configuration for template
 */
private function get_pagination_data(int $total_rows, int $limit, string $search_query = '', string $search_column = ''): array {
    $pagination_query = '';
    if ($search_query !== '' && $search_column !== '') {
        $pagination_query = 'search_query=' . urlencode($search_query) . '&search_column=' . urlencode($search_column);
    }

    return [
        'total_rows' => $total_rows,
        'limit' => $limit,
        'pagination_root' => 'countries/manage',
        'pagination_query' => $pagination_query,
        'record_name_plural' => 'countries',
        'include_showing_statement' => true
    ];
}

The pagination module is invoked in the view with Modules::run('pagination/display', $pagination_data), which renders page numbers and a "Showing X to Y of Z" statement.

The Manage View

The view file manage.php renders the full content area. Here is the complete file:

View File
<h1>Manage Countries</h1>
<?= flashdata() ?>
<?php if (!empty($search_query)): ?>
    <p>Showing results for <strong><?= out($search_query) ?></strong></p>
<?php endif; ?>
<?php
echo '<p class="flex-row justify-between">';
echo anchor('countries/create', 'Create New Country Record', ['class' => 'button alt']);
if ((count($rows) > 9) || (!empty($search_query))) {
    $btn_attr = [
        'class' => 'alt',
        'mx-get' => 'countries/search_modal',
        'mx-build-modal' => json_encode([
            'id' => 'search-modal',
            'modalHeading' => 'Search Countries',
            'modalFooter' => '<button class="alt" onclick="closeModal()">Cancel</button><button form="search-form">Search</button>'
        ])
    ];
    echo form_button('search_btn', 'Search <i class="tg tg-search"></i>', $btn_attr);
}
echo '</p>';
if (empty($rows)) {
    echo '<p>There are currently no records to display.</p>';
    return;
}
echo Modules::run('pagination/display', $pagination_data);
?>

<div class="table-container">
    <table class="records-table">
        <thead>
            <tr>
                <th colspan="3">
                    <div>
                        <div> </div>
                        <div>Records Per Page: <?php
                        $dropdown_attr['onchange'] = 'setPerPage()';
                        echo form_dropdown('per_page', $per_page_options, $selected_per_page, $dropdown_attr); 
                        ?></div>
                    </div>                    
                </th>
            </tr>
            <tr>
                <th class="text-left">Country Title</th>
            <th class="text-left">Country Code</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach($rows as $row): 
                $link_attr = [
                    'mx-get' => 'countries/show/'.$row->id,
                    'mx-select' => '.detail-grid',
                    'mx-build-modal' => json_encode([
                        'id' => 'record-preview-modal',
                        'width' => '640px',
                        'modalHeading' => 'Record Preview',
                        'modalFooter' => '<a href="countries/show/'.$row->id.'" class="button alt mt-0 xs">View Details</a>'
                    ])
                ];
                ?>
                <tr>
                <td><?= anchor('#', out($row->country_title), $link_attr) ?></td>
                <td><?= out($row->country_code) ?></td>                    <td>
                        <div class="actions">
                            <a href="countries/show/<?= $row->id ?>" class="button alt button-round"><i class="tg tg-eye"></i></a>
                            <a href="countries/create/<?= $row->id ?>" class="button alt button-round"><i class="tg tg-pencil"></i></a>
                            <a href="countries/delete_conf/<?= $row->id ?>" class="button alt button-round"><i class="tg tg-trash"></i></a>
                        </div>
                    </td>
                </tr>
            <?php endforeach; ?>
        </tbody>
    </table>
</div>

<?php 
if(count($rows)>9) {
    unset($pagination_data['include_showing_statement']);
    echo Modules::run('pagination/display', $pagination_data);
}
?>

<script>
function setPerPage() {
    const selectedIndex = document.querySelector('select[name="per_page"]').value;
    window.location.href = '<?= BASE_URL ?>countries/set_per_page/' + selectedIndex;
}
</script>

Note that this is a Trongate view partial - it does not include <!DOCTYPE html>, <html>, <head>, or <body> tags because those are provided by the admin template. The view only supplies the content that appears within the content area of the layout diagram above.

Each row in the table has three action buttons: View (eye icon), Edit (pencil icon), and Delete (trash icon). The country title itself doubles as a link that opens a quick-preview modal via Trongate MX.

Per-Page Selector

A dropdown in the table header lets the user choose between 10, 20, 50, or 100 records per page. When changed, the setPerPage() JavaScript function redirects to /countries/set_per_page/{index}, which stores the preference in the session:

PHP
/**
 * Set number of records per page for pagination.
 *
 * Stores user preference in session for consistent pagination across requests.
 *
 * @return void
 */
public function set_per_page(): void {
    $this->trongate_security->make_sure_allowed();

    $selected_index = segment(3, 'int');

    if (!isset($this->per_page_options[$selected_index])) {
        $selected_index = 1;
    }

    $_SESSION['selected_per_page'] = $selected_index;
    redirect('countries/manage');
}

Search Modal

The Search button opens an MX-powered modal where the user enters a query and selects which column to search:

View File
<?php
/**
 * Search modal form for filtering countries.
 *
 * Loaded via MX into a modal when the Search button is clicked.
 */
$searchable_columns = [
    'country_title' => 'Country Title',
    'country_code' => 'Country Code',
];

$form_attr = ['id' => 'search-form'];
echo form_open('countries/submit_search', $form_attr);
?>
    <div class="form-group">
        <?= form_label('Search Query') ?>
        <?= form_input('search_query', '', ['placeholder' => 'Enter search term...', 'autocomplete' => 'off']) ?>
    </div>
    <div class="form-group">
        <?= form_label('Search In') ?>
        <?= form_dropdown('search_column', $searchable_columns) ?>
    </div>
<?= form_close() ?>

When the user submits the search form, submit_search() validates the input and redirects back to /countries/manage with the search parameters in the query string. The manage method reads these parameters and passes them to the view, which shows only matching records.

We're continually improving the Trongate documentation. If anything is incorrect, unclear, incomplete, or could be better, we'd genuinely appreciate your input.

Share your thoughts in the Documentation Feedback.

Leave Feedback About This Page