Milk Admin

FormBuilder - Conditional Field Visibility

The FormBuilder class provides powerful methods to show or hide form fields based on the value of other fields. This allows you to create dynamic, user-friendly forms that only display relevant fields based on user selections.

💡 Hiding Fields Permanently

If you need to hide a field completely (not conditionally), you have two options:

  • In the Controller: Use modifyField() to set the field type as hidden:
    ->modifyField('field_name', ['form-type' => 'hidden'])
  • In the Model: Use hideFromEdit() to hide the field from edit forms:
    ->string('field_name', 100)->hideFromEdit()

Overview

Conditional visibility allows you to:

  • Show/hide fields based on dropdown selections
  • Display additional fields when a checkbox is checked
  • Create multi-step forms with conditional logic
  • Reduce form complexity by hiding irrelevant fields

Basic Methods

showIf() - Single Field or Container

Shows a field or container when a milk expression evaluates to true.

// Syntax
$formBuilder->showIf($field_or_container_id, $expression)

// Parameters:
// - $field_or_container_id: The field name or container id to show/hide
// - $expression: Milk expression that evaluates to true/false

Simple Example

User Status Form

This example shows different fields based on the selected status value.

namespace Modules;

use App\Abstracts\{AbstractModule, AbstractModel};
use App\Attributes\RequestAction;
use App\Response;

class UserStatusModule extends AbstractModule {

    protected function configure($rule): void
    {
        $rule->page('userStatus')
             ->title('User Status Form')
             ->menu('User Status')
             ->access('registered');
    }

    #[RequestAction('home')]
    public function home() {
        $form = \Builders\FormBuilder::create($this->model, $this->page)
            ->showIf('activation_date', '[status] == "active"')
            ->showIf('activated_by', '[status] == "active"')
            ->showIf('reason_inactive', '[status] == "inactive"')
            ->showIf('archive_date', '[status] == "archived"')
            ->showIf('archive_notes', '[status] == "archived"')
            ->addStandardActions()
            ->render();

        Response::render($form, ['title' => $this->title, 'form' => $form]);
    }
}

class UserStatusModel extends AbstractModel {
    protected function configure($rule): void
    {
        $rule
        ->table('user_status')
        ->id()
        ->string('name', 100)

        // Status dropdown - controls visibility of other fields
        ->string('status', 50)->options([
            'pending' => 'Pending',
            'active' => 'Active',
            'inactive' => 'Inactive',
            'archived' => 'Archived'
        ])->formType('list')

        ->string('description', 255)

        // Conditional fields - shown only when status = 'active'
        ->date('activation_date', false)
        ->string('activated_by', 100, false)

        // Conditional field - shown only when status = 'inactive'
        ->text('reason_inactive', false)

        // Conditional fields - shown only when status = 'archived'
        ->date('archive_date', false)
        ->text('archive_notes', false);
    }
}

How It Works

Behind the Scenes

When you use showIf(), the FormBuilder:

  1. Adds data-milk-show attribute to the field wrapper or container
  2. Applies style="display:none" to hide the field initially
  3. JavaScript evaluates the expression on each recalculation
  4. When the expression is true, the field is shown; otherwise it is hidden

Generated HTML Example

<!-- The control field (status) -->
<div class="mb-3">
    <label for="status">Status</label>
    <select name="status" id="status" class="form-control">
        <option value="pending">Pending</option>
        <option value="active">Active</option>
        <option value="inactive">Inactive</option>
        <option value="archived">Archived</option>
    </select>
</div>

<!-- Conditional field - initially hidden -->
<div class="mb-3"
     data-milk-show="[status] == "active""
     style="display:none">
    <label for="activation_date">Activation Date</label>
    <input type="date" name="activation_date" id="activation_date" class="form-control">
</div>

Common Use Cases

1. Dropdown-Based Conditional Fields

