How to implement Ajax autosave in Drupal 8 forms

How to implement Ajax autosave in Drupal 8 forms

Jill Lasak's picture

Although Drupal 8 uses Garlic (a JS library) to cache the state of a partially completed form to the local storage of your browser, sometimes a more permanent type of storage is required. In this case, we needed to save a form field to the database immediately after it was filled out, not just on "submit". Using Drupal 8's form API, we can easily add Ajax autosave to fields in a form.

In Drupal 8, the Ajax callback must return HTML markup or a set of Ajax commands. Even though these don't really serve a purpose for our autosave, I chose to return HTML markup in order to avoid an error. The 'ajax_response' form element, and the 'wrapper' references below create a place where the markup is returned.

In my example below, I'm adding autosave to a node field, but you could easily modify this code to make it apply to another entity or a custom form.

Step one: Add the Ajax widget to the field

In Drupal 8, Ajax widgets can be added almost the same way as in Drupal 7.

Via a hook_form_alter:

/**
 * Implements hook_form_alter().
 */

function your_module_form_[form_id]_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  $form['sample_field_name']['widget']['#ajax'] = array(
    'callback' => 'Drupal\your_module\Form\FormAlter::autosave',
    'event' => 'change',
    'wrapper' => 'ajax_placeholder',
    'progress' => array(
      'type' => 'throbber',
      'message' => NULL,
    ),
  );

  // Add placeholder for Ajax response markup
  $form['ajax_response'] = array(
    '#type' => 'html_tag',
    '#tag' => 'div',
    '#value' => t('Placeholder for ajax response'),
    '#attributes' => array(
      'class' => array('hidden'),
      'id' => array('ajax_placeholder'),
    ),
  );
}

or if you are creating the form from scratch, add it to the form element definition:

$form['field_name'] = array(
  '#type' => 'radios',
  '#title' => $this->t('Sample field title'),
  '#default_value' => 1,
  '#options' => array(0 => $this->t('Option 1'), 1 => $this->t('Option 2')),
  '#ajax' => array(
    'callback' => 'Drupal\your_module\Form\CustomForm::autosave',
    'event' => 'change',
    'wrapper' => 'ajax_placeholder',
    'progress' => array(
      'type' => 'throbber',
      'message' => NULL,
    ),
  ),
);

// Add placeholder for Ajax response markup
$form['ajax_response'] = array(
  '#type' => 'html_tag',
  '#tag' => 'div',
  '#value' => t('Placeholder for ajax response'),
  '#attributes' => array(
    'class' => array('hidden'),
    'id' => array('ajax_placeholder'),
  ),
);

If you are implementing via a hook_form_alter, the placement of the Ajax widget may change depending on the type of field. The above will work for radio buttons and select boxes. For textareas, you may need to add it at

$value['widget'][0]['value']['#ajax']

rather than

$value['widget']['#ajax']

Step two: Add the callback function to autosave

Here we are getting the node object and the changed form element from the $form_state variable, and setting the new value of the changed element to the node before saving. We then return a response to the form.

Make sure the class name below matches the callback you wrote in the previous step.

namespace Drupal\your_module\Form;


use Drupal\node\Entity\Node;


class FormAlter {

  public static function autosave(array &$form, \Drupal\Core\Form\FormStateInterface $form_state) {

    // Load the node and get changed form element
    $node = $form_state->getFormObject()->getEntity();
    $triggering_element = $form_state->getTriggeringElement();

    // Set and save node with new value for field
    $node->set($triggering_element, $triggering_element['#value']);
    $node->save();

    // Add an Ajax response to avoid error 
    $response = [
      '#markup' => '<div class="hidden">Saved</div>',
    ];
    return $response;

  }

}

If you need to autosave more than one field on the form, you can replace

$node = $form_state->getFormObject()->getEntity();

with

$node_id = $form_state->getFormObject()->getEntity()->id();
$node = node_load($node_id); 

so the newly autosaved node is reloaded at each call.


With this, you should be able to create autosave in a node, or with slight modifications, in another entity. Good luck!

Reference:
https://api.drupal.org/api/drupal/core!core.api.php/group/ajax/8.2.x

Comments

I have created a media bundle and added dependent field through code as mentioned above.
But when I try to load the media bundle in a Content Type using Entity Reference field through Inline Entity Form module, it doesn't work.
Do you have any idea how we can achieve this?

Jill Lasak's picture

Hi Dinesh,

I don't have experience with media bundles in D8 or Inline Entity Form module yet, so I can't share any direct experience. You could try to play around with the placement of the ajax widget. I gave these 2 examples in the article, but I've only tested with simple textarea and radio button fields:

$value['widget'][0]['value']['#ajax']
$value['widget']['#ajax']

I imagine the placement may be different in your case.

Great post! I want the user to tick the checkbox (a boolean field on the node form), which will trigger an ajax change to the #default_value of the field's widget. I tried your sample code, but I'm not getting a response from the callback. I don't see

$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand(
...
));
return $response;

in your sample code like what's in the AJAX API page (https://api.drupal.org/api/drupal/core!core.api.php/group/ajax/8.2.x). I'm wondering if that's my problem.

Jill Lasak's picture

Hi Eric,

It's probably too late to help you, but in case someone else reads this: I think your issue has to do with the boolean field more than needing to return an AjaxCommand.
If you were able to solve and have some time, please share your solution!

Thanks,
Jill

Great tutorial Jill,

Thanks for the help.

Do you know how one might set the correct change to a checkbox prior to saving? I have the module setup and working correctly aside from setting the checkbox before the save. I have played around with removing/xdebugging each line to determine why I'm getting a 500 ajax post error when I leave it as $node->set($triggering_element, $triggering_element['#value']);

Is there something besides value that I should be changing for a checkbox. I inspected the element closely and don't see a control for checked vs unchecked. I think that the error is coming from trying to set the value to itself.

Jill Lasak's picture

Hi Shawn,

Hm, I would think changing value for a boolean field would be enough.

I haven't tried with boolean values myself, but can you debug to find the value of $triggering_element['#value'] when it's a boolean?
If it's equal to 1 or true, I would think it would work in the line $node->set($triggering_element, $triggering_element['#value'].

If you've already solved your issue, feel free to share your solution here!

Thanks,
Jill

Post new comment

About Urban Insight

We create elegant, mobile-friendly websites.

We solve complex problems using Drupal and open source software.

Learn More

Snippet

^([1-2]?[0-9]{1,2}\.){3}[1-2]?[0-9]{1,2}$

This regex will match IPv4 IP addresses (e.g. 192.168.1.125). It might be possible to improve it to only match octets up to 255.