Drupal 8 Ajax forgetting form changes - php

I have an issue with multiple AJAX requests modifying a form within drupal 8.
Let me explain - I have been trying to build a quiz module within drupal, and decided to use a widget to create a variable amount of quiz questions, within this question widget is a list of possible answers.
I have created an AJAX button within my question widget which allows answers to be removed and it works the first time it is submitted, but for some reason the second time I run the ajax call the form is reset (like no changes have been made, and no answers have been removed). I have debugged the form array after I have unset an answer, aswell as when the ajax callback is run the second time.
The ajax-enabled button uses the default ajax replace method and I have tried different methods of returning the form (minus an answer) within my AJAX callback including simply unsetting the selected form element and returning the form, aswell as using the AjaxResponse and HtmlCommand classes. I have also tried to rebuild both the form, via formbuilder, and form_state with no joy.
Also each button and answer have unique names/id's.
Here is my widget code (including the button definition):
<?php
/**
* #file
* Contains \Drupal\pp_quiz\Plugin\Field\FieldWidget\QuestionWidget.
*/
namespace Drupal\pp_quiz\Plugin\Field\FieldWidget;
use Drupal\pp_quiz\Controller\QuizController;
use Drupal\pp_quiz\Ajax\AjaxHandler;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'question_widget' widget
*
* #FieldWidget(
* id = "question_widget",
* label = #Translation("Quiz question widget"),
* field_types = {
* "quizquestion_type",
* },
* )
*/
class QuestionWidget extends WidgetBase
{
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$ajaxhandler = new AjaxHandler();
$input = $form_state->getUserInput();
//grab answer count from element array (using delta index)
$question = $items->get($delta)->getValue();
$answercount = count(unserialize($question['answer']));
$element['question'] = array(
'#type'=>'text_format',
'#format' => 'normal',
'#title' => gettext('Question'),
'#description' => gettext("The Question Text"),
'#required' => TRUE,
'#element_validate' => array(
array(
'\Drupal\pp_quiz\Controller\QuizController',
'validateQuestion'
),
),
);
$element['answers_description'] = array('#markup' => 'Create answers below and select which are correct by checking boxes');
$tableheader = array(
'answer' => array('data' => t('Answer'), 'field' => 'answer'),
);
for ($i=0; $i<$answercount; $i++) {
$name = "{$delta}answer{$i}";
$options[$name] = array(
'answer' => array(
'data' => array(
'#type'=>'textfield',
//fix for losing answers on addmore button
'#value'=>isset($input[$name]) ? $input[$name] : '',
'#name' => $name,
'#required' => TRUE,
),
),
);
}
$element['answers'] = array(
'#type'=>'tableselect',
'#header' => $tableheader,
'#options' => $options,
);
$element['removeanswer'] = array(
'#type' => 'submit',
'#value' => t('Remove Answer'),
'#name' => "{$delta}removeanswer",
'#questionno' => $delta,
'#attributes' => array(
'class' => array('removeanswer')
),
'#ajax' => array(
'callback' => array(
$ajaxhandler,
'removeAnswerAjax',
),
'wrapper' => 'edit-field-quiz-questions-wrapper',
),
);
$element = array(
'#type' => 'question',
) + $element;
return $element;
}
}
As you can see above, my 'removeanswer' submit button element has an ajax callback to a function called 'removeAnswerAjax' on the class 'AjaxHandler'. Below is the code for this callback:
<?php
/**
* #file
* Contains \Drupal\pp_quiz\Ajax\AjaxHandler.
*/
namespace Drupal\pp_quiz\Ajax;
use \Drupal\pp_quiz\Controller\QuizController;
use \Drupal\pp_quiz\Entities\QuizResults;
use \Drupal\Core\Form\FormStateInterface;
use \Drupal\Core\Ajax\AjaxResponse;
use \Drupal\Core\Ajax\HtmlCommand;
class AjaxHandler {
public function removeAnswerAjax(&$form, FormStateInterface $form_state) {
$questionno = $form_state->getTriggeringElement()['#questionno'];
$response = new AjaxResponse();
//find selected answer for question number (questionno)
foreach($form['field_quiz_questions']['widget'][$questionno]['answers']['#value'] as $answer_key=>$answer) {
unset($form['field_quiz_questions']['widget'][$questionno]['answers']['#options'][$answer]);
unset($form['field_quiz_questions']['widget'][$questionno]['answers']['#default_value'][$answer]);
unset($form['field_quiz_questions']['widget'][$questionno]['answers'][$answer]);
}
$response->addCommand(new HtmlCommand('#edit-field-quiz-questions-wrapper', $form['field_quiz_questions']['widget']));
$form_state->setRebuild();
return $response;
}
}
If anyone could shed any light on why the form is being reset before it is passed as an argument to my ajax callback, I would love you forever :-)
Thanks for reading.

