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.
- How to add a
user_idfield withbelongsTorelationship - How to enable autocomplete search with MilkSelect
- How to display usernames in tables using dot notation
- How to create the search API endpoint
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());
}
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"
|
->formType('milkSelect') |
Uses MilkSelect component (autocomplete dropdown) |
->apiUrl('...', 'username') |
Sets the API endpoint and the field to display
|
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:
- Loads the
userrelationship (defined withbelongsTo('user', ...)) - Extracts the
usernamefield from the related user - Displays it in the table
->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')
- 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
]);
}
- 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
#[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!
- 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.
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', ...)andcolumn('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_idvalues in the database - The relationship alias must match:
belongsTo('user', ...)→column('user.username', ...)
Summary: Quick Checklist
- Model: Add
user_idfield withbelongsTo(),formType('milkSelect'), andapiUrl('...&action=related-search-field&f=user_id', ...) - Database: Run
php milkadmin/cli.php yourmodule:update - Table: Add
->column('user.username', 'Owner', 'text')in your list method - Controller: Nothing needed! The framework handles search automatically
- Form: Nothing! FormBuilder handles it automatically
action=search-users in apiUrl() and add a custom searchUsers() method to your controller (see Option B in Step 4).