This example shows a 3-level product hierarchy picker built with three OPTION inputs. The available values in each level depend on the selection made in the previous level.
Phase: Init
form.createOptionEntry("Level1")
.setLabel("Level 1")
.setRequired(true)
.onValueChange(["Level2", "Level3"])
.buildFormInput()
form.createOptionEntry("Level2")
.setLabel("Level 2")
.setRequired(true)
.onValueChange(["Level3"])
.buildFormInput()
form.createOptionEntry("Level3")
.setLabel("Level 3")
.setRequired(true)
.buildFormInput()
This code creates three required option inputs: Level 1, Level 2, and Level 3. The first two inputs use onValueChange() with lockInputNames so that dependent inputs are temporarily read-only while their values and value options are refreshed.
Phase: ValueOptions
switch (form.sourceInputName()) {
case "Level1":
return level1_ValueOptions()
case "Level2":
return level2_ValueOptions(input.Level1)
case "Level3":
return level3_ValueOptions(input.Level1, input.Level2)
default:
return []
}
This code clears dependent selections when a higher-level value changes. If Level 1 changes, both Level 2 and Level 3 are reset and their value options are invalidated. If Level 2 changes, Level 3 is reset and refreshed.
Because lockInputNames is defined in onValueChange(), the affected dependent inputs are temporarily read-only while the update is being processed. Once the request finishes, the inputs become editable again. If multiple changes are triggered before the response arrives, only the latest response should take effect.
Level 1 Value Options
List level1_ValueOptions() {
def qapi = api.queryApi()
def t = qapi.tables().companyParameterRows("Forms_ProductHierarchy")
return qapi.source(t, [t.Level1, t.Level1Label])
.distinct()
.sortBy { [qapi.orders().ascNullsFirst(it.Level1Label)] }
.stream {
it.collectEntries {
[(it.Level1): it.Level1Label]
}
}
}
This function loads distinct Level 1 values and labels from the Forms_ProductHierarchy company parameter table and returns them as option key-value pairs.
Level 2 Value Options
List level2_ValueOptions(String level1) {
def qapi = api.queryApi()
def exprs = qapi.exprs()
def t = qapi.tables().companyParameterRows("Forms_ProductHierarchy")
def filters = [exprs.bool(true).equal(true)]
if (level1) {
filters << t.Level1.equal(level1)
}
return qapi.source(t, [t.Level2, t.Level2Label], exprs.and(*filters))
.distinct()
.sortBy { [qapi.orders().ascNullsFirst(it.Level2)] }
.stream {
it.collectEntries {
[(it.Level2): it.Level2Label]
}
}
}
This function loads Level 2 options from the same table and filters them by the selected Level 1 value. As a result, users only see Level 2 options relevant to their Level 1 selection.
Level 3 Value Options
List level3_ValueOptions(String level1, String level2) {
def qapi = api.queryApi()
def exprs = qapi.exprs()
def t = qapi.tables().companyParameterRows("Forms_ProductHierarchy")
def filters = [exprs.bool(true).equal(true)]
if (level1) {
filters << t.Level1.equal(level1)
}
if (level2) {
filters << t.Level2.equal(level2)
}
return qapi.source(t, [t.Level3, t.Level3Label], exprs.and(*filters))
.distinct()
.sortBy { [qapi.orders().ascNullsFirst(it.Level3)] }
.stream {
it.collectEntries {
[(it.Level3): it.Level3Label]
}
}
}
This function loads Level 3 options and filters them by both Level 1 and Level 2. This ensures that the third picker shows only the values valid for the selected hierarchy path.
Phase: OnValueChange
switch (form.sourceInputName()) {
case "Level1":
form.updateInput("Level2")
.setValue(null)
.invalidateValueOptions()
form.updateInput("Level3")
.setValue(null)
.invalidateValueOptions()
break
case "Level2":
form.updateInput("Level3")
.setValue(null)
.invalidateValueOptions()
break
}
This code clears dependent selections when a higher-level value changes. If Level 1 changes, both Level 2 and Level 3 are reset and their value options are invalidated. If Level 2 changes, Level 3 is reset and refreshed.
Summary
Use this pattern when option values depend on previous selections. It is useful for hierarchical pickers where each next level must be filtered according to the current selection path.
To prevent inconsistent state during asynchronous updates, you can define lockInputNames in onValueChange() so that dependent inputs are locked while the update is running. Use excludeInputNames only if you need to omit selected inputs from the payload sent to the backend, for example when the form also contains large inputs that do not need to be submitted for this action.