One possible reason that your Ajax doesn't takes effect the second time is rebuilding of the form, in Ajax callback. As you are rebuilding the form using -
$form_state->setRebuild()
When a form is rebuilt, all the field and form ids are suffixed by a random build number. thus the selector is not found and the response is not replaced in DOM.
Try removing the form rebuild.

Related

Drupal form submit action changing at random times

I have created a custom Drupal module/form that when submitted the action takes you to another page and posts data with it. Pretty standard stuff.
Randomly, the form action changes at random (not set) intervals during the day that are not correlating to anything immediately obvious - for example cron runs. It only happens maybe once or twice a day, so if anyone has any idea what might cause this or point me in the right direction.
Anything anyone feels needs adding, let me know.
The is the custom module code:
/**
* Test form declaration
*/
function test_form($form, &$form_state){
$form['#attributes'] = array('id' => "test-form");
$form['search-field'] = array(
'#type' => 'textfield',
'#title' => t('<span class="highlighted">Test</span>'),
'#attributes' => array(
'class' => array('form-control form-text'),
),
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => 'Search',
'#attributes' => array(
'class' => array('btn btn-default text-hide'),
),
);
return $form;
}
/**
* search form submit
*/
function test_form_submit($form, &$form_state){
$searchQuery = $form_state['values']['search-field'];
$query = array();
if($search <> ''){
$query = array(
'field_geofield_distance[distance]' => '100',
'field_geofield_distance[unit]' => '3959',
'field_geofield_distance[origin]' => $search
);
}
drupal_goto('test-page', array('query' => $query));
}
If your form creates data (let's say a node) possibly you have a redirection on rules when creating a node that with specific values redirects to another page.
Look for rules that are activated when content creation occur or when submission occur.
Also you could have some redirection magic in the top of Drupal, like a wrong configuration of an .htacess file. If your form does the same thing always it should behave the same always. And why it's redirecting to page of old site?
Finally, if the new site have code from the old site the check submission, creation and form altering hooks.
Hope that helps.

Drupal 7 add ajax listener directly to node form

I'm trying to register an ajax listener to a checkbox on one of my standard node forms. I want to display certain regions of the form only in case, the field is checked. I can get the drupal ajax ecosystem to work, when I use it in a custom form, but I'm unable to make it work on my node forms.
First I hook into form_alter to check if I'm with the node type I want to add the listener for:
function interceptor_form_alter(&$form, &$form_state, $form_id) {
if($form_id === 'film_node_form') {
interceptor_dvd_listener($form, $form_state);
}
}
Then I try to attach the ajax stuff to my checkbox, which name is field_dvd:
function interceptor_dvd_listener(&$form, &$form_state) {
$form['field_dvd'] = array(
'#title' => t('You want to display DVD informations too?'),
'#type' => 'checkbox',
'#ajax' => array(
'callback' => 'interceptor_dvd_listener_callback',
'wrapper' => 'checkboxes-div',
'effect' => 'slide',
'progress' => array('type' => 'none'),
),
);
return $form;
}
The function interceptor_dvd_listener_callback is never called. I try to print some debug information there, but nothing happens...
function interceptor_dvd_listener_callback($form, $form_state) {
data_service_log_object($form);
}
UPDATE
After passing the form variable as reference to interceptor_dvd_listener the ajax callback worked like expected.
Most likely error:
You are not passing $form by reference to interceptor_dvd_listener() function. You are also not using the returned value from this function. Change the function definition to:
function interceptor_dvd_listener(&$form, &$form_state) {
$form['field_dvd'] = array(
'#title' => t('You want to display DVD informations too?'),
'#type' => 'checkbox',
'#ajax' => array(
'callback' => 'interceptor_dvd_listener_callback',
'wrapper' => 'checkboxes-div',
'effect' => 'slide',
'progress' => array('type' => 'none'),
),
);
}
Now see if it works. If it doesn't then, do the following:
Make sure that your custom module name is "interceptor".
Make sure that the id of the form you are modifying is "film_node_form".
Change the function interceptor_form_alter() to:
function interceptor_form_alter(&$form, &$form_state, $form_id) {
if($form_id == 'film_node_form') {
interceptor_dvd_listener($form, $form_state);
}
}
If it still doesn't work, print out $form variable at the end of interceptor_form_alter() function and see if field_dvd has the #ajax key.
You could also use this module (https://www.drupal.org/project/field-conditional-state) instead of writing all this custom code.
This interceptor_dvd_listener_callback() should return value, in documentation:
After form processing is complete, ajax_form_callback() calls the
function named by #ajax['callback'], which returns the form element
that has been updated and needs to be returned to the browser, or
alternatively, an array of custom Ajax commands.
Why don't you use this:
https://api.drupal.org/api/drupal/developer!topics!forms_api_reference.html/7#states
It is drupal 7 standard feature to hide or show some fields when some elements has some states.

drupal ajax callback on language selection

Is there way to bind an ajax callback when the language is changed, i want to update a nodereference-dropdown when a language is changed (to show the values only in that language).
following code is not working (form_alter) although other callbacks are working.
Can someone help me how can i achieve that?
$form['language']['#ajax'] = array(
'callback' => 'mymodule_something_language_callback',
'wrapper' => 'my-module-replace',
'#weight' => 2
);
Thanks.
FROM COMMENTS
heres the var_dump of $form['language'];
array
'#type' => string 'select' (length=6)
'#title' => string 'Language' (length=8)
'#default_value' => string 'und' (length=3)
'#options' =>
array
'und' => string 'Language neutral' (length=16)
'en' => string 'English' (length=7)
'ar' => string 'Arabic' (length=6)
The problem is that the Locale module alters this form element after hook_form_alter() is called (see this post).
Here's how I solved this problem:
First, change the order in which Drupal implements its hooks, putting 'form_alter' as the last one:
<?php
/**
* Implementation of hook_module_implements_alter()
*/
function chronos_module_implements_alter(&$implementations, $hook) {
if ($hook == 'form_alter') {
// Move mymodule_form_alter() to the end of the list. module_implements()
// iterates through $implementations with a foreach loop which PHP iterates
// in the order that the items were added, so to move an item to the end of
// the array, we remove it and then add it.
$group = $implementations['chronos'];
unset($implementations['chronos']);
$implementations['chronos'] = $group;
}
}
Next, add a form element and the '#ajax' element that you want on $form['language']:
<?php
/**
* Implements hook_form_alter().
*/
function mymodule_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'page_node_form') {
// alter the form
$form['container'] = array(
'#prefix' => '<div id="ajax-language">',
'#suffix' => '</div>',
);
$form['language']['#ajax'] = array(
'callback' => 'mymodule_save_language_callback',
'wrapper' => 'ajax-language'
);
return $form;
}
}
Lastly, add your callback:
<?php
/**
* Returns changed part of the form.
*
* #return renderable array
*
* #see ajax_example_form_node_form_alter()
*/
function chronos_save_language_callback($form, $form_state) {
# set session variables or perform other actions here, if applicable
return $form['container'];
}