$form = \Builders\FormBuilder::create($this->model, $this->page)
    // Show shipping address fields only when shipping type is 'custom'
    ->showIf('shipping_address', '[shipping_type] == "custom"')
    ->showIf('shipping_city', '[shipping_type] == "custom"')
    ->showIf('shipping_zip', '[shipping_type] == "custom"')

    // Show billing fields only when billing type is 'different'
    ->showIf('billing_address', '[billing_type] == "different"')
    ->showIf('billing_city', '[billing_type] == "different"')
    ->showIf('billing_zip', '[billing_type] == "different"')
    ->render();

2. Checkbox-Based Conditional Fields

$form = \Builders\FormBuilder::create($this->model, $this->page)
    // Show company fields only when 'is_company' checkbox is checked
    // Note: Checkbox value is typically '1' when checked
    ->showIf('company_name', '[is_company] == 1')
    ->showIf('vat_number', '[is_company] == 1')
    ->showIf('registration_number', '[is_company] == 1')
    ->render();

3. Multiple Conditions for Same Field

// Show different fields for different payment methods
$form = \Builders\FormBuilder::create($this->model, $this->page)
    // Show card fields when payment method is 'credit_card'
    ->showIf('card_number', '[payment_method] == "credit_card"')
    ->showIf('card_expiry', '[payment_method] == "credit_card"')
    ->showIf('card_cvv', '[payment_method] == "credit_card"')

    // Show bank fields when payment method is 'bank_transfer'
    ->showIf('bank_name', '[payment_method] == "bank_transfer"')
    ->showIf('account_number', '[payment_method] == "bank_transfer"')
    ->showIf('swift_code', '[payment_method] == "bank_transfer"')

    // Show PayPal email when payment method is 'paypal'
    ->showIf('paypal_email', '[payment_method] == "paypal"')
    ->render();

4. User Type Based Fields

$form = \Builders\FormBuilder::create($this->model, $this->page)
    // Show admin-specific fields
    ->showIf('admin_level', '[user_type] == "admin"')
    ->showIf('permissions', '[user_type] == "admin"')
    ->showIf('department', '[user_type] == "admin"')

    // Show customer-specific fields
    ->showIf('customer_type', '[user_type] == "customer"')
    ->showIf('discount_level', '[user_type] == "customer"')
    ->showIf('credit_limit', '[user_type] == "customer"')

    // Show vendor-specific fields
    ->showIf('vendor_category', '[user_type] == "vendor"')
    ->showIf('commission_rate', '[user_type] == "vendor"')
    ->showIf('contract_date', '[user_type] == "vendor"')
    ->render();

Complete Working Example

E-commerce Product Form

namespace Modules;

use App\Abstracts\{AbstractModule, AbstractModel};
use App\Attributes\RequestAction;
use App\Response;

class ProductModule extends AbstractModule {

    protected function configure($rule): void
    {
        $rule->page('products')
             ->title('Product Management')
             ->menu('Products')
             ->access('registered');
    }

    #[RequestAction('edit')]
    public function edit() {
        $id = _absint($_REQUEST['id'] ?? 0);
        $product = $this->model->getByIdForEdit($id);

        $form = \Builders\FormBuilder::create($this->model, $this->page)
            ->addFieldsFromObject($product, 'edit')

            // Show digital product fields when type is 'digital'
            ->showIf('download_url', '[product_type] == "digital"')
            ->showIf('file_size', '[product_type] == "digital"')
            ->showIf('download_limit', '[product_type] == "digital"')

            // Show physical product fields when type is 'physical'
            ->showIf('weight', '[product_type] == "physical"')
            ->showIf('dimensions', '[product_type] == "physical"')
            ->showIf('shipping_class', '[product_type] == "physical"')

            // Show subscription fields when type is 'subscription'
            ->showIf('billing_period', '[product_type] == "subscription"')
            ->showIf('trial_days', '[product_type] == "subscription"')
            ->showIf('renewal_price', '[product_type] == "subscription"')

            // Show discount fields only when 'has_discount' is checked
            ->showIf('discount_percentage', '[has_discount] == 1')
            ->showIf('discount_start_date', '[has_discount] == 1')
            ->showIf('discount_end_date', '[has_discount] == 1')

            ->addStandardActions('?page=' . $this->page, true)
            ->render();

        Response::render(['form' => $form], [
            'title' => $id > 0 ? 'Edit Product' : 'Add Product',
            'form' => $form
        ]);
    }
}

