Milk Admin

Adding field with autocomplete search

Revision: 2025/10/27

This guide shows you how to add a field with autocomplete search to your existing module with autocomplete search, table display, and automatic form handling.

What You'll Learn:
  • How to add a user_id field with belongsTo relationship
  • How to enable autocomplete search with MilkSelect
  • How to display usernames in tables using dot notation
  • How to create the search API endpoint
⚠️ Prerequisites: This guide assumes you already have a working module. If you need to create a new module from scratch, see:

Example: Projects Module

We'll use a Projects module as example, where each project can be assigned to a user. This pattern works for:

  • Tasks → assigned to users
  • Orders → created by users
  • Posts → written by users
  • Tickets → opened by users

Step 1: Add user_id Field to Your Model

Open your model file and add the user_id field configuration inside the configure() method.

Example: ProjectsModel.php

protected function configure($rule): void
{
    $rule->table('#__projects')
        ->id()
        ->string('title', 200)->required()
        // ... your other fields ...

        // ========================================
        // ADD THIS USER_ID FIELD
        // ========================================
        ->int('user_id')
            ->nullable()
            ->label('Project Owner')
            ->belongsTo('user', \Modules\Auth\UserModel::class, 'id')
            ->formType('milkSelect')
            ->apiUrl('?page=projects&action=related-search-field&f=user_id', 'username')

        ->timestamp('created_at')->hideFromEdit()->saveValue(time());
}
✅ That's all you need! Using action=related-search-field&f=user_id means no controller method is required.

Understanding Each Method

Method Purpose
->int('user_id') Creates an integer field in the database to store the user ID
->nullable() Makes the field optional (remove this if you want it required)
->label('Project Owner') Sets the label shown in forms
->belongsTo('user', UserModel::class, 'id') Defines the relationship: "this project belongs to a user"
  • 'user': alias for accessing the relationship
  • UserModel::class: the related model
  • 'id': the key field in the users table
->formType('milkSelect') Uses MilkSelect component (autocomplete dropdown)
->apiUrl('...', 'username') Sets the API endpoint and the field to display
  • First parameter: your search API URL
    • Automatic: '?page=projects&action=related-search-field&f=user_id'
    • Custom: '?page=projects&action=search-users'
  • Second parameter: which user field to show (username, email, etc.)
✅ Important: The searchRelated() method is already built into the framework! You don't need to add it to your model.

Step 2: Update the Database

Run the CLI command to add the user_id column to your database table:

php milkadmin/cli.php projects:update

Replace projects with your actual module name (e.g., employees:update, tasks:update).


Step 3: Display Username in Table

Update your controller's list method to show the username in the table using dot notation.

Example: ProjectsController.php - list() method

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

    $tableBuilder = TableBuilder::create($this->model, 'tblProjects')
        // ========================================
        // ADD THIS LINE TO SHOW USERNAME
        // ========================================
        ->column('user.username', 'Owner', 'text')

        // ... rest of your table configuration ...
        ->hideColumns(['created_at'])
        ->render();

    // ... rest of your code ...
}

Understanding Dot Notation

The dot notation user.username automatically:

  1. Loads the user relationship (defined with belongsTo('user', ...))
  2. Extracts the username field from the related user
  3. Displays it in the table
💡 You can access any field from the related user:
  • ->column('user.username', 'Username', 'text')
  • ->column('user.email', 'Email', 'text')
  • ->column('user.status', 'Status', 'text')

Step 4: Add Search API Endpoint

There are two ways to enable the autocomplete search functionality:

Option A: Automatic Method (Recommended for Most Cases)

The simplest approach - no controller code needed! Just use the built-in related-search-field action:

Model Configuration

->int('user_id')
    ->nullable()
    ->label('User')
    ->belongsTo('user', \Modules\Auth\UserModel::class, 'id')
    ->formType('milkSelect')
    ->apiUrl('?page=projects&action=related-search-field&f=user_id', 'username')
✅ That's it! No controller method needed. The framework automatically:
  • Reads the relationship configuration from your model
  • Queries the related table (users in this case)
  • Filters by the search term
  • Returns matching results in the correct format

Understanding the URL

  • action=related-search-field - Uses the built-in automatic handler
  • &f=user_id - Specifies which field relationship to search (must match your field name)
  • 'username' - The field to display from the related table

Option B: Custom Method (For Advanced Filtering)

Use this approach when you need to filter results or apply custom business logic.

Model Configuration

->int('user_id')
    ->nullable()
    ->label('User')
    ->belongsTo('user', \Modules\Auth\UserModel::class, 'id')
    ->formType('milkSelect')
    ->apiUrl('?page=projects&action=search-users', 'username')

Controller Method

#[RequestAction('search-users')]
public function searchUsers() {
    $search = $_REQUEST['q'] ?? '';
    $options = $this->model->searchRelated($search, 'user_id');

    // OPTIONAL: Apply custom filtering here
    // Example: Only show active users
    // $options = array_filter($options, function($userId) {
    //     $user = UserModel::find($userId);
    //     return $user && $user->status === 'active';
    // }, ARRAY_FILTER_USE_KEY);

    Response::json([
        'success' => 'ok',
        'options' => $options
    ]);
}
💡 When to use custom methods:
  • Filter results based on user permissions (e.g., only show users in same department)
  • Apply status filters (e.g., only active users, only verified users)
  • Add custom sorting or grouping
  • Include additional data in the response
  • Log search queries for analytics