Embedding an AHAH form element into a theme_table-generated table (Drupal)

I am trying to include a Drupal form element in a table I am generating with theme_table/theme('table',..). Specifically, I am trying to include a submit button with an AHAH attached.
Currently, I am just including as one cell in each row a call to drupal_render to render my dynamically generated AHAH element. The button renders fine, but without the AHAH attached.
So, my question is: is there a way to attach an AHAH to something that is just drupal_rendered?
If not, how else can I attach an AJAX/AHAH call to an element in a theme_table-generated table? I need to allow users to perform certain actions on rows of data in the table but need the page to not refresh.
TIA,
Benjy
I believe AHAH only works with drupal_get_form(). You'd have to write the AJAX yourself ( handbook page).
Maybe you could save the return value from drupal_get_form() in a variable and pass it to the theme function?
I think this is sort of a bug but i created a very easy generic workaround :) Solution by Januz did not work for me since the table was used inside a form already and also it's only applicable to very specific use cases and not a generic solution.
You can have a look at the following code and the function i wrote as an example
Notice : You may probably need to modify the code to suit your needs
/**
* #param $form
* #param $form_state
* #param $form_id
*/
function mymodule_form_alter(&$form, &$form_state, $form_id) {
$test_button = array(
'test_button' => array(
'#type' => 'button',
'#value' => t('Add'),
'#name' => 'test_button',
'#id' => 'test_button',
'#weight' => 2,
'#ajax' => array(
'callback' => '_mymodule_ajax_callback',
)
)
);
$form['mytable'] = array(
'#weight' => 1,
'#theme' => 'table',
'#header' => array(
array("data" => "Item"),
),
'#rows' => array(
'data' => array(
array(
'data' => $test_button
),
)
),
);
// This is a work around, ajax elements do not work when used in drupal tables
mymodule_table_ajax_workaround($test_button, $form, $form_state);
}
/**
* Ajax enabled form elements do not work then used inside a drupal table and rendered via theme_table
* This is a workaround to address the issue
* #param $elements
* #param $form
* #param $form_state
*/
function mymodule_table_ajax_workaround($elements, &$form, &$form_state) {
foreach ($elements as $element_name => $element_info) {
drupal_add_js(array('ajax' => array(
$element_name => array(
'callback' => $element_info['#ajax']['callback'],
'event' => 'mousedown',
'keypress' => true,
'prevent' => 'click',
'url' => '/system/ajax',
'submit' => array(
'_triggering_element_name' => $element_name . '_fake',
'_triggering_element_value' => $element_info['#value'],
)
),
)), 'setting');
$form['form_ajax_workaround'][$element_name] = array(
'#name' => $element_name . '_fake',
'#input' => true,
'#value' => $element_info['#value'],
'#ajax' => array(
'callback' => $element_info['#ajax']['callback']
)
);
}
}
You can do this via theme function and table generation. AHAH will work. The only problem is when you have multiple "tables", things start getting hairy.
Short explanation (correct me if I am wrong!): AJAX enabled form elements appear invisible to Drupal's Form API when they remain inside a "#theme" => "table" render array. This is probably due to render() using element_children() which does not look into a table's #header or #rows elements. A way to workaround this is to add dummy elements to the form which are visible to the Form API and bind AJAX events to the original elements to trigger the dummy elements.
I forked Sina Salek's great workaround for Drupal 7, main differences are:
Different form field keys, names and ID's are allowed,
The callback from the JS AJAX settings object is removed since it does not seem to exist in JS,
Added the base path variable to the AJAX URL for sites that are not installed in the web root,
Use "#type" => "value" for the dummy buttons instead of the internal options "#input" => TRUE,
The dummy element is available through the exact same name as the original element, without a "_fake" postfix, since the original elements are invisible to the Form API and the dummy elements do not appear in the DOM,
The full #ajax array is copied to the dummy element instead of just the callback element so other settings (ie. for the throbber) remain.
Allows the user to set custom JS AJAX options per button.
Example of implementing the workaround per AJAX enabled element when building a form:
/**
* My form.
*/
function MY_MODULE_my_form($form, &$form_state) {
// Define button. NB: An ID and name are required!
$my_button = array(
'#type' => 'button',
'#id' => 'test_button',
'#name' => 'test_button',
'#value' => t('Button label'),
'#ajax' => array('callback' => '_MY_MODULE_ajax_callback'),
);
// Call table AJAX workaround function on the button.
MY_MODULE_table_ajax_workaround($my_button, $form);
// Define table.
$form['my_table'] = array(
'#theme' => 'table',
'#header' => array(
array('data' => t('Column header')),
),
'#rows' => array(
'data' => array(
array('data' => $my_button),
)
),
);
return $form;
}
/**
* Workaround for AJAX enabled form elements that remain inside tables.
*
* Problem: AJAX enabled form elements appear invisible to Drupal's Form API
* when they remain inside a #theme => table render array. This is probably due
* to render() using element_children() which does not look into a table's
* #header or #rows elements.
*
* Workaround: Add dummy elements to the form which are visible to the Form API
* and bind AJAX events to the original elements to trigger the dummy elements.
*
* Based on:
* #link http://stackoverflow.com/questions/1981781
*
* Shared at:
* #link http://stackoverflow.com/a/31098784/328272
*
* Another workaround is the render elements using custom theme functions, but
* this seems slightly more complicated and rendered elements are no longer
* alterable.
* #link https://www.drupal.org/node/2101557#comment-7920151
*/
function MY_MODULE_table_ajax_workaround($element, &$form, $js_ajax_options = array()) {
// Add dummy element.
$form['table_ajax_workaround'][$element['#id']] = array(
'#type' => 'value',
'#name' => $element['#name'],
'#value' => $element['#value'],
'#ajax' => $element['#ajax'],
);
// Bind AJAX event to the original element, default properties are used. See
// Drupal.ajax in misc/ajax.js.
$js_setting['ajax'][$element['#id']] = drupal_array_merge_deep(array(
'event' => 'mousedown',
'keypress' => true, // Use lowercase booleans to support IE.
'prevent' => 'click',
'url' => base_path() . 'system/ajax',
'submit' => array(
'_triggering_element_name' => $element['#name'],
'_triggering_element_value' => $element['#value'],
),
), $js_ajax_options);
// Add JS setting.
drupal_add_js($js_setting, 'setting');
}

