I'm trying to add an Ajax submit to a regular node edit form (Drupal 9). I'm using Field Groups so that there's a series of tabs, and I've added a pair of "buttons" (Right now they're just markup <div>s, but I could change them to real buttons if that's recommended) that users can use to move forward and back through the tabs with some Javascript.

The ask now is that each time the tab focus changes, the form is submitted via Ajax. All the examples I'm finding seem to be doing something other than what I'm after. I've seen examples using completely custom forms, loading forms, and altering fields, but nothing that just adds the ability to submit a a regular node form with Ajax.

Things I've tried:

  • Adding an #ajax subarray of various sorts to the buttons and/or the regular submit element. Nothing Ajaxy at all has happened in any of these experiments
  • Using Javascript to trigger a click event on the submit, without and without an #ajax subarray. All I get is a regular submit with page refresh
  • Bypassing all of that just calling $.post($('#form-id').attr('action'), $('#form-id').serialize()). There's some semblance of Ajax submit going on as my custom hook_form_alter gets hit, but the node isn't saved/updated. I think that's failing because there's no proper submit/op value associated with it, and I haven't been able to figure out how to add/set it.

Can someone point me in the right direction?

2 Answers 2


after reading a tone of documentations and examples of other Drupal developers, I figured out a way which worked for me very good. But this may not a best practice way, after all ;-)

Here is what I've done, to add a new button to a node content type edit form:

how to build an async node save button

add new button to a content type

In your module add hook form_alter to you my_module.module file to add the new button to your content type edit form:

   * Implements hook_form_alter().
  function emp_save_edit_form_alter(&$form, FormStateInterface $form_state): void {
    $formObject = $form_state->getFormObject();
    $entity = $formObject->getEntity();
    $contentType = $entity->getType();

    if ($contentType === "YOUR_CONTENT_TYPE") {

      $form['actions'][] = [
        '#type' => 'submit',
        '#value' => 'Async save',
        // this is required to have the entity stored properly
        '#submit' => [
        // this enables the button to used the async way
        '#ajax' => [
          'callback' => 'formSubmitAjax',
          /*if you want to to use a progress bar instead of the spinning clock, the use this:
          'progress' => [
            'type'=> 'bar',
            'message' => 'saving...'
        '#weight' => 1,


This will a submit button named "Async save" to your edit form which should do the following:

  • call the function submitForm and save of the node entity form (or any other entity form, if you like)
  • call the function formSubmitAjax Both are executed with ajax. The edit form should not be reloaded.

add function formSubmitAjax

Then also add the still missing function formSubmitAjax to your my_module.module file:

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
   * Submit form with ajax - add messages to be shown after ajax request is processed
  function formSubmitAjax(&$form, FormStateInterface $form_state): AjaxResponse
    $response = new AjaxResponse();
    // ToDo add redirect when form is an "add-form"

    // if functions "submitForm" or "save" will have errors, these should be collected in $form_state->errors
    if ($errors = $form_state->getErrors()) {
      foreach ($errors as $error) {
        $errorHtml = strtr('<div class="messages messages--error">$errorText</div>', ['$errorText' => $error->render()]);
        // show message as a drupal Dialog Modal
        $response->addCommand(new OpenModalDialogCommand('Error', $errorHtml));
    } else {
      $date = new DateTime();
      // add a message to show things worked out - with current timestamp
      $successMsg = 'Your changes have been saved at ' . $date->format('Y-m-d H:i:s');
      $successHtml = strtr('<div class="messages messages--status">$statusText</div>', ['$statusText' => $successMsg]);
      $response->addCommand(new OpenModalDialogCommand('Saved', $successHtml));
    $response->addCommand(new ReplaceCommand(NULL, $form));
    // the changed time of the form has to be changed after a form is submitted to prevent the error "this page as already saved by another user" or so
    $response->addCommand(new InvokeCommand('input[name="changed"]','val',[time()]));
    // prevent messages to be shown after reloading the edit form, all recent messages have to be deleted
    return $response;

Te be sure this code find it's way into Drupal, rebuild your cache:

drush cr

The first point is the only straightforward way. A regular node edit form has already Ajax form elements. You can check the "ajax" subarray of drupal-settings-json delivered to the browser.

You should see your own Ajax callback here as well:

  "ajax": {
    "edit-field-article-add-more": {
      "callback": [
      "wrapper": "field-article-add-more-wrapper",
      "effect": "fade",
      "event": "mousedown",
      "keypress": true,
      "prevent": "click",
      "url": "\/node\/123\/edit?destination=\/admin\/content\u0026ajax_form=1",
      "dialogType": "ajax",
      "submit": {
        "_triggering_element_name": "field_article_add_more",
        "_triggering_element_value": "Add another item"
    "edit-field-paragraph-add-more-add-more-button-text": {
      "callback": [

If not, then you have to debug server-side PHP. If yes, then client-side Javascript.

  • There's script tag in the page source with data-drupal-selector="drupal-settings-json". Browser just tenaciously cached an experiment where I tried adding an #ajax subarray to an arbitrary item. Otherwise, there's no evidence of any ajax associated with the node form either there, nor when I analyse the $form array w/ xdebug in my hook_form_alter.
    – beltouche
    Jul 28, 2021 at 13:51
  • I now have four questions: 1) Am I looking in the right spot? 2) Do I just add an #ajax subarray to the regular submit button? 3) What's the callback? Is '::submitForm' sufficient or do I need to write a custom function that somehow invokes the whole '#submit' array of functions? 4) How do I trigger the Ajax instead of the regular submit?
    – beltouche
    Jul 28, 2021 at 13:52
  • As said, point 1 (#ajax) is the right spot. Add an entity reference autocomplete widget to the node form and you'll see the ajax add more button from the json output above.
    – 4uk4
    Jul 28, 2021 at 13:57
  • I accept that point #1 is the way to go. The "Am I looking in the right spot?" was referring to where/how I found the "drupal-settings-json" you mentioned.
    – beltouche
    Jul 28, 2021 at 14:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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