Tuesday, January 24, 2017

Form API #states: dependent fields

Drupal provides a powerfull API for building different kind of forms. One of the most cool thing in this API, I think, it's a #states feature. It allows developers to create form elements that can change their state depending on some conditions. For instance you can create a text input which will be visible only if checkbox is checked. Or even make multiple conditions for an element.

Using this feature you can create OR, AND and XOR conditions. Let's consider an example. We have a form with two checkboxes and one text input:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
  ];

  return $form;
}
Let's say we want to show text_input_1 only if check_box_1 is checked. This example shows a SIMPLE CONDITION:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    // #states array describes element states depends on conditions.
    '#states' => [
      // State: this input will be visible only if checkbox_1 is checked.
      'visible' => [
        // Condition: selector of a checkbox this input depends on.
        'input[name="checkbox_1"]' => [
          // Condition value.
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  return $form;
}
A complete list of existing states and condition values you can find here. Next example is AND CONDITION: let's show text_input_1 only if checkbox_1 AND checkbox_2 are checked:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        // Just provide a few conditions in one state array.
        'input[name="checkbox_1"]' => [
          'checked' => TRUE,
        ],
        'input[name="checkbox_2"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  return $form;
}
OR CONDITION. In this case we will show text_input_1 only if checkbox_1 OR checkbox_2 is checked:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        // OR condition: provide a few conditions each wrapped into a
        // state array.
        [
          'input[name="checkbox_1"]' => [
            'checked' => TRUE,
          ],
        ],
        [
          'input[name="checkbox_2"]' => [
            'checked' => TRUE,
          ],
        ]
      ],
    ],
  ];

  return $form;
}
And a final example: XOR CONDITION. Show text_input_1 only if checkbox_1 OR checkbox_2 is checked but not both:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  $form['checkbox_2'] = [
    '#title' => t('Checkbox 2'),
    '#type' => 'checkbox',
  ];

  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'visible' => [
        [
          'input[name="checkbox_1"]' => [
            'checked' => TRUE,
          ],
        ],
        'xor',
        [
          'input[name="checkbox_2"]' => [
            'checked' => TRUE,
          ],
        ]
      ],
    ],
  ];

  return $form;
}
Of course you can combine conditions and make them really complex.

Be careful with 'required' state


If you make field required depends on a condition you have to a validate this case on a back-end side manually:
<?php

/**
 * Form implementation.
 */
function module_form($form, $form_state) {
  $form['checkbox_1'] = [
    '#title' => t('Checkbox 1'),
    '#type' => 'checkbox',
  ];

  // If checkbox is checked then text input
  // is required (with a red star in title).
  $form['text_input_1'] = [
    '#title' => t('Text input 1'),
    '#type' => 'textfield',
    '#states' => [
      'required' => [
        'input[name="checkbox_1"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];

  $form['actions'] = [
    'submit' => [
      '#type' => 'submit',
      '#value' => t('Submit'),
    ],
  ];

  return $form;
}

/**
 * Form validate callback.
 */
function module_form_validate($form, $form_state) {
  // if checkbox is checked and text input is empty then show validation
  // fail message.
  if (!empty($form_state['values']['checkbox_1']) &&
    empty($form_state['values']['text_input_1'])
  ) {
    form_error($form['text_input_1'], t('@name field is required.', [
      '@name' => $form['text_input_1']['#title'],
    ]));
  }
}

Key notes:

2 comments:

  1. These conditions remind me conditions from mongodb queries, but also they look like hell. Is difference between AND and OR only in inner arrays? But if you want to use XOR you have to use 'xor' array element between two conditions. Really?

    Let's look at mongodb conditions.
    {
    $and : [
    { $or : [ { price : 0.99 }, { price : 1.99 } ] },
    { $or : [ { sale : true }, { qty : { $lt : 20 } } ] }
    ]
    }

    I guess it looks more readable =)

    ReplyDelete
    Replies
    1. "Is difference between AND and OR only in inner arrays?"
      Yes

      "But if you want to use XOR you have to use 'xor' array element between two conditions. Really?"
      Yes

      Delete