Conditional Visibility
Progressive disclosure is a design pattern where form fields appear or disappear based on previous user selections. This improves user experience by showing only relevant fields and reducing visual clutter. With Signals, you can implement this pattern declaratively using bindVisible().
The Pattern
The key to conditional visibility is:
-
Create
ValueSignalinstances to track the values that control visibility -
Bind form fields to both the data bean (using
Binder) and to signals (usingbindValue()) -
Use
bindVisible()to show or hide sections based on signal values
This creates a reactive chain: when a user changes a field, the signal updates, which triggers visibility changes in dependent sections.
Example: Visa Application Form
This example demonstrates a multi-level visa application form where fields appear progressively based on user selections.
Setting Up Signals and Data
First, create the signals that control visibility:
Source code
ConditionalVisibility.java
Each signal tracks a value that other fields depend on. Initialize them with sensible defaults.
Level 0: Base Question
The root question controls whether any visa-related fields appear:
Source code
ConditionalVisibility.java
The checkbox is bound to both:
-
The data bean via
Binder- for data persistence -
The signal via
bindValue()- for reactive visibility control
When the user changes this checkbox, needsVisaSignal updates automatically.
Level 1: Conditional Section
The visa section appears only when sponsorship is needed:
Source code
ConditionalVisibility.java
The bindVisible(needsVisaSignal) method creates a reactive binding: the section’s visibility automatically tracks the signal’s boolean value. When needsVisaSignal is true, the section appears. When false, it’s hidden.
The visaTypeSelect field is also bound to visaTypeSignal, which allows the next level to react to the selected visa type.
Level 2: Nested Conditions
For more complex conditions, use a lambda expression:
Source code
ConditionalVisibility.java
The lambda () → needsVisaSignal.get() && visaTypeSignal.get() == VisaType.H1B reads multiple signals. This section appears only when:
-
Visa sponsorship is needed (
needsVisaSignalistrue) -
The selected visa type is H1-B
The framework tracks which signals you access in the lambda and automatically updates visibility when any of them change.
Level 3: Multi-Level Nesting
You can nest conditions as deeply as needed:
Source code
ConditionalVisibility.java
This section requires three conditions to be true:
-
Visa sponsorship is needed
-
Visa type is H1-B
-
User has held H1-B previously
Each level progressively discloses more details, creating a smooth, focused user experience.
Key Concepts
Value Binding
bindValue() connects a form field to a signal using two callbacks:
Source code
Java
checkbox.bindValue(needsVisaSignal, needsVisaSignal::set);
comboBox.bindValue(visaTypeSignal, visaTypeSignal::set);The method signature is:
Source code
Java
field.bindValue(Signal<V> signal, Consumer<V> writeCallback)Read callback (signal): The first parameter is a Signal<V>. The framework reads its value using signal.get() whenever it needs to update the field. When the signal changes, the field automatically updates.
Write callback: The second parameter is a Consumer<V> that receives the new value when the user changes the field. Typically, you pass the signal’s setter (e.g., signal::set) to update the signal with the new field value, but the callback can also run some validation for input value.
This creates a two-way binding:
-
User changes field → write callback is invoked → signal is updated via
set() -
Signal changes → framework reads via
get()→ field is updated
Null Write Callback
You can pass null as the write callback for read-only bindings:
Source code
Java
field.bindValue(mySignal, null);In this case:
-
The field displays the signal’s value
-
User changes to the field are ignored (the signal is not updated)
-
The signal can still be updated programmatically, and the field reflects those changes
This is useful when you want a field to display signal-driven data but prevent user input from modifying that data directly.
Visibility Binding
bindVisible() controls component visibility reactively:
Source code
Java
// Simple: show when signal is true
component.bindVisible(booleanSignal);
// Complex: show based on multiple conditions
component.bindVisible(() -> signal1.get() && signal2.get() == expectedValue);The framework automatically:
-
Tracks which signals are accessed in the lambda
-
Re-evaluates visibility when any tracked signal changes
-
Updates the component’s visibility accordingly
Combining with Binder
Use Binder alongside signals to maintain data integrity:
Source code
Java
Checkbox checkbox = new Checkbox("Label");
binder.forField(checkbox).bind(Data::getValue, Data::setValue); // Data binding
checkbox.bindValue(signal, signal::set); // Reactive bindingThis dual binding ensures:
-
Data flows into your business objects (via
Binder) -
UI reactivity works correctly (via signals)
Best Practices
Create one signal per conditional value: If three checkboxes control visibility independently, create three boolean signals.
Initialize signals with defaults: Set initial values that match your form’s default state to avoid inconsistencies.
Use lambda expressions for complex conditions: When visibility depends on multiple signals or computed logic, use bindVisible(() → …).
Keep conditions readable: Extract complex visibility logic into well-named variables or methods if needed.
When to Use This Pattern
Use conditional visibility with signals when:
-
Form fields should appear or disappear based on user input
-
You need progressive disclosure for complex forms
-
Multiple levels of nesting are required
-
Visibility logic needs to be reactive and automatic
For simple show/hide based on a single button click, regular setVisible() may be sufficient. Use signals when the visibility rules are more complex or data-driven.