4

How can I call a JavaScript function after a file has completed uploading using the Drupal Form API (FAPI) managed_file element?

5

This is tricky in D8, as the ajax code does not offer any hooks or callbacks after the function has run. The solution is to override the ajax callback function that the managed_file element uses, adding an additional custom command that is triggered when the file upload is complete.

First, we create a custom AJAX command, that will be used to trigger the code we want to execute. Details on creating custom ajax commands can be found here.

First, the Ajax command on the PHP side. This will go into [MODULE]/src/Ajax:

<?php

namespace Drupal\[MODULE]\Ajax;

use Drupal\Core\Ajax\CommandInterface;

/**
 * Command to trigger an event when managed file upload is complete.
 */
class ManagedFileUploadCompleteEventCommand implements CommandInterface {

  /**
   * Implements Drupal\Core\Ajax\CommandInterface:render().
   */
  public function render() {
    return [
      'command' => 'triggerManagedFileUploadComplete',
    ];
  }

}

Next, the triggerManagedFileUploadComplete() command needs to be created on the JavaScript side. This is done in [MODULE]/js/managed_file_upload_complete_event_command.js.

(function (Drupal) {

  "use strict";

  /**
   * Add new custom command.
   */
  Drupal.AjaxCommands.prototype.triggerManagedFileUploadComplete = function () {
    // Do stuff here after file upload is complete.
    alert(Drupal.t("File upload complete!"));
  };

}(Drupal));

Now the new JavaScript file needs to be registered in MODULE.libraries.yml:

command.managed_file_upload_complete_event_command:
  js:
    js/managed_file_upload_complete_event_command.js: {}
  dependencies:
    - core/jquery
    - core/jquery.once

With this, the custom command can be attached using the library: [MODULE]/command.managed_file_upload_complete_event_command

Now that the custom command and it's related JS callback have been completed, the next thing is to add the new command onto the end of the ajax callback, so it is executed when the upload is complete.

The default ajax callback for managed_file elements is \Drupal\file\Element\ManagedFile::uploadAjaxCallback(). This needs to be overridden, adding the command created above to be run after all other ajax commands.

The #ajax property of the managed_file element is added in \Drupal\file\Element\ManagedFile::processManagedFile(). As such, if we want to alter the ajax, it has to happen after the above #process handler has been run. So we add another #process callback to be called after the default #process callback. In our process callback, we can change the #ajax callback to a function of our own. The #process callbacks are declared in \Drupal\file\Element\ManagedFile::getInfo(). Adding another #process handler can therefore be done in hook_element_info_alter().

/**
 * Implements hook_element_info_alter().
 */
function MODULE_element_info_alter(array &$info) {
  // Add a custom #process hook to the managed_file element:
  $info['managed_file']['#process'][] = 'MODULE_managed_file_process';
  // Add the custom command to managed_file elements, so that it is
  // available when called:
  $info['managed_file']['#attached']['library'][] = '[MODULE]/command.managed_file_upload_complete_event_command';
}

Now the command has been created and attached to all managed_file elements. The next step is to override the #ajax callback with our own, adding the custom command to it. Let's look at the overridden function (should be put in the .module file):

/**
 * Custom ajax callback for managed files.
 *
 * Overrides \Drupal\file\Element\ManagedFile\ManagedFile::uploadAjaxCallback()
 *
 * @see \Drupal\file\Element\ManagedFile\ManagedFile::uploadAjaxCallback
 */
function MODULE_managed_file_ajax_callback(array &$form, FormStateInterface $form_state) {
  // Retrieve the original response.
  $response = \Drupal\file\Element\ManagedFile\ManagedFile::uploadAjaxCallback($form, $form_state, \Drupal::request());

  // Add our own command to the end, so our command is run last:
  $response->addCommand(new \Drupal\[MODULE]\Ajax\ManagedFileUploadCompleteEventCommand());

  return $response;
}

The last thing to do is to replace the original ajax command with our own. This is done in the process callback we registered in hook_widget_info_alter():

/**
 * Custom process callback added to managed_file elements.
 *
 * Replaces the original #ajax callback with a custom one.
 */
function MODULE_managed_file_process(array &$element, FormStateInterface $form_state) {
  $element['upload_button']['#ajax']['callback'] = 'MODULE_managed_file_ajax_callback';

  return $element;
}

This tells Drupal to replace the original ajax callback with MODULE_managed_file_ajax_callback(). This callback retrieves the response from the original callback, and adds our custom command to the end of it. The custom command is called when the file has been uploaded, allowing us to run whatever JS we need on the server side after files have completed uploading.

| improve this answer | |
  • Thanks for your answer, this helped me a lot! I'm doing the same thing now with a FieldWidget that removes the need for the hook_managed_file_process and hook_element_info_alter. This gives you the ability to apply it per field, so it would not apply to all managed file upload fields. You can select the widget in the form display. If anyone is interested, I can upload my code example as extra answer. – FLY Jan 7 at 15:28
  • Please do. Add it as as separate reply, both so you can get credit, but also so that it provides an alternate answer. – Jaypan Jan 7 at 15:30
1

