Forms
Recipes for building module admin forms and data tables.
This chapter collects task-oriented recipes for building the admin-cabinet UI of a MikoPBX module: settings forms that read and write a model, server-paginated data grids backed by a REST API v3 endpoint, and surgical changes to forms that already ship with the core.
Every recipe is anchored to a real, working example module — Extensions/EXAMPLES/WebInterface/ModuleExampleForm/ — and uses primary-source APIs from the core BaseForm class. Throughout the cookbook we thread a single fictional module, ModuleBlackList (config class BlackListConf, main class BlackListMain, model BlackListNumbers, table m_BlackListNumbers), so the recipes read as one continuous story.
What a module form is made of
A module admin page is wired together from five collaborating pieces. The example module ships one of each, so use it as your skeleton:
Phalcon form class
Declares the fields and their types
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Forms/ModuleExampleFormForm.php
Controller
Loads the model, builds dropdown options, renders the form, handles save/delete
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Controllers/ModuleExampleFormController.php
Volt view
HTML template that prints the form
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/App/Views/ModuleExampleForm/
JavaScript module
Client-side validation and AJAX submit/grid logic
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/public/assets/js/src/module-example-form-modify.js
Model
The table the form persists to
Extensions/EXAMPLES/WebInterface/ModuleExampleForm/Models/ModuleExampleForm.php
The form class is the heart of the page. It extends MikoPBX\AdminCabinet\Forms\BaseForm, which adds MikoPBX-specific element helpers on top of the standard Phalcon\Forms\Form:
namespace Modules\ModuleExampleForm\App\Forms;
use MikoPBX\AdminCabinet\Forms\BaseForm;
use Phalcon\Forms\Element\Hidden;
use Phalcon\Forms\Element\Numeric;
use Phalcon\Forms\Element\Text;
class ModuleExampleFormForm extends BaseForm
{
public function initialize($entity = null, $options = null): void
{
parent::initialize($entity, $options);
// Hidden primary key — always present, never shown to the user
$this->add(new Hidden('id', ['value' => $entity->id]));
// Plain Phalcon elements work directly via add()
$this->add(new Text('text_field'));
$this->add(new Numeric('integer_field', ['maxlength' => 4, 'defaultValue' => 3]));
// BaseForm helpers add MikoPBX-styled widgets
$this->addTextArea('text_area_field', $entity->text_area_field ?? '', 90);
$this->addCheckBox('checkbox_field', intval($entity->checkbox_field) === 1);
$this->addSemanticUIDropdown(
'select_field',
$options['priorities'] ?? [],
$entity->select_field ?? 'medium'
);
}
}The three recipes
1. Create a module settings form
Build a single-record settings page (one row in the module table) from scratch: declare the form class, load the model in the controller, render it in a Volt view, and submit it over AJAX with client-side validation. This is the base + ui recipe combination described in the core skill's reference/recipes.md.
➡️ Create a module settings form
2. Create a server-paginated datatable
When a module manages many rows (blacklisted numbers, log entries, queued tasks) you render a Fomantic/DataTables grid whose pages are fetched from the server instead of loading every row into the browser. The grid calls a REST API v3 getList endpoint that returns a PBXApiResult. The list action in Extensions/EXAMPLES/REST-API/ModuleExampleRestAPIv3/Lib/RestAPI/Tasks/Actions/GetListAction.php is the canonical shape to copy:
➡️ Create a server-paginated datatable
3. Add a field to an existing form
Sometimes you do not own the form — you want to inject one extra field into a core page (for example, add a setting to the SIP extension form) without patching core files. MikoPBX exposes a WebUI hook, WebUIConfigInterface::ON_BEFORE_FORM_INITIALIZE, which BaseForm::initialize() dispatches to every enabled module via PBXConfModulesProvider::hookModulesMethod(). Your module implements the hook, receives the live form plus its entity, and calls $form->add(...).
➡️ Add a field to an existing form
Compiling the JavaScript
The browser-side code lives as ES6+ source under public/assets/js/src/ (for the example: module-example-form-modify.js and module-example-form-index.js). MikoPBX serves the transpiled copy from js/cache/<moduleUniqueID>/..., which the controller registers on the footer asset collection:
js/cache/<moduleUniqueID> is not a directory the build writes into — it is a symlink the installer creates pointing at your module's public/assets/js (PbxExtensionUtils::createAssetsSymlinks(), Core/src/Modules/PbxExtensionUtils.php:105-114; the same is done for css and img). So the URL js/cache/ModuleBlackList/module-black-list-modify.js resolves to <moduleDir>/public/assets/js/module-black-list-modify.js.
That means Babel's output directory is public/assets/js — the parent of src/, not a cache subfolder:
Editing the src/ file alone changes nothing in the running cabinet — the page loads the compiled file from js/cache/. Re-run the Babel step (or your build task) after every JavaScript edit, and clear the browser cache if the new code does not appear.
Where to go next
Start from an empty interface scaffold: Module interface (empty).
Then follow the recipes above in order — most modules implement recipe 1, add recipe 2 when they manage a list, and reach for recipe 3 only to extend a core page.
Last updated
Was this helpful?