⚠️ Important: When using custom methods, make sure the action name in #[RequestAction('search-users')] matches the one in your model's apiUrl().

Step 5: The Form (Automatic!)

If you're using FormBuilder, the form field is generated automatically. No changes needed!

✅ FormBuilder automatically handles:
  • Rendering the MilkSelect autocomplete field
  • Loading the current user when editing
  • Showing username instead of ID
  • Saving the selected user ID

Complete Example

Here's a minimal complete example using the automatic method (recommended):

1. Model Configuration

->int('user_id')
    ->nullable()
    ->label('User')
    ->belongsTo('user', \Modules\Auth\UserModel::class, 'id')
    ->formType('milkSelect')
    ->apiUrl('?page=projects&action=related-search-field&f=user_id', 'username')

2. Table Configuration

$tableBuilder = TableBuilder::create($this->model, 'tblProjects')
    ->column('user.username', 'Owner', 'text')
    // ... rest of your config ...

3. Controller API Method

Not needed! The related-search-field action handles everything automatically.

💡 Want custom filtering? Use Option B from Step 4 instead and add your custom searchUsers() method to the controller.

How It Works

1. User Opens the Form

FormBuilder generates MilkSelect field
↓
If editing existing record:
  belongsTo loads the user → shows "mario" instead of "5"

2. User Types in the Autocomplete

With Automatic Method (related-search-field):

User types "mar"
↓
After 300ms → AJAX request to: ?page=projects&action=related-search-field&f=user_id&q=mar
↓
Framework automatically:
  - Reads belongsTo config for user_id
  - Queries: SELECT * FROM users WHERE username LIKE '%mar%' LIMIT 20
↓
Returns: {"success":"ok", "options":{"1":"mario","5":"marco"}}

With Custom Method (search-users):

User types "mar"
↓
After 300ms → AJAX request to: ?page=projects&action=search-users&q=mar
↓
searchUsers() in controller calls searchRelated("mar", "user_id")
↓
+ Custom filtering/logic applied (if any)
↓
Framework queries: SELECT * FROM users WHERE username LIKE '%mar%' LIMIT 20
↓
Returns: {"success":"ok", "options":{"1":"mario","5":"marco"}}

3. User Selects and Saves

User selects "mario" (ID=1)
↓
FormBuilder saves: UPDATE projects SET user_id=1 WHERE id=10

Common Variations

Make the Field Required

->int('user_id')
    ->required()  // ← Add this
    ->error('Please select a user')  // Optional custom error
    ->belongsTo('user', \Modules\Auth\UserModel::class, 'id')
    ->formType('milkSelect')
    ->apiUrl('?page=projects&action=search-users', 'username')

Display Email Instead of Username

// In Model:
->apiUrl('?page=projects&action=search-users', 'email')  // ← Change to 'email'

// In Table:
->column('user.email', 'User Email', 'text')

Add Multiple User Fields

// Owner
->int('owner_id')
    ->belongsTo('owner', \Modules\Auth\UserModel::class, 'id')
    ->formType('milkSelect')
    ->apiUrl('?page=projects&action=related-search-field&f=owner_id', 'username')
    ->label('Project Owner')

// Manager
->int('manager_id')
    ->nullable()
    ->belongsTo('manager', \Modules\Auth\UserModel::class, 'id')
    ->formType('milkSelect')
    ->apiUrl('?page=projects&action=related-search-field&f=manager_id', 'username')
    ->label('Project Manager')

The related-search-field endpoint automatically handles different fields - just change the &f= parameter!


Troubleshooting

Problem: Shows ID instead of username in form

Solution:

  • Check that ->belongsTo('user', ...) is present
  • The alias in belongsTo must match: belongsTo('user', ...) and column('user.username', ...)

Problem: Autocomplete doesn't work

Check:

  • The apiUrl() endpoint matches your action: apiUrl('?page=projects&action=search-users', ...)
  • The #[RequestAction('search-users')] exists in controller
  • Open browser console (F12) and check for JavaScript errors
  • Check Network tab to see if API request is sent

Problem: Empty username column in table

Solutions:

  • Check that records have user_id values in the database
  • The relationship alias must match: belongsTo('user', ...)column('user.username', ...)

Summary: Quick Checklist

✅ To add user selection to your module (Simple Method):
  1. Model: Add user_id field with belongsTo(), formType('milkSelect'), and apiUrl('...&action=related-search-field&f=user_id', ...)
  2. Database: Run php milkadmin/cli.php yourmodule:update
  3. Table: Add ->column('user.username', 'Owner', 'text') in your list method
  4. Controller: Nothing needed! The framework handles search automatically
  5. Form: Nothing! FormBuilder handles it automatically
💡 Need custom filtering? Use action=search-users in apiUrl() and add a custom searchUsers() method to your controller (see Option B in Step 4).

Related Documentation

Loading...