Based on @Jaypan answer and @FLY comment I made a FieldWidget. This indeed removed the need for the hook_managed_file_process and hook_element_info_alter. This also made it possible to use the widget on a per field basis.

First make your custom FieldWidget in [MODULE]/src/Plugin/Field/FieldWidget:

namespace Drupal\[MODULE]\Plugin\Field\FieldWidget;

use Drupal\Core\Form\FormStateInterface;
use Drupal\file\Plugin\Field\FieldWidget\FileWidget as CoreFileWidget;

/**
 * @FieldWidget(
 *   id = "[MODULE]_file_widget",
 *   label = @Translation("[MODULE] file upload widget"),
 *   field_types = {
 *     "file"
 *   }
 * )
 */
class FileWidget extends CoreFileWidget {

  /**
   * Override to replace the upload/file HTML control
   * with the [MODULE] form element.
   *
   */
  public static function process($element, FormStateInterface $form_state, $form) {

    $element = parent::process($element, $form_state, $form);

    if (!isset($element['upload'])) {
      return $element;
    }

    $element['upload_button']['#ajax']['callback'] = [get_called_class(), '[MODULE]ManagedFileAjaxCallback'];
    $element['upload_button']['#attached']['library'][] = '[MODULE]/[MODULE].managed_file_upload_complete_event_command';

    return $element;
  }

  /**
   * Custom ajax callback for managed files.
   *
   * Overrides \Drupal\file\Element\ManagedFile\ManagedFile::uploadAjaxCallback()
   *
   * @see \Drupal\file\Element\ManagedFile\ManagedFile::uploadAjaxCallback
   */
  public function [MODULE]ManagedFileAjaxCallback(array &$form, FormStateInterface $form_state) {
    // Retrieve the original response.
    $response = \Drupal\file\Element\ManagedFile::uploadAjaxCallback($form, $form_state, \Drupal::request());

    // Add our own command to the end, so our command is run last:
    $response->addCommand(new \Drupal\[MODULE]\Ajax\ManagedFileUploadCompleteEventCommand($form, $form_state));

    return $response;
  }
}

Then as Jaypan already wrote create a file in [MODULE]/src/Ajax for your custom Ajax command (Give the file the name of your class with the php extension):

namespace Drupal\[MODULE]\Ajax;

use Drupal\Core\Ajax\CommandInterface;

/**
 * Command to trigger an event when managed file upload is complete.
 */
class ManagedFileUploadCompleteEventCommand implements CommandInterface {

  /**
   * Implements Drupal\Core\Ajax\CommandInterface:render().
   */
  public function render() {
    return [
      'command' => 'triggerManagedFileUploadComplete',
    ];
  }

}

Create your javascript file that's being triggered by the command in [MODULE]/js/managed_file_upload_complete_event_command.js:

(function (Drupal) {

  "use strict";

  /**
   * Add new custom command.
   */
  Drupal.AjaxCommands.prototype.triggerManagedFileUploadComplete = function () {
    // Do stuff here after file upload is complete.
    alert(Drupal.t("File upload complete!"));
  };

}(Drupal));

Register your javascript file in [MODULE].libraries.yml:

[MODULE].managed_file_upload_complete_event_command:
  js:
    js/managed_file_upload_complete_event_command.js: {}
  dependencies:
    - core/jquery
    - core/jquery.once

You can also use $form and $form_state in your custom command to use this in your javascript if you need to.

I ended up using different values from the files being uploaded. My [MODULE]/src/Ajax/ManagedFileUploadCompleteEventCommand.php looks like this:

namespace Drupal\[MODULE]\Ajax;

use Drupal\Core\Ajax\CommandInterface;
use Drupal\file\Entity\File;

/**
 * Command to trigger an event when managed file upload is complete.
 */
class ManagedFileUploadCompleteEventCommand implements CommandInterface {
  // Constructs a ReadMessageCommand object.
  public function __construct($form) {
    $this->form = $form;
  }
  /**
   * Implements Drupal\Core\Ajax\CommandInterface:render().
   */
  public function render() {
    $files = array();
    $form_delta = $this->form['#file_upload_delta'];

    for($i=0;$i<$form_delta;$i++) {
      $file_id = array_keys($this->form[$i]['#files'])[0];
      $file = File::load($file_id);
      $uri = $file->getFileUri();
      $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager')->getViaUri($uri);
      $file_path = $stream_wrapper_manager->getExternalUrl();
      $file_name = $file->getFilename();

      $files[$i] = array(
        'file_id' => $file_id,
        'file_path' => $file_path,
        'file_name' => $file_name,
      );
    }

    return [
      'command' => 'triggerManagedFileUploadComplete',
      'files' => $files,
    ];
  }
}

You can use them in the javascript file you made in [MODULE]/js/managed_file_upload_complete_event_command.js like this:

(function (Drupal) {

  "use strict";

  /**
   * Add new custom command.
   */
  Drupal.AjaxCommands.prototype.triggerManagedFileUploadComplete = function (ajax, response, status) {
    var files = response.files;

    // Do stuff here after file upload is complete.
    console.log(files.file_id);
    console.log(files.file_path);
    console.log(files.file_name);
  };

}(Drupal));
| improve this answer | |

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.