Skip to main content
Version: 5.4.0

Design a form

Best practices​

A form usually requires a stepper component and form items. To build the form with efficiency, it is recommended to use the following method:

  1. Plan: Create an initial layout that a step form will need, a StepForm component.

  2. Build one form item: Design a single form items with all the correct styling and the mandatory validations.

  3. Duplicate the form item: Clone that form item and to rapidly build the rest of the form.

  4. Repeat: Clone the entire form to create the next section of the form.

  5. Customise: Modify the form item that requires special validation rules.

  6. Design form submission: Allow the user to verify whether they want to submit the form by adding a modal via the Dialog component, as well as a Loading component.

  7. Integrate: Bind form items and the corresponding inputs.

The following shows an example to design a form with dynamic rows.

Create a sample form​

Design a StepForm​

Form Title: Course Application Form

Form Description: Please complete the following questions so we can collect the course, instructor, location, and schedule details.

This form is divided into the following sections, each using a step in the StepForm component. The sections are:

  1. Course Info
  2. Instructor Particulars
  3. Class Location
  4. Class Schedule

Step 1. Course Info​

This section collects the basic course details. Use a Form Item for each field and place the relevant input component inside it.

Field NameComponent TypeRequiredNotes
Course TitleInputYesStores the course name.
Course CodeInputYesStores the unique course code.
Enrollment TypeSelectYesUse dropdown options such as Part-time, Full-time, and Online/Remote. Bind the onChange event to update the Subject Type options.
Subject TypeSelectYesPopulate the options dynamically based on the selected Enrollment Type.
Course FeeInputYesAdd $ as the prefix text.
DescriptionTextAreaNoProvides a larger text box for the course description.

Step 2. Instructor Particulars​

This section collects the instructor's personal and registration details.

Field NameComponent TypeRequiredNotes
SalutationSelectYesUse dropdown options such as Mr, Mrs, Ms, and Dr.
NameInputYesStores the instructor's full name.
NRICInputYesAdd NRIC validation if the form requires Singapore NRIC format checking.
EmailInputYesAdd email format validation.
Contact No.InputYesAdd phone number validation where required.
Registration No.InputYesStores the instructor's registration number.

Step 3. Class Location​

This section collects the venue details for the class.

Field NameComponent TypeRequiredNotes
Postal CodeInputYesStores the postal code for the class venue.
AddressInputYesStores the full address.
Floor / Unit No.InputYesBuild this as two small Input components: one for the floor number and one for the unit number. Add custom validation to require both values.

Step 4. Class Schedule​

This section collects the class date range and weekly schedule. Use a dynamic table row so users can add multiple weekly schedule entries.

Field NameComponent TypeRequiredNotes
Start DateDatePickerYesUse the YYYY-MM-DD format. Add custom validation to make sure the start date is not after the end date.
End DateDatePickerYesUse the YYYY-MM-DD format. Add custom validation to make sure the end date is not before the start date.
Weekly ScheduleTableNoBind the table datasource to an array state, then add a button that appends a new schedule row.

Define the following columns:

Column (dataIndex)Column TitleFormat TypeRequiredNotes
dayDaySelectNoUse weekday options from Monday to Sunday.
startStart TimeTimePickerNoUse the HH:mm format.
endEnd TimeTimePickerNoUse the HH:mm format.

Add an Add Schedule button below the table. Bind its click event to append a new object to the weekly schedule array, for example { day: undefined, start: undefined, end: undefined }.

Source code​

note

The source code must be used together with the component bindings described in the Binding section. Copying the source code alone is not enough for validation, navigation, and data persistence to work.

Copy the following code and paste into the Source Code Panel, then click Save:

