How do I render a submit button so that it's disabled initially and enabled by JavaScript? We have a form that includes a React JS frontend component. The component collects some required information, which the React component then supplies to the backend by populating a hidden form field.

To ensure that a user cannot bypass entering the required data through the component, we need to satisfy the following requirements:


Before the React component loads, the "Submit" button needs to be disabled. This ensures that a user can't submit the form just by disabling JS or just clicking the button before the page finishes loading.
After the React component loads, any time that the required values get cleared, the "Submit" button needs to be disabled.
Once the required values are supplied, the "Submit" button should be enabled.
It must not be possible to submit the form if the hidden value is empty.


I've tried rendering the form like this:

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form = [];

    $form['selected_values'] = [
      '#type' =&gt; 'hidden',
    ];

    $form['actions'] = [
      '#type'   =&gt; 'actions',
      '#weight' =&gt; 100,
    ];

    $form['actions']['submit'] = [
      '#type'         =&gt; 'submit',
      '#value'        =&gt; $this-&gt;t('Submit'),
      '#button_type'  =&gt; 'primary',
      '#disabled'     =&gt; TRUE, // Controlled by JS on the frontend
    ];

    return $form;
  }


And then in JavaScript, I have an onValueChange() event callback that looks like this:

function onValueChange(value) {
  if (value.trim() === '') {
    $(submitButton).prop('disabled', true);
  }
  else {
    $(submitButton)
      .prop('disabled', false)
      .removeClass('is-disabled');
  }
}


I also tried:

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form = [];

    $selected_values = form_state-&gt;getValue('selected_values');

    $form['selected_values'] = [
      '#type' =&gt; 'hidden',
    ];

    $form['actions'] = [
      '#type'   =&gt; 'actions',
      '#weight' =&gt; 100,
    ];

    $form['actions']['submit'] = [
      '#type'         =&gt; 'submit',
      '#value'        =&gt; $this-&gt;t('Submit'),
      '#button_type'  =&gt; 'primary',
      '#disabled'     =&gt; !empty($selected_values),
    ];

    return $form;
  }



Both of these approaches demonstrate the same problem -- clicking the "Submit" doesn't submit the form; it just seems to rebuild the form. With the second approach, it looks like the incoming selected values are always empty during the form render; they don't get applied to the form state until after the form is constructed.

How can we ensure that the "Submit" button starts out disabled, while requiring the necessary values?
Using #disabled on the submit button is the wrong solution because it communicates to the Drupal form API that the button is not expecting input. In other words, it causes Drupal to act as if the button were not there at all. Hence, clicking the submit button has no effect. This all makes sense from a security perspective, because normally you wouldn't want crafty users disabling backend logic just by using developer tools to force a disabled or read-only form element into being enabled.

Instead, the solution is two-fold:


Render the button as disabled via HTML attributes, so that the button is only disabled in the browser but is still considered an active button on the backend.
Mark the hidden field as '#required' =&gt; TRUE so that it's not possible to bypass the form even if the user is crafty with Dev Tools.


Here's what that looks like in practice:

  public function buildForm(array $form, FormStateInterface $form_state) {
    $form = [];

    $form['selected_values'] = [
      // Though this field is hidden, it's required. So, the title is still
      // shown if/when the field is ever NULL during submission.
      '#title'    =&gt; $this-&gt;t('File selection'),
      '#type'     =&gt; 'hidden',
      '#required' =&gt; TRUE,
    ];

    $form['actions'] = [
      '#type'   =&gt; 'actions',
      '#weight' =&gt; 100,
    ];

    $form['actions']['submit'] = [
      '#type'         =&gt; 'submit',
      '#value'        =&gt; $this-&gt;t('Submit'),
      '#button_type'  =&gt; 'primary',
      '#attributes' =&gt; [
        // This button is programmatically enabled/disabled via JS on the
        // frontend.
        'disabled' =&gt; 'disabled',
    ];

    return $form;
  }


The frontend logic for enabling the button stays the same. The difference here is that the button is only disabled in the rendered HTML rather than being disabled in the backend form. In addition, if a crafty user enables the button by removing the disabled attribute on the HTML DOM element, they'll just end up with a validation error that says:


  File selection field is required.


Separately, as for why the second approach failed to enable/disable the "Submit" button based on the incoming values, this is because Drupal builds the form first before processing the incoming values so that it knows which form elements need to be invoked to process the input. So, at the time that the form builder is invoked, there is nothing yet in form state values. Hypothetically, it might be possible to get the raw input via $form_state-&gt;getUserInput() and then control whether or not the button should be enabled based on that; but, this approach may have security implications.