How to include drupal form elements in a data table

I have a data table which is populated with data unrelated to drupal content (from a third party system). The data relates to photos which must be approved / flagged as inappropriate.
So I'm writing a Drupal admin module which should moderate this content. So far, I have built a table using theme('table',...) which shows 1 photo per row, along with some other meta data. I now want to include some form buttons in the table and build some Ajax actions which are triggered by those buttons.
This article looks relevant http://drupal.org/node/112358 but I'm concerned this isn't the Drupal 6 way of doing things.
Can anyone offer some advice on how best to approach this problem - preferably using core modules / form override functions. Drupal version is 6.14.
Custom theme functions allow you to render content in a completely custom way. You can also create a custom template for your content, this could include the buttons.
hook_theme() will allow you to create your own content type to add to the theme function so you could have theme('moderatedimages').
A workaround would be to put the HTML for the moderate buttons into the table data so that it is output by theme table. This would save you having to write your own theme function.
For the AJAX calls you will need to build your own menu callback using hook_menu() and a custom function. (Code snippets are from this tutorial.)
<?php
function mymodule_products_menu() {
$items = array();
$items['products/get'] = array(
'title' => 'mymodule callback',
'page callback' => 'mymodule_myfunction',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK
);
return $items;
}
This will call the function mymodule_myfunction(). One thing to remember about this function is that you want to print the result and exit. You may want to use drupal_json() to encode the response.
Why are you using the standard theme_table() function? Just make your own theme function that includes the form elements you need. More on theme functions here and here.
I had the same problem. I found the solution in drupal-directory/modules/menu/menu.admin.inc. The Form-API and Themes will be used only!
Implementation:
-write your query with scheme-api (I think you did it already for your theme('table',...) )
-for each row create a form like this:
$form[$row->id]['column_name_1'] = array(...here description of the column -> checkbox, textfield...);
$form[$row->id]['column_name_2'] = array(...);
-write your own theme-funktion, witch use hook_theme_table (you habe already did it, you need only to change some cells into checkboxes or other form-elements.)
I write a submit-function now, but it doesn't work :-) For more information see the menu-modul.
Hier my code:
/**
* Form for editing the event types.
*
* #ingroup forms
*/
function mymodule_event_type_overview_form() {
$vid = variable_get('mymodule_category_vocabulary', '');
$sql = "SELECT term_data.tid AS tid,
term_data.name AS tname,
term_data.vid AS vid,
term_site_config.color AS color,
term_site_config.site_enabled AS site_enabled,
term_site_config.site_shown AS site_shown
FROM {term_data} term_data
INNER JOIN {term_site_config} term_site_config
ON term_data.tid = term_site_config.tid
WHERE term_data.vid = %d";
$result = db_query(db_rewrite_sql($sql), $vid);
$form = array(); while ($term = db_fetch_object($result)) {
$form[$term->tid]['tname'] = array(
'#type' => 'value',
'#value' => $term->tname,
);
$form[$term->tid]['color'] = array(
'#type' => 'value',
'#value' => $term->color,
);
$form[$term->tid]['enabled'] = array(
'#type' => 'checkbox',
'#default_value' => $term->site_enabled,
);
$form[$term->tid]['shown'] = array(
'#type' => 'checkbox',
'#default_value' => $term->site_shown,
);
// Build a list of operations.
$operations = array();
$operations['delete'] = l(t('delete'), 'admin/settings/mymodule/eventtype/'. $term->tid .'/delete');
$operations['edit'] = l(t('edit'), 'admin/settings/mymodule/eventtype/'. $term->tid .'/edit');
$form[$term->tid]['operations'] = array();
foreach ($operations as $op => $value) {
$form[$term->tid]['operations'][$op] = array('#value' => $value);
} }
if (element_children($form)) {
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Save configuration'),
);
$form['reset'] = array(
'#type' => 'submit',
'#value' => t('Reset to defaults'),
);
if (!empty($_POST) && form_get_errors()) {
drupal_set_message(t('The settings have not been saved because of the errors.'), 'error');
} }else {
$form['empty'] = array('#value' => t('There are no event types yet.')); }
return $form; }
/**
* Theme the event type overview form into a table.
*
* #ingroup themeable
*/
function theme_mymodule_event_type_overview_form($form) {
$header = array(
t('Event type'),
t('Color'),
array('data' => t('Enabled'), 'class' => 'checkbox'),
array('data' => t('Shown'), 'class' => 'checkbox'),
array('data' => t('Operations'), 'colspan' => '2'), );
$rows = array(); foreach (element_children($form) as $id) {
$element = &$form[$id];
if (isset($element['tname'])) {
$row = array(
t($element['tname']['#value']),
t($element['color']['#value']),
array('data' => drupal_render($element['enabled']), 'class' => 'checkbox'),
array('data' => drupal_render($element['shown']), 'class' => 'checkbox'),
array('data' => drupal_render($element['operations']['delete'])),
array('data' => drupal_render($element['operations']['edit'])),
);
$rows[] = $row; } } $output = ''; if ($rows) {
$output .= theme('table', $header, $rows); }
$output .= drupal_render($form);
return $output;
}
You will get the html-code of the form if you call drupal_get_form('mymodule_event_type_overview_form');
and don't forget co write following function in mymodule.module:
/**
* Implementation of hook_theme().
*/ function mymodule_theme() {
return array(
'mymodule_event_type_overview_form' => array(
'arguments' => array(),
),
);
}
Have fun :-)
Katja

Categories