Milk Admin

Adding Pre-loaded Select with Relationships

Revision: 2025/10/28

This guide shows you how to add a select field that loads all data immediately (no autocomplete fetch), perfect for small datasets like categories, departments, or roles.

What You'll Learn:
  • How to add a relationship field with pre-loaded options
  • How to create a second model and link it to the first
  • How to display related data in tables using dot notation
  • How to create multiple select fields for many-to-many relationships
⚠️ When to use pre-loaded vs autocomplete:
  • Pre-loaded (this guide): Small datasets (up to ~100 items) - categories, departments, roles, statuses
  • Autocomplete fetch: Large datasets (hundreds/thousands) - users, products, customers. See Autocomplete Search Guide
💡 Prerequisites: This guide assumes you already have a working module. If not, see:

Example: Employees with Categories

We'll create a Categories model and link it to Employees. This pattern works for:

  • Employees → Categories (departments, roles)
  • Products → Categories
  • Posts → Categories/Tags
  • Tasks → Priorities/Statuses

Step 1: Create the Second Model (Categories)

First, create a simple model for the data you want to select from (e.g., categories).

File: CategoryModel.php

<?php
namespace Modules\Employees;
use App\Abstracts\AbstractModel;

class CategoryModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule->table('#__employee_categories')
            ->id()
            ->string('name', 100)->required()->label('Category Name')
            ->text('description')->nullable();
    }

    /**
     * Get list of categories for select options
     * Returns array: [id => name]
     */
    public function getList()
    {
        $list = $this->query()->order('name')->getResults();
        $array_return = [];
        foreach ($list as $value) {
            $array_return[$value->id] = $value->name;
        }
        return $array_return;
    }
}
✅ Key Points:
  • The getList() method is essential - it returns [id => name] for the select
  • Keep it in the same namespace as the main model (e.g., Modules\Employees)
  • Use ->order('name') to sort alphabetically

Step 2: Register the Model in the Module

Before creating the database table, you need to register the CategoryModel in your module's configuration.

File: EmployeesModule.php

<?php
namespace Modules\Employees;
use App\Abstracts\AbstractModule;

class EmployeesModule extends AbstractModule
{
    protected function configure($rule): void
    {
        $rule->page('employees')
            ->title('Employees')
            ->menu('Employees', '', 'bi bi-people-fill', 20)

            // ========================================
            // ADD THIS LINE TO REGISTER SECONDARY MODELS
            // ========================================
            ->addModels(['category' => CategoryModel::class])

            ->access('public')
            ->version(20251028);
    }
}
✅ Important: The ->addModels() method tells the system to manage these secondary models during installation and updates.

Step 3: Add Relationship Field to Main Model

Now add the category_id field to your main model (e.g., EmployeesModel).

Example: EmployeesModel.php

protected function configure($rule): void
{
    $rule->table('#__employees')
        ->id()
        ->string('name', 100)->required()
        ->string('surname', 100)->required()

        // ========================================
        // ADD THIS CATEGORY FIELD
        // ========================================
        ->int('category_id')
            ->belongsTo('category', CategoryModel::class, 'id')
            ->formType('milkSelect')
            ->options((new CategoryModel())->getList())
            ->label('Category')
            ->nullable()

        ->timestamp('created_at')->hideFromEdit()->saveValue(time());
}

Understanding Each Method

Method Purpose
->int('category_id') Creates an integer field in the database to store the category ID
->belongsTo('category', CategoryModel::class, 'id') Defines the relationship: "this employee belongs to a category"
  • 'category': alias for accessing the relationship
  • CategoryModel::class: the related model
  • 'id': the key field in the categories table
->formType('milkSelect') Uses MilkSelect component (dropdown with search)
->options(...) Pre-loads all options at page load
Difference from apiUrl: No AJAX fetch, all data loaded immediately
->nullable() Makes the field optional (use ->required() if mandatory)
⚠️ Key Difference:
  • ->options(...) = Pre-loads all data (this guide)
  • ->apiUrl(...) = Fetches via AJAX while typing (autocomplete guide)

Step 4: Create/Update the Database Tables

Run the CLI command to create all tables and update the schema:

php milkadmin/cli.php employees:update

This command will:

  • Create the #__employee_categories table if it doesn't exist
  • Add the category_id column to the #__employees table
  • Execute the afterCreateTable() method to populate initial data (if defined)
💡 Tip: You can add sample data automatically by defining the afterCreateTable() method in your CategoryModel:
protected function afterCreateTable(): void
{
    $sql = "INSERT INTO `" . $this->table . "` (`name`, `description`) VALUES
        ('Engineering', 'Engineering department'),
        ('Sales', 'Sales department'),
        ('Marketing', 'Marketing department');";
    $this->db->query($sql);
}

Step 5: Display Category in Table

Update your controller's list method to show the category name using dot notation.

Example: EmployeesController.php - list() method

#[RequestAction('home')]
public function list()
{
    // ... your existing code ...

    $tableBuilder = TableBuilder::create($this->model, 'tblEmployees')
        ->column('name', 'Name', 'text')
        ->column('surname', 'Surname', 'text')

        // ========================================
        // ADD THIS LINE TO SHOW CATEGORY
        // ========================================
        ->column('category.name', 'Category', 'text')

        // ... rest of your table configuration ...
        ->render();

    // ... rest of your code ...
}
✅ Dot Notation: The category.name automatically loads the category relationship and displays the name instead of the ID.

Multiple Select - Many-to-Many Relationships

Now let's add a multiple select field where employees can be assigned to multiple categories (or doctors, skills, etc.).