View code
class LowcodeComponent extends Component {
state = {
"currentStep": 0,
"startError": '',
"endError": '',
"weekly": [{
"day": undefined,
"start": undefined,
"end": undefined
}],
"enrollmentSubject": [{
"enrollmentId": 1,
"subjectId": ["Accounting", "Business", "Computing"]
}, {
"enrollmentId": 2,
"subjectId": ["Accounting", "Business", "Computing", "Design", "Education"]
}, {
"enrollmentId": 3,
"subjectId": ["Computing", "Education"]
}],
"confirmVisible": false,
"subjectTypeArray": [],
"showAll": false,

// Used to store Field instances
"courseInfoFormField": undefined,
"instructorParticularsFormField": undefined,
"classLocationFormField": undefined,
"classScheduleFormField": undefined,

// Used to store the specific data of each form (for data echoing/re-populating)
"courseInfoData": {},
"instructorParticularsData": {},
"classLocationData": {},
"classScheduleData": {}
}

checkDates(rule, value, callback) {
if (this.state.classScheduleFormField.getValue("startDate") !== undefined && this.state.classScheduleFormField.getValue("endDate") !== undefined) {
if (moment(this.state.classScheduleFormField.getValue("startDate"), 'YYYY-MM-DD').isAfter(this.state.classScheduleFormField.getValue("endDate"))) {
this.setState({
startError: 'error',
endError: 'error'
})
callback("Start Date cannot be after End Date!");
} else {
this.setState({
startError: 'default',
endError: 'default'
})
callback();
}
} else {
callback();
}
return;
}

checkFloorUnit(rule, value, callback) {
const floor = this.state.classLocationFormField.getValue("floorField");
const unit = this.state.classLocationFormField.getValue("unitField");

if (!floor || !unit) {
callback("Both Floor and Unit numbers are required.");
} else {
callback();
}
}

onClick_AddSchedule() {
this.setState({
weekly: [...this.state.weekly, {
"day": undefined,
"start": undefined,
"end": undefined
}]
})
}

onChange_EnrollmentType(...args) {
const subjects = this.state.enrollmentSubject.find(x => x.enrollmentId == args[0]).subjectId;
this.setState({
subjectTypeArray: subjects
});
}

isFormValid() {
let errors = {};
if (this.state.currentStep === 0 && this.state.courseInfoFormField) {
errors = this.state.courseInfoFormField.getErrors();
} else if (this.state.currentStep === 1 && this.state.instructorParticularsFormField) {
errors = this.state.instructorParticularsFormField.getErrors();
} else if (this.state.currentStep === 2 && this.state.classLocationFormField) {
errors = this.state.classLocationFormField.getErrors();
} else if (this.state.currentStep === 3 && this.state.classScheduleFormField) {
errors = this.state.classScheduleFormField.getErrors();
}

for (const value of Object.values(errors)) {
if (value !== null) {
return false;
}
}
return true;
}

// Extracted common method: Save the form data of the current step
saveCurrentStepData() {
if (this.state.currentStep === 0 && this.state.courseInfoFormField) {
this.setState({ courseInfoData: this.state.courseInfoFormField.getValues() });
} else if (this.state.currentStep === 1 && this.state.instructorParticularsFormField) {
this.setState({ instructorParticularsData: this.state.instructorParticularsFormField.getValues() });
} else if (this.state.currentStep === 2 && this.state.classLocationFormField) {
this.setState({ classLocationData: this.state.classLocationFormField.getValues() });
} else if (this.state.currentStep === 3 && this.state.classScheduleFormField) {
this.setState({ classScheduleData: this.state.classScheduleFormField.getValues() });
}
}

onClick_Next(e, v) {
if (this.isFormValid()) {
// Before leaving the current Step, extract and save the existing form values first
this.saveCurrentStepData();

const nextStep = this.state.currentStep + 1;
this.setState({
currentStep: nextStep
});
}
}

onClick_Previous(e, v) {
// Even when returning to the previous step, it's better to save the filled content on the current page
this.saveCurrentStepData();

const prevStep = this.state.currentStep - 1;
if (prevStep >= 0) {
this.setState({
currentStep: prevStep
});
}
}

onClick_Submit(e, v) {
if (this.isFormValid()) {
this.saveCurrentStepData();
this.setState({
confirmVisible: true
});
this.$("confirm-dialog-box").open();
}
}

onClick_ConfirmSubmit() {
this.utils.navigateTo('trainingfoundation', 'ListingPage');
}

// ---- Below are the bound saveField events ----
// When the form component renders, it passes in its own field; when it unmounts, it may pass back null/undefined.
// After receiving the field, we check if the state has previously saved data, and if so, use setValues directly to re-populate the form.

courseInfoSaveField(field) {
this.setState({ courseInfoFormField: field });
if (field && Object.keys(this.state.courseInfoData || {}).length > 0) {
field.setValues(this.state.courseInfoData);
}
}

instructorParticularsSaveField(field) {
this.setState({ instructorParticularsFormField: field });
if (field && Object.keys(this.state.instructorParticularsData || {}).length > 0) {
field.setValues(this.state.instructorParticularsData);
}
}

classLocationSaveField(field) {
this.setState({ classLocationFormField: field });
if (field && Object.keys(this.state.classLocationData || {}).length > 0) {
field.setValues(this.state.classLocationData);
}
}

classScheduleSaveField(field) {
this.setState({ classScheduleFormField: field });
if (field && Object.keys(this.state.classScheduleData || {}).length > 0) {
field.setValues(this.state.classScheduleData);
}
}
}

Binding​

After the components are added to the page, bind the form, step, and table components to the functions and state values defined in the Source Code Panel.

Bind the StepForm​

Bind the StepForm's current step value to:

this.state.currentStep

This allows the Next and Previous buttons to control which step is shown.

Bind each form section​

For each Form component, bind both the field state and the Save Field event. The Save Field event is required because the source code uses the field instance to get values, set values, and check validation errors.

Form sectionField bindingSave field event
Course Infothis.state.courseInfoFormFieldcourseInfoSaveField
Instructor Particularsthis.state.instructorParticularsFormFieldinstructorParticularsSaveField
Class Locationthis.state.classLocationFormFieldclassLocationSaveField
Class Schedulethis.state.classScheduleFormFieldclassScheduleSaveField