class ProductModel extends AbstractModel {
    protected function configure($rule): void
    {
        $rule
        ->table('products')
        ->id()
        ->string('name', 200)
        ->text('description', false)
        ->float('price')

        // Product type selector
        ->string('product_type', 50)->options([
            'physical' => 'Physical Product',
            'digital' => 'Digital Product',
            'subscription' => 'Subscription'
        ])->formType('list')

        // Physical product fields
        ->float('weight', false)
        ->string('dimensions', 100, false)
        ->string('shipping_class', 50, false)

        // Digital product fields
        ->string('download_url', 255, false)
        ->string('file_size', 50, false)
        ->int('download_limit', false)

        // Subscription fields
        ->string('billing_period', 50, false)
        ->int('trial_days', false)
        ->float('renewal_price', false)

        // Discount checkbox and fields
        ->checkbox('has_discount', false)
        ->float('discount_percentage', false)
        ->date('discount_start_date', false)
        ->date('discount_end_date', false);
    }
}

Best Practices

1. Field Order Matters

Place the control field (the field being watched) before the conditional fields in your form for better UX.

// Good - control field comes first
->fieldOrder(['id', 'status', 'activation_date', 'activated_by'])

// Not recommended - conditional fields appear before control
->fieldOrder(['id', 'activation_date', 'activated_by', 'status'])

2. Make Conditional Fields Optional

Fields that are conditionally shown should typically be optional in the database schema.

// Good - conditional fields are optional (false parameter)
->date('activation_date', false)  // Second parameter = false means nullable
->string('activated_by', 100, false)

// Avoid - conditional required fields can cause validation issues
->date('activation_date')  // Required field that might be hidden

3. Group Related Conditional Fields

Use showIf() on a container id to show/hide a full section at once.

// Good - grouped related fields via container
->addContainer('CNT_ACTIVATION', ['activation_date', 'activated_by', 'activation_notes'], 3, '', 'Activation')
->showIf('CNT_ACTIVATION', '[status] == "active"')

4. Clear Field Labels

Make field labels descriptive so users understand why fields appear/disappear.

// Good - descriptive labels
->string('activation_date')->formLabel('Date when user was activated')
->string('reason_inactive')->formLabel('Reason for deactivation')

// Less clear
->string('activation_date')->formLabel('Date')
->string('reason_inactive')->formLabel('Reason')

Removing Conditional Visibility

If you need to remove conditional visibility from a field:

$form = \Builders\FormBuilder::create($this->model, $this->page)
    ->showIf('activation_date', '[status] == "active"')

    // Later, remove the condition
    ->removeFieldCondition('activation_date')

    ->render();

Technical Details

JavaScript Implementation

The conditional visibility is handled by the MilkForm JavaScript class in milk-form.js. It:

  • Automatically detects elements with data-milk-show attributes
  • Evaluates expressions on each recalculation
  • Shows/hides fields with smooth transitions
  • Manages required field validation when fields are hidden

Browser Compatibility

The conditional visibility feature works in all modern browsers that support:

  • ES6 JavaScript classes
  • CSS transitions
  • DOM dataset API

Troubleshooting

Fields Not Showing/Hiding

  1. Verify field names inside the expression match the real form names (case-sensitive)
  2. Check that the expression evaluates to true/false as expected
  3. Ensure JavaScript is loaded (check browser console)
  4. Verify field is added to form before applying conditional visibility

Validation Issues

If required fields are causing validation errors when hidden:

  • Make conditional fields optional in the model (use false parameter)
  • The JavaScript automatically disables required validation for hidden fields
  • Ensure you're using the latest version of milk-form.js

Next Steps

Now that you understand conditional field visibility, explore:

  • Form Validation: Custom validation rules and error handling
  • Complex Field Types: Working with milkSelect, file uploads, and editors
  • Form Actions: Custom callbacks and action handling
  • AJAX Forms: Dynamic form submission without page reload
Loading...