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.

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

My php page had a line like,

<?php
  shell_exec
('wget <a href="http://www.example.com/path/to/something'">http://www.example.com/path/to/something'</a>);
?>

The actual parameters of wget command were more complicated. There were two things I had to resolve. wget command does not come with OS X by default, so you needed to install it first. One of easy ways to install wget is through homebrew, which I already had. I simply ran,

$ brew install wget

Ron explained how to install homebrew.

But then I figured that apache running the php page could not find wget command, which is located in /usr/local/bin/wget. I use MAMP and I had to tell MAMP where to look for wget. I opened up text file, /Applications/MAMP/Library/bin/envvars and added the following line in it,

export PATH=$PATH:/usr/local/bin

Stop and start Apache Server of MAMP and the problem was solved.