🌐 AJAX Configuration
Basic AJAX Setup
SelectiveUI.bind('#mySelect', {
ajax: {
url: '/api/search',
method: 'POST',
keepSelected: true
}
});
AJAX Options
| Option |
Type |
Required |
Description |
url |
String |
Required |
API endpoint URL |
method |
String |
Optional |
"GET" or "POST". Default: "GET" |
keepSelected |
Boolean |
Optional |
Keep selected items. Default: false |
data |
Object | Function |
Optional |
Additional data sent to the server for search requests |
dataByValues |
Function |
Optional |
Custom function to prepare payload when loading specific values. If not provided, default payload is used. |
Request Parameters
Search Request (Normal)
SelectiveUI automatically sends the following parameters for search:
| Parameter |
Type |
Description |
search |
String |
Keyword search |
page |
Number |
Current page (pagination) |
selectedValue |
String |
Selected values (comma separated) |
Load by Values Request (setValue)
When using setValue() with values not yet loaded, SelectiveUI sends:
| Parameter |
Type |
Description |
load_by_values |
String |
Flag "1" to indicate this is a load-by-values request |
values |
String |
Comma-separated list of values to load (e.g., "101,202,303") |
+ data params |
Mixed |
Additional parameters from data config (if function) |
💡 Auto-load Missing Values:
When you use setValue() with values that don't exist in the current options, SelectiveUI automatically loads them from the server in the background. This works synchronously - existing values are selected immediately, while missing values are loaded without blocking the UI.
Response Format
The server needs to return JSON in one of the following formats (same for both search and load-by-values requests):
Format 1: Simple Array (Options only)
[
{ "value": "1", "text": "Option 1" },
{ "value": "2", "text": "Option 2" }
]
Format 2: With Groups (Simple)
[
{
"label": "Fruits",
"options": [
{ "value": "apple", "text": "Apple 🍎" },
{ "value": "banana", "text": "Banana 🍌" }
]
},
{
"label": "Vegetables",
"options": [
{ "value": "carrot", "text": "Carrot 🥕" }
]
},
{ "value": "other", "text": "Other (no group)" }
]
Format 3: With Groups (Explicit Type)
[
{
"type": "optgroup",
"label": "Fruits",
"data": {
"collapsed": "false"
},
"options": [
{
"value": "apple",
"text": "Apple 🍎",
"data": {
"imgsrc": "/images/apple.jpg"
}
}
]
},
{
"type": "option",
"value": "other",
"text": "Other item"
}
]
Format 4: With Pagination
{
"object": [
{ "value": "1", "text": "Option 1" },
{ "value": "2", "text": "Option 2" }
],
"page": 0,
"total_page": 5
}
Format 5: With Groups & Pagination
{
"data": [
{
"label": "Fruits",
"options": [
{ "value": "apple", "text": "Apple 🍎" }
]
},
{ "value": "single", "text": "Single option" }
],
"page": 0,
"totalPages": 5,
"hasMore": true
}
Format 6: With items key
{
"items": [
{
"type": "optgroup",
"label": "Electronics",
"options": [
{ "value": "phone", "text": "Phone" }
]
}
],
"pagination": {
"page": 0,
"totalPages": 5,
"hasMore": true
}
}
Item Object Properties
Option Properties
| Property |
Type |
Required |
Description |
type |
String |
Optional |
"option" (automatically detect if not available label) |
value |
String |
Required |
Value of options |
text |
String |
Required |
Text is displayed |
selected |
Boolean |
Optional |
Selected or not |
data |
Object |
Optional |
Custom data (data-* attributes) |
imgsrc |
String |
Optional |
Image URL (when imageMode = true) |
OptGroup Properties
| Property |
Type |
Required |
Description |
type |
String |
Optional |
"optgroup" (Automatically detect if there is a label or options) |
label |
String |
Required |
Group name |
data |
Object |
Optional |
Custom data for optgroup (ví dụ: collapsed) |
options |
Array |
Required |
Array of option objects |
Pagination Properties
| Property |
Type |
Optional |
Description |
page |
Number |
Optional |
Current page number |
totalPages | total_page |
Number |
Optional |
Total number of pages |
hasMore |
Boolean |
Optional |
The next page will be counted automatically if there is none |
Dynamic Data Function
SelectiveUI.bind('#mySelect', {
ajax: {
url: '/api/search',
data: function(searchTerm, page) {
return {
search: searchTerm,
page: page,
category: 'electronics',
limit: 20,
userId: getCurrentUserId()
};
}
}
});
Server-side Examples
PHP Example (With Groups)
<?php
$search = $_POST['search'] ?? '';
$page = (int)($_POST['page'] ?? 0);
$limit = 20;
$offset = $page * $limit;
$query = "SELECT
c.name as category_name,
p.id as value,
p.name as text,
p.image_url as imgsrc
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.name LIKE ?
ORDER BY c.name, p.name
LIMIT ? OFFSET ?";
$stmt = $pdo->prepare($query);
$stmt->execute(["%$search%", $limit, $offset]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
$grouped = [];
$currentGroup = null;
foreach ($results as $row) {
if ($row['category_name']) {
if ($currentGroup === null || $currentGroup['label'] !== $row['category_name']) {
if ($currentGroup !== null) {
$grouped[] = $currentGroup;
}
$currentGroup = [
'label' => $row['category_name'],
'options' => []
];
}
$currentGroup['options'][] = [
'value' => $row['value'],
'text' => $row['text'],
'data' => ['imgsrc' => $row['imgsrc']]
];
} else {
if ($currentGroup !== null) {
$grouped[] = $currentGroup;
$currentGroup = null;
}
$grouped[] = [
'value' => $row['value'],
'text' => $row['text']
];
}
}
if ($currentGroup !== null) {
$grouped[] = $currentGroup;
}
$countQuery = "SELECT COUNT(*) FROM products WHERE name LIKE ?";
$total = $pdo->query($countQuery, ["%$search%"])->fetchColumn();
$totalPages = ceil($total / $limit);
header('Content-Type: application/json');
echo json_encode([
'data' => $grouped,
'page' => $page,
'totalPages' => $totalPages,
'hasMore' => $page < $totalPages - 1
]);
?>
Node.js Example (With Groups)
app.post('/api/search', async (req, res) => {
const { search = '', page = 0 } = req.body;
const limit = 20;
const offset = page * limit;
const results = await db.query(`
SELECT
c.name as category_name,
p.id as value,
p.name as text,
p.image_url as imgsrc
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.name ILIKE $1
ORDER BY c.name, p.name
LIMIT $2 OFFSET $3
`, [`%${search}%`, limit, offset]);
const grouped = [];
let currentGroup = null;
results.rows.forEach(row => {
if (row.category_name) {
if (!currentGroup || currentGroup.label !== row.category_name) {
if (currentGroup) grouped.push(currentGroup);
currentGroup = {
label: row.category_name,
options: []
};
}
currentGroup.options.push({
value: row.value,
text: row.text,
data: { imgsrc: row.imgsrc }
});
} else {
if (currentGroup) {
grouped.push(currentGroup);
currentGroup = null;
}
grouped.push({
value: row.value,
text: row.text
});
}
});
if (currentGroup) grouped.push(currentGroup);
const total = await db.query(
'SELECT COUNT(*) FROM products WHERE name ILIKE $1',
[`%${search}%`]
);
const totalPages = Math.ceil(total.rows[0].count / limit);
res.json({
data: grouped,
page: page,
totalPages: totalPages,
hasMore: page < totalPages - 1
});
});
Complete AJAX Example with Groups
SelectiveUI.bind('#products', {
ajax: {
url: '/api/products/search',
method: 'POST',
keepSelected: true,
data: function(search, page) {
return {
q: search,
page: page,
per_page: 20,
include_categories: true
};
}
},
placeholder: 'Search products...',
searchable: true,
imageMode: true,
imageWidth: '60px',
imageHeight: '60px',
multiple: true,
on: {
load: (callback, instance) => {
console.log('Products loaded with groups');
},
change: (callback, instance, value) => {
console.log('Selected products:', value);
}
}
});
Tips for AJAX with Groups
- Group Detection: The library automatically detects groups if an item has a
label or options property
- Mixed Content: It is possible to mix groups and single options in the same response
- Group Data: Use
data.collapsed = "true" to cause the default group to collapse
- Pagination: Pagination works the same way for both grouped and non-grouped data
- KeepSelected: Option
keepSelected: true still works with items in groups
- Performance: Groups help organize data better, especially with large lists
Troubleshooting
Q: Groups not showing?
- Check if the response is in the correct format (label + options)
- Check the console log for parsing errors
Q: Does pagination not work with groups?
- Ensure that the page, totalPages are returned correctly
- Check infinite scroll trigger distance
Q: Are the selected items in the group missing?
- Use keepSelected: true in your ajax config
- The server needs to return the selectedValue in the request
💡 Examples
1. Basic Single Select
<select id="country">
<option value="vn">Vietnam</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>
<script>
SelectiveUI.bind('#country', {
placeholder: 'Select country',
searchable: true
});
</script>
2. Multiple Selection
<select id="human" data-placeholder="Human functions" multiple>
<option value="wa">Walk</option>
<option value="sl">Sleep</option>
<option value="ea">Eat</option>
<option value="dr">Drink</option>
</select>
<script>
SelectiveUI.bind('#human');
</script>
3. With Images
<select id="products">
<option value="1" data-imgsrc="img/product1.jpg">Product 1</option>
<option value="2" data-imgsrc="img/product2.jpg">Product 2</option>
</select>
<script>
SelectiveUI.bind('#products', {
imageMode: true,
imageWidth: '80px',
imageHeight: '80px',
imagePosition: 'left',
imageBorderRadius: '8px'
});
</script>
4. AJAX with Pagination
SelectiveUI.bind('#users', {
ajax: {
url: '/api/users',
method: 'POST',
keepSelected: true,
data: function(search, page) {
return {
q: search,
page: page,
per_page: 20
};
}
},
placeholder: 'Search users...',
searchable: true
});
5. Prevent Change with Validation
SelectiveUI.bind('#restricted', {
on: {
beforeChange: (callback, instance, newValue) => {
if (!hasPermission(newValue)) {
alert('You do not have permission!');
callback.cancel();
}
}
}
});
6. Custom Mask Display
<select id="users">
<option value="1" data-mask="<b>John</b> Doe">John Doe</option>
<option value="2" data-mask="<b>Jane</b> Smith">Jane Smith</option>
</select>
<script>
SelectiveUI.bind('#users', {
allowHtml: true
});
</script>
7. Programmatic Control
const select = SelectiveUI.find('#mySelect');
select.value = '3';
select.value = ['1', '2', '5'];
console.log(select.valueText);
select.open();
setTimeout(() => select.close(), 3000);
8. Destroy & Rebind
SelectiveUI.destroy('#mySelect');
SelectiveUI.rebind('#mySelect', {
multiple: true,
placeholder: 'New placeholder'
});