1

I've got a form that builds an array of data. On submission, it updates a table displayed in the browser, using AJAX; that works fine.

I want a second submit button that downloads the same data as a CSV file. I've tried a number of variations, but I can't seem to both send the file response and satisfy the AJAX requirement for a returned array.

The button:

$form['download'] = [
  '#type' => 'submit',
  '#value' => $this->t('Download data'),
  '#weight' => '40',
  '#ajax' => [
    'callback' => '::downloadCallback',
    'event' => 'click',
  ],
];

...and the response section of the callback:

$file_content = $serializer->serialize($data, 'csv');
$response = new Response($file_content);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'data.csv');
$response->headers->set('Content-Disposition', $disposition);
$form_state->setResponse($response);

I've also tried using this instead of setResponse:

$response->send();

These all result in an error:

TypeError: Argument 1 passed to Drupal\Core\Render\MainContent\AjaxRenderer::renderResponse() must be of the type array, null given, called in /mnt/www/html/mowebd8dev/docroot/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php on line 89 in /mnt/www/html/mowebd8dev/docroot/core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php on line 45 #0 /mnt/www/html/mowebd8dev/docroot/core/lib/Drupal/Core/Form/FormAjaxResponseBuilder.php(89): Drupal\Core\Render\MainContent\AjaxRenderer->renderResponse(NULL, Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\CurrentRouteMatch))

I've tried also returning a simple render array with some dummy markup, and that prevents the error, but still doesn't result in a file download. Is there a way to accomplish this? Or should I just not be using AJAX for this purpose?

  • 2
    You can't download a file with AJAX directly (for security reasons). In my experience the way to solve it is to redirect to a URL that sets the content disposition header, which has the same effect because browsers don't change the URL when encountering such a response. So one approach would be to save the file instead of serving it directly, with a hashed filename, then return a RedirectCommand from the AJAX callback with the URL for a route you create that takes that hash as a parameter. In the controller, load up the tmp file that matches the hash, serve it, then delete it. Voila – Clive Jan 30 at 19:26
  • 1
    A cron job to clear up orphaned files after a period of time would also be wise if you go with that approach. – Clive Jan 30 at 19:28
  • I believe I was overthinking it. By creating a response and setting the form response to it, I get the desired effect without ajax -- the file downloads, but the form doesn't reload and the output region isn't changed. – WRD Jan 31 at 16:04
1

I was off-track in even trying to do this with AJAX, as Clive mentioned in his comment. Instead, I just made it a normal submit button, and then overrode the form action. The result is that the data is downloaded as a file, the form doesn't reload, and the data output section doesn't update, which is the desired behavior in this case.

The button, along with a new format element:

$form['download']['format'] = [
  '#type' => 'select',
  '#options' => $this->getValidFormats(),
  '#title' => $this->t('Select format'),
  '#weight' => '0',
];
$form['download']['download'] = [
  '#type' => 'submit',
  '#value' => $this->t('Download'),
  '#weight' => '10',
];

The submit function:

public function submitForm(array &$form, FormStateInterface $form_state) {
  switch ($form_state->getValue('op')->__toString()) {
    case 'Download':
      $this->download($form, $form_state);
      break;
  }
}

And the relevant part of the download function:

$response = new Response($file_content);
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'data.' . $format);
$response->headers->set('Content-type', $content_types[$format]);
$response->headers->set('Content-Disposition', $disposition);
$response->headers->set('Content-Transfer-Encoding', 'binary');
$response->headers->set('Content-length', strlen($file_content));
$form_state->setResponse($response);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.