In the Advanced tab > Visible, click the Switch Setter, then select Variable Input. Set the following display condition for each Form, so that only the current step is shown:

Form sectionCondition
Course Infothis.state.currentStep == 0 || this.state.showAll
Instructor Particularsthis.state.currentStep == 1 || this.state.showAll
Class Locationthis.state.currentStep == 2 || this.state.showAll
Class Schedulethis.state.currentStep == 3 || this.state.showAll
note

The showAll value is useful when you want to preview or troubleshoot all sections at once.

Bind the form field names​

Set the name property of each Form Item to match the keys used in the source code. These names are used when calling getValues(), setValues(), getValue(), and getErrors().

SectionForm Item names in sequence
Course InfocourseTitle, courseCode, enrollmentType, subjectType, courseFee, description
Instructor Particularssalutation, name, nric, email, contactNo, registrationNo
Class LocationpostalCode, address, floorField, unitField
Class SchedulestartDate, endDate

For the Floor / Unit No. row, bind the two input components with the names floorField and unitField. The checkFloorUnit function uses these names to validate that both values are entered.

important

A red required mark only shows that the field is visually required. To block users from moving to the next step, configure the Form Item validation rule, such as Required, for each mandatory field.

For each mandatory Form Item, configure the validation rule in the Form Item settings:

  1. Select the Form Item.

  2. Set the name property to the matching field name listed in the table above.

  3. Add a Required validation rule.

  4. Enter a validation message, such as Course Title is required..

  5. Save the component settings.

Repeat these steps for each field marked as required in the form design.

For the Floor / Unit No. row, bind checkFloorUnit as the custom validator for both the floor input and the unit input.

Bind the dropdown behaviour​

For Enrollment Type, configure the Select options with values that match the enrollmentId values in this.state.enrollmentSubject.

Then bind the Select onChange event to:

onChange_EnrollmentType

For Subject Type, bind the Select datasource to:

this.state.subjectTypeArray

When the user selects an enrollment type, onChange_EnrollmentType updates subjectTypeArray, and the Subject Type dropdown refreshes with the matching options.

note

The Subject Type datasource depends on the Enrollment Type selection. If the Subject Type dropdown does not refresh, confirm that the Enrollment Type option values match the enrollmentId values in the source code.

Bind the schedule table​

For the Weekly Schedule table, bind the table data to:

this.state.weekly

Create the following table columns:

Column titleData keyComponent
DaydaySelect
Start TimestartTimePicker
End TimeendTimePicker

Set the Start Time and End Time TimePicker format to:

HH:mm

Bind the Add Schedule button onClick event to:

onClick_AddSchedule

This appends a new schedule object to this.state.weekly, which adds a new row to the table.

For Start Date and End Date, bind checkDates as the custom validator. This checks that the start date is not later than the end date.

Bind the navigation buttons​

Bind the step navigation buttons as follows:

ButtonEvent
NextonClick_Next
PreviousonClick_Previous
SubmitonClick_Submit

Configure Next and Submit as validating submit buttons. Configure Previous so that it does not validate the current step, because users should be able to return to an earlier step without completing the current one.

The source code also calls isFormValid() before moving to the next step or opening the confirmation dialog. This makes sure the current step is checked before the form continues.

If users can still move to the next step without filling mandatory fields, check the following:

  • The required rules are configured on the Form Items.
  • Each Form Item has the correct name value.
  • The Form Items are placed inside the correct parent Form.
  • The Next button is configured as a validating submit button.

If values disappear when returning to a previous step, check the following:

  • Each Form has the correct field binding.
  • Each Form has the correct Save Field event.
  • The visibility condition for each Form uses this.state.currentStep.
  • The Form Item name values match the keys used in the source code.

Bind the confirmation dialog​

Bind the confirmation dialog's visibility to:

this.state.confirmVisible

Bind the dialog's OK event to:

onClick_ConfirmSubmit

When the user confirms the submission, the form navigates to another listing page, which is beyond the scope of this article.

Preview the form​

Save the page and open it in preview mode. The form should display one section at a time, with the StepForm indicator showing the current step.

Use the Next and Previous buttons to move between the four sections. When you return to a previous step, the values entered earlier should still be shown because each form section saves its data before the step changes.

In Step 1, select an Enrollment Type and confirm that the Subject Type dropdown is updated based on the selected enrollment type. In Step 4, click Add Schedule and confirm that a new row is added dynamically to the Weekly Schedule table.

note

The screenshots below use mockup data for illustration only. In preview mode, your form should look and behave similarly, but the actual values can be different.

The following shows how the steps look.

Step 1:

Form Step 1

Step 2:

Form Step 2

Step 3:

Form Step 3

Step 4:

Form Step 4

Last updated on 19 Oct 2026