A slot booking widget that guides the user through a four-step cascading selection: group type → group → time slot → number of seats. Each step is unlocked only after the previous one is filled, and available options are fetched live from a schedule defined in the platform.
Read Form Rules before using this component — keys, labels, required fields, expressions, visibility, and validation all follow shared conventions.
- Appointment booking where users must pick a group type, group, and time slot before reserving a seat
- Any service backed by the platform's schedule/slot API (
/service/api/v1/schedules/{scheduleId}/...)
Instead of customslotbooking, use… |
For… |
customdatepicker |
Simple date/time picking with no slot capacity logic |
customcascadingdropdowns |
Generic hierarchical dropdown not backed by schedules |
| Prop |
Type |
Required? |
Description |
label |
string |
required |
Visible label for the overall widget. |
required |
boolean |
required |
Always false. The expression controls the runtime value. |
defaultRequired |
boolean |
required |
true for required fields, false for optional ones. |
scheduleId |
string |
required |
UUID of the schedule that supplies group types, groups, and slots. Set in the portal settings panel or here directly. |
configs |
array |
required |
Fixed array of exactly 4 config objects — one per step. See below. |
hideField |
boolean |
optional |
Hides the entire widget. Toggle via expressions["props.hideField"]. |
configs is always an array of exactly 4 objects, in this order:
| Index |
Step |
isSeats |
Notes |
| 0 |
Group type |
— |
Root dropdown; no parent. Fetches from the schedule's group-types API. |
| 1 |
Group |
— |
Depends on index 0. Fetches groups for the chosen group type. |
| 2 |
Time slot |
— |
Depends on index 1. Fetches available slots for the chosen group. |
| 3 |
Seats |
true |
Depends on index 2. Number input; user enters how many seats to book. |
| Field |
Type |
Required? |
Description |
key |
string |
required |
UPPER_SNAKE_CASE key. Each config must have a unique key. |
label |
string |
required |
Visible label for this step's input. |
parent |
string |
optional |
key of the preceding config. Absent on index 0 (root); required on 1 and 2. |
required |
boolean |
required |
Always true for all four steps. |
bindlabel |
string |
required |
Key in each option object to display. "name" for group type and group; "label" for time slot. |
bindvalue |
string |
required |
Key in each option object to store. "code" for group type and group; "slotId" for time slot. |
placeholder |
string |
optional |
Ghost text shown before the user picks. |
requiredErrorMessage |
string |
optional |
Validation message when the step is skipped. |
All fields above except bindlabel/bindvalue, plus:
| Field |
Type |
Required? |
Description |
isSeats |
boolean |
required |
Must be true. Tells the runtime this step is a number input. |
maxSeats |
number |
optional |
Client-side cap on the number of seats. Enforced before submission. |
defaultValue |
number |
optional |
Pre-filled seat count. |
hidden |
boolean |
optional |
Hides the seats step entirely (e.g. when seats are not applicable). |
maxSeatErrorMessage |
string |
optional |
Shown when the count exceeds maxSeats. |
minSeatErrorMessage |
string |
optional |
Shown when the count is less than 1. |
numbersOnlyErrorMessage |
string |
optional |
Shown when the input is not a valid number. |
| Key |
Description |
required |
Shown when the overall widget is required and not completed. |
invalidSlot |
Shown when the chosen slot is no longer available at submission time. |
The widget calls the schedule API under the hood — the scheduleId prop is substituted into the paths:
| Step |
Endpoint |
| Group type |
GET /service/api/v1/schedules/{scheduleId}/group-types |
| Slot |
GET /service/api/v1/schedules/{scheduleId}/slots |
Groups and available seats are derived from the group-types and slots responses.
{
"key": "SLOT_BOOKING",
"type": "customslotbooking",
"props": {
"label": "Book an appointment",
"required": false,
"defaultRequired": true,
"scheduleId": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
"configs": [
{
"key": "SLOT_GROUP_TYPE",
"label": "Group Type",
"required": true,
"bindlabel": "name",
"bindvalue": "code",
"placeholder": "Select a group type",
"requiredErrorMessage": "Group type is required."
},
{
"key": "SLOT_GROUP",
"label": "Group",
"parent": "SLOT_GROUP_TYPE",
"required": true,
"bindlabel": "name",
"bindvalue": "code",
"placeholder": "Select a group",
"requiredErrorMessage": "Group is required."
},
{
"key": "SLOT_TIME_RANGE",
"label": "Time Slot",
"parent": "SLOT_GROUP",
"required": true,
"bindlabel": "label",
"bindvalue": "slotId",
"placeholder": "Select a time slot",
"requiredErrorMessage": "Time slot selection is required."
},
{
"key": "SLOT_SEATS",
"label": "Number of Seats",
"parent": "SLOT_TIME_RANGE",
"isSeats": true,
"required": true,
"placeholder": "Enter number of seats",
"maxSeats": 5,
"requiredErrorMessage": "Number of seats is required.",
"maxSeatErrorMessage": "Exceeds the maximum allowed seats for this slot.",
"minSeatErrorMessage": "Minimum of 1 seat required.",
"numbersOnlyErrorMessage": "Please enter a valid whole number."
}
]
},
"expressions": {
"props.required": "!(field?.props?.hideField || field?.hide) && field?.props?.defaultRequired"
},
"validation": {
"messages": {
"required": "This field is required.",
"invalidSlot": "The selected slot is no longer available. Please choose another."
}
}
}