4

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?

3
  • 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 Commented Jan 30, 2019 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. Commented Jan 30, 2019 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. Commented Jan 31, 2019 at 16:04

1 Answer 1

3

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:

use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; $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); 

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.