How do I render a submit button so that it's disabled initially and enabled by JavaScript?
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.