Example: Appointments with Multiple Doctors

The Appointments module already has this implemented. Here's how it works:

// In AppointmentsModel.php
->array('doctor_ids')
    ->options((new DoctorsModel())->getList())
    ->label('Doctors')
    ->formType('milkSelect')
    ->formParams(['type' => 'multiple'])

Understanding Multiple Select

Method Purpose
->array('doctor_ids') Creates a field that stores multiple values as array (JSON in database)
->options(...) Pre-loads all available options
->formType('milkSelect') Uses MilkSelect component
->formParams(['type' => 'multiple']) This makes it multiple! Allows selecting multiple items
💡 Database Storage:
  • Multiple values are stored as JSON array in the database
  • Example: ["1", "3", "5"] for IDs 1, 3, and 5
  • The framework automatically handles JSON encoding/decoding

Complete Example: Employees with Multiple Skills

Let's add a multiple select for employee skills:

1. Create SkillModel.php

<?php
namespace Modules\Employees;
use App\Abstracts\AbstractModel;

class SkillModel extends AbstractModel
{
    protected function configure($rule): void
    {
        $rule->table('#__employee_skills')
            ->id()
            ->string('name', 100)->required()->label('Skill Name');
    }

    public function getList()
    {
        $list = $this->query()->order('name')->getResults();
        $array_return = [];
        foreach ($list as $value) {
            $array_return[$value->id] = $value->name;
        }
        return $array_return;
    }
}

2. Update EmployeesModel.php

protected function configure($rule): void
{
    $rule->table('#__employees')
        ->id()
        ->string('name', 100)->required()
        ->string('surname', 100)->required()

        // Single select (one category)
        ->int('category_id')
            ->belongsTo('category', CategoryModel::class, 'id')
            ->formType('milkSelect')
            ->options((new CategoryModel())->getList())
            ->label('Category')

        // ========================================
        // MULTIPLE SELECT (many skills)
        // ========================================
        ->array('skill_ids')
            ->options((new SkillModel())->getList())
            ->label('Skills')
            ->formType('milkSelect')
            ->formParams(['type' => 'multiple'])
            ->nullable()

        ->timestamp('created_at')->hideFromEdit()->saveValue(time());
}

3. Register SkillModel in EmployeesModule.php

protected function configure($rule): void
{
    $rule->page('employees')
        ->title('Employees')
        ->menu('Employees', '', 'bi bi-people-fill', 20)

        // ========================================
        // REGISTER BOTH SECONDARY MODELS
        // ========================================
        ->addModels([
            'category' => CategoryModel::class,
            'skill' => SkillModel::class
        ])

        ->access('public')
        ->version(20251028);
}

4. Create/Update Tables

php milkadmin/cli.php employees:update

This single command will:

  • Create the #__employee_skills table
  • Add the skill_ids column (TEXT type for JSON storage) to #__employees
  • Execute afterCreateTable() for initial data population
✅ That's it! The form will now show:
  • Category: Single select dropdown (one choice)
  • Skills: Multiple select dropdown (select many)

Displaying Multiple Values in Tables

To display multiple selected values in tables, you need custom formatting:

// In EmployeesController.php
$tableBuilder = TableBuilder::create($this->model, 'tblEmployees')
    ->column('name', 'Name', 'text')
    ->column('category.name', 'Category', 'text')

    // For multiple values, you need custom formatting
    ->column('skill_ids', 'Skills', 'callback', function($value, $record) {
        if (empty($value)) return '-';
        $skillModel = new SkillModel();
        $skills = [];
        foreach ($value as $skillId) {
            $skill = $skillModel->find($skillId);
            if ($skill) $skills[] = $skill->name;
        }
        return implode(', ', $skills);
    })

    ->render();

Comparison: Single vs Multiple vs Autocomplete

Feature Single Pre-loaded Multiple Pre-loaded Autocomplete Fetch
Field Type ->int('category_id') ->array('skill_ids') ->int('user_id')
Selection One item only Multiple items One item only
Data Loading All at page load All at page load AJAX fetch on typing
Configuration ->options(...) ->options(...)
->formParams(['type' => 'multiple'])
->apiUrl(...)
Best For Small lists (10-100 items) Small lists (10-100 items) Large lists (100+ items)
Database Storage Integer (single ID) JSON array of IDs Integer (single ID)

Troubleshooting

Problem: Dropdown shows IDs instead of names

Solution:

  • Check that getList() method exists in the related model
  • Verify getList() returns [id => name] format
  • Ensure you're calling (new CategoryModel())->getList() correctly

Problem: Multiple select not working

Check:

  • Field type is ->array('field_name') not ->int()
  • ->formParams(['type' => 'multiple']) is present
  • Database field type supports JSON storage (TEXT or JSON type)

Problem: Empty dropdown

Solutions:

  • Check that the related table has data
  • Verify getList() method returns non-empty array
  • Check for PHP errors in browser console

Summary: Quick Reference

✅ Single Select (Pre-loaded):
->int('category_id')
    ->belongsTo('category', CategoryModel::class, 'id')
    ->formType('milkSelect')
    ->options((new CategoryModel())->getList())
✅ Multiple Select (Pre-loaded):
->array('skill_ids')
    ->options((new SkillModel())->getList())
    ->formType('milkSelect')
    ->formParams(['type' => 'multiple'])
✅ Autocomplete (Fetch):
->int('user_id')
    ->belongsTo('user', UserModel::class, 'id')
    ->formType('milkSelect')
    ->apiUrl('?page=module&action=related-search-field&f=user_id', 'username')

Related Documentation

Loading...