Debugging a nasty AJAX problem, I found that drupal_rebuild_form() (called from ajax_form-callback()) always fails to rebuild the values in $form and $form_state. Instead, the values of both were cleared.
It turns out that something was lost in some patch, and the assumption in _form_builder_handle_input_element() is that it can call all of these with a third parameter, $form_state, with the intent of rebuilding the form from $form_state.
$value_callback = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value';
...
if (!isset($element['#value'])) {
// Call #type_value without a second argument to request default_value handling.
if (function_exists($value_callback)) {
$element['#value'] = $value_callback($element, FALSE, $form_state);
}
But of course, since none of the functions form_type_checkbox_value() and all its friends do not take a $form_state third arg, all this gets lost.
This patch is my guess at what they were thinking here. But FAPI gurus will have to delve into the history. I don't claim that it's pretty, just that I think it's what must have been intended.
| Comment | File | Size | Author |
|---|---|---|---|
| #163 | drupal.form-builder-values.163.patch | 33.63 KB | effulgentsia |
| #187 | drupal.form-builder-values.187.patch | 25.9 KB | sun |
| #185 | drupal.form-builder-values.185.patch | 25.89 KB | sun |
| #181 | drupal.form-builder-values.181.patch | 25.29 KB | effulgentsia |
| #179 | drupal.form-builder-values.179.patch | 25.29 KB | effulgentsia |
Comments
Comment #1
rfayNote related #447930: #value_callback documentation and signature (D7)
Comment #2
rfayThe WTF factor of this stuff is so high that I'm not even sure this is the only way or even the correct way to fix this.
I should mention this comment in ajax_form_callback() (ajax.inc) where the drupal_rebuild_form() is called:
The intent seems to be that we are indeed destroying everything (which is what happens) and rebuilding it from $form_state.
This calling code for drupal_rebuild_form() with the assumption of how drupal_rebuild_form() should work went in with the ctools patch, #544418: Integrate CTools AJAX framework with Drupal to extend (and replace) existing ahah framework by merlinofchaos.
The latest interface change for form_type_select_value() and friends, including the call to
$value_callback()and a change to all the called functions, went in with #483778: The great form.inc cleanup tour, part 1 by Frando, but the chain of intent goes back before that. The original change of $value_callback() calling with 3 arguments went in with #322344: (notice) Form improvements from Views by merlinofchaos and Frando.I should note that there was one form_type_xxx_value() function that did take $form_state, but for its own purposes. form_type_image_button_value() used $form_state to deal with an IE anomaly, and it's possible that this whole WTF revolves around that. Also, I did not change form_type_image_button_value(), since I'm not familiar with that bug or its history.
Comment #3
rfayThe Examples module AJAX Example now provides a testbed for this. Use the "AJAX Example: Advanced Commands" menu.
All of the examples there that use anything but 'submit' will demonstrate the issue, I believe. So the CSS command, for example (which doesn't work without an additional patch, but which shows the form action) would work as a testbed.
It's in CVS HEAD now, and will be in the dev tarballs in several hours.
Comment #4
effulgentsia commentedNo, value callbacks are passed $form_state for special purposes only; most of them shouldn't need it. Instead, they get a $input parameter that _form_builder_handle_input_element() should create for them correctly in this code:
Can you post the steps to reproduce the bug you're seeing?
Comment #5
effulgentsia commentedx-post. thanks for posting the example :)
Comment #6
rfay@effulgentsia, the problem is the default value callback, which is always there. It calls form_type_xxx_value().
The $input parameter is always NULL in this case, because drupal_rebuild_form() is trying to rebuild from scratch, from $form_state.
So we end up trying to rebuild the $form from $form_state. But because the form has already been cleared deliberately, we get no values. And then the form_state is cleared based on the new $form.
Comment #7
katbailey commentedJust chiming in to comfirm this bug. $form_state['values'] gets wiped after the call to drupal_rebuild_form in ajax_form_callback() - and rfay's description above looks pretty dead on based on what I was seeing when trying to debug. Also marking as critical, because I think it is ;-)
Comment #8
katbailey commentedum, now marking as critical
Comment #9
effulgentsia commentedOk, I think I might have found the problem:
drupal_rebuild_form()sets$form_state['input'] = array()_form_builder_handle_input_element()calls$value_callback($element, FALSE, $form_state)instead of$value_callback($element, $input, $form_state), because there's no $form_state['input']._form_builder_handle_input_element()callsform_set_value($element, $element['#value'], $form_state)which wipes out the data from $form_state['values'].I don't know what the solution is, but this definitely seems like a critical bug.
Comment #10
effulgentsia commentedComment #11
effulgentsia commentedSome guidance for how to test this:
$form = drupal_rebuild_form($form_id, $form_state, $form_build_id);within ajax_form_callback().Expected behavior: after the breakpointed line $form_state['values'] and elements' #value properties should contain the data submitted by the AJAX form.
Comment #12
rfaySo @effulgentsia, you don't think this patch flies? It certainly resolves the issue. Whether it's the right thing or what was intended is another issue perhaps.
Comment #13
effulgentsia commentedI don't, but FAPI often makes no sense to me, so I'd like to know what quicksketch, sun, or another FAPI guru thinks.
Comment #14
sunIt seems like you're doing the same everywhere.
Hence, not a job for each value callback.
I'm on crack. Are you, too?
Comment #15
chx commentedWait a second. There was a very clear intention of destroying POST before rebuild form is called, so that every form is starting from the same state. By pulling values out of form_state['values'] you are circumventing this protection and we get back to the sorry mess we had before.
Comment #16
chx commentedI talked with rfay and he promised he will post a bug report.
Comment #17
effulgentsia commented@chx: I'm curious what rfay posts with respect to a bug report, but please see #11. That's either a very clear FAPI bug report, or else that behavior of drupal_rebuild_form() is by design, in which case, ajax_form_callback() is calling drupal_rebuild_form() with the wrong expectation, and this becomes an AJAX bug. But I think only you can answer whether the "Expected behavior" in that comment is the wrong expectation.
Comment #18
rfayHere's an attempt at a clearer bug description.
Using the core AJAX callback, ajax_form_callback() in ajax.inc, I set up a standard callback. The callback is passed $form and $form_state. However, $form_state is reset to default values before it's made available to the #ajax['callback']. They are available and correctly processsed at the time the form submit is called, but are destroyed later in AJAX processing.
What I did: Set up a standard AJAX callback using a 'select' element to drive the AJAX, and which returns an array of AJAX Commands.
What I expected: The callback should get correct, submitted values for $form_state, to drive decisionmaking within the callback.
What happened instead: $form and $form_state contained only default values, not the submitted values.
Analysis:
The AJAX Example (Advanced commands) in CVS HEAD or the 7.x dev tarball of http://drupal.org/project/examples can be used to demonstrate the behavior. Use any of the select or testfield or checkbox-driven demonstrations.
Two patches are provided that approach this problem from the perspective of letting form_type_ELEMENTTYPE_value return the value provided by $form_state, assuming that this was the original authors' intent.
The history of the code in question is discussed in #2.
Comment #19
chx commentedWell. Apparently people keep inventing clever ways to code around the clarifications we tried to build in. So what about tthis?
Comment #20
chx commentedAnd to clarify where I stand -- if you blindly copy user input from one form to the next, then you will not be able to uncheck a checkbox in a multistep form.
Comment #21
chx commentedI have a lot to say sorry for the many posts here: "Stepping through ajax_form_callback() it's clear that $form_state is destroyed during the call to drupal_rebuild_form() in ajax_form_callback(). Everything is fine to that point." This is not so. We delete form_state['input'] and after my patch form_state['values'] so that every form, whether it's regenerated or not starts from the same, clean state. You have your liberty of using a submit handler to populate form_state as you please and reuse that as node_form does... What is the Drupal 7 (supposed?) workflow if not this?
Comment #22
rfay@chx takes issue with the very idea of how #ajax['callback'] is used in the examples module, but it seems to me to be the intent of the entire D7 thrust with AJAX: Standardizing the callback instead of everybody writing huge complex messes of code that are seldom understood.
His final statements of the evening in IRC were:
rfay: there is not a single mention of #callback in modules.
rfay: that's where we are stuck
we need better documentation here.
All of the above are true: The core modules were not updated because they're a maze of uses of #ajax['path'], all nonstandard, many of which do not follow chx's advice on the subject in his D6 handbook page. They should be updated.
We do need better documentation here. That's where this bug comes from. We're trying to write simple examples of AJAX that work that will serve as understandable starting points for all developers. D7 makes that possible. We need to do it. The way the examples exist at this point is not necessarily the only way, nor is it necessarily correct. The intent is to find good, simple, correct ways and use them.
Comment #23
effulgentsia commentedWe need quicksketch or merlinofchaos to look at this. We need to:
1) Understand what the correct expectation is for what's in $form and $form_state within a #ajax['callback'] function.
2) Ensure that ajax_form_callback() is written in a way to satisfy that expectation.
Yes, AJAX in D7 *should* be made easy. People implementing #ajax['callback'] functions should understand what $form and $form_state represent, and all the details involved in getting them properly initialized should be safely hidden away from them by whatever magic needs to exist in ajax_form_callback().
Comment #24
sunIt looks like this bug report will have an API change. Yes, we urgently need merlinofchaos and quicksketch in here.
Comment #25
rfayOne possibility on this is that we accept that $form_state['values'] is destroyed before the callback gets to look at it, which, if I understand chx correctly, is intentional and appropriate. The documentation and example could show that if you want the form_state, you have to grab it in a submit handler and stuff it somewhere else. This would be sad from a developer experience standpoint, but if I understand chx correctly, he considers it essential to the integrity of the forms system. And I'm not sure he'd approve of capturing $form_state['values'] and stuffing it somewhere else so that the callback could look at it.
Comment #26
katbailey commentedThis is exactly how it worked in D6 and it seems that the misunderstanding is that although the Ajax side of things has changed greatly in D7, FAPI itself hasn't, so a submit handler is still required in order to hold on to anything from $form_state['values'], i.e. by putting it into $form_state['something_else'].
Comment #27
effulgentsia commentedHere's more puzzling info that doesn't require rfay's example module. I did a fresh install of HEAD, edited the "page" content type, added an "unlimited values" text field, then went to node/add/page. Then I filled in the following:
Then I breakpointed the line described in #11 and started a debug request by clicking the "add another item" for the multi-valued text field. I got the following results before and after the breakpoint.
Before rebuild
$form_state['values']['body']['zxx'][0]['value'] = 'Body'
$form['body']['zxx'][0]['value']['#default_value'] = 'Body'
$form['body']['zxx'][0]['value']['#value'] = 'Body'
$form_state['values']['revision'] = 1
$form['revision_information']['revision']['#default_value'] = false
$form['revision_information']['revision']['#value'] = 1
$form_state['values']['log'] = 'Log message'
$form['revision_information']['log']['#default_value'] = ''
$form['revision_information']['log']['#value'] = 'Log message'
After rebuild
$form_state['values']['body']['zxx'][0]['value'] = 'Body'
$form['body']['zxx'][0]['value']['#default_value'] = 'Body'
$form['body']['zxx'][0]['value']['#value'] = 'Body'
$form_state['values']['revision'] = false
$form['revision_information']['revision']['#default_value'] = false
$form['revision_information']['revision']['#value'] = false
$form_state['values']['log'] = 'Log message'
$form['revision_information']['log']['#default_value'] = 'Log message'
$form['revision_information']['log']['#value'] = 'Log message'
These results were the same regardless of whether I was using HEAD, or the #19 patch.
Questions:
As I understand it, one of two things needs to happen:
Summary: merlinofcahos, quicksketch, chx: please help us understand if either of the above statements is true, or if I'm totally missing the point of how any of this is supposed to work.
Comment #28
sun1) There is a certain expectation by developers.
2) The current code in HEAD does not seem to do what developers expect.
3) We have a whole framework for expectations.
So why don't we start with a test that fails, but shows the expectation? We need a test for whatever this patch results in anyway.
Comment #29
rfayDoing a test is a great and desirable idea, but we currently have no simpletest capability for just javascript or for AJAX submissions. #237566: Automated JavaScript unit testing framework is the followup on this. I have some time set aside for this this month, but don't know if I'll be successful in pushing it forward.
Comment #30
sunWe can certainly mimic AJAX requests in the current testing framework. Note that the JS unit testing framework is only about unit tests within JavaScript.
Comment #31
rfayWe can make a call to system/ajax and parse the result. That's about the limit. It's ugly. It makes many, many assumptions about the form being presented to system/ajax. But it can be done. What we cannot do is load up a form, change an AJAX-enabled element, and test the resultant behavior.
Comment #32
effulgentsia commentedA test for this is definitely possible and a great idea. I've never written a test before, but what a great use-case to use for teaching myself how to do it. I have some other work I need to get done today, but I'll work on this as soon as I can unless someone beats me to it.
The purpose of the test is to inspect $form and $form_state before and after drupal_rebuild_form() under different circumstances. So, we can create a module that implements a menu callback that returns a form and another menu callback that the form can be submitted to. That 2nd menu callback can duplicate what ajax_form_callback() does up until the call to drupal_rebuild_form(). The test can then make assertions of what's in $form and $form_state before and after calling drupal_rebuild_form(). The difficulty is we don't know what the proper expectation is (what should we be asserting?), but a little playing with it should reveal some pattern, which we can use to form an expectation, and then let more knowledgeable folks tell us that our expectations are wrong.
Comment #33
effulgentsia commentedIt's possible that this will reveal that there actually is consistency and that drupal_rebuild_form() does indeed destroy $form_state['values'] and #value properties, and that this is by design. If this turns out to be the case, then see my last bullet point in #27.
Comment #34
sunWow.
I just read the original post and first comment on #367006: [meta] Field attach API integration for entity forms is ungrokable once again. And I just realized that it's (partially) talking about the very same wonky issue that requires some wonky form values massaging to make AJAX callbacks and form submissions work without JS.
However, that issue has been turned into more in-depth WTF-issue about Field API + Form API integration, so I just want to mention that this issue is not the only one.
Comment #35
chx commented@effulgentsia you looked at node form which does this in the beginning of the node_form:
which copies over values. That's why you are puzzled.
Comment #36
sunNode form is not the only form that contains such weird magic. While working on #414424: Introduce Form API #type 'text_format', I realized that there are quite some other (entity) forms that implement this or even more weird code to temporarily store + resurrect a "thing" in $form_state, most often converted from array to object, and on rebuild, fetch that "thing" back, most often converted from object to array, or, the other way around. Countless of code lines, plus a lot of technically unnecessary form submit handlers and separate form handling functions, or in other words:
Stuff we don't want.
Comment #37
rfay@effulgentsia, have you had any success in your investigation of this?
Comment #38
chx commentedWell, I took good historical look at the problem. Let me see
Sun's patch while convinient goes against years of Drupal hardening. However, salvaging it is possible and not too hard: just make sure that form_builder nukes form_state[values] in case of a form_error. So add to form_builder:
that will make sure form_state[values] wont get a chance to be reused if the form failed validation, most importantly the option checker.
Edit: observe how #10 suggested the same but for all cases even when it's not necessary.
Comment #39
sunTo summarize once again:
right into drupal_rebuild_form() itself, so drupal_rebuild_form() would immediately bail out and NOT return a new rebuilt form in case there was an validation error. (The important part is !form_get_errors() here.)
Note that the question whether we will be able to uncheck a checkbox may still remain. (Needs tests)
We need tests for this patch.
Comment #40
chx commentedThat and then we need a multistep form that has a checkbox on it and check whether you can check, press next, uncheck, save and it gets saved unchecked. And then we need a test that posts tampered POST to a form (have a select box and post something that's not among the options) and make sure it does not survive in the next step.
Comment #41
sunrfay will summarize the remaining problem (or already has). In short: We need to setup the value before value callbacks are invoked.
Comment #42
rfayThe patch in #39 doesn't actually solve the problem here. I'm about to try sun's new patch in #41. He's way too fast.
The core issue here is that when a form element like a select triggers an AJAX callback, the #ajax['callback'] function should receive in $form_state['values'] the NEW (changed) value. And of course all the other values.
The original behavior, and the behavior still in #39 is that the $form_state['values'] of the #ajax item (the triggering element) comes through as the default value, not the new value from the form.
There are several trivial tests that use $form_state['values'] for this in the "Advance Ajax" test in the Examples module, http://drupal.org/project/examples. The "Ajax CSS" test, for example, is select-element driven, and is a good example. With #39 and the default behavior, the $form_state['values']['css_example'] never comes through as the updated value.
Comment #43
rfay#41 does not resolve the issues discussed in #42, at least in my quick test.
Comment #44
sun#41 doesn't look right to me, so any further stuff should be based on #39.
Comment #45
chx commentedWell, fine, but adding a if (!empty($form_state['values'])) check would be good to avoid iterating #parents when there is nothing (which, let's face it, is the common case.
Comment #46
effulgentsia commented@rfay, re: #37: No. I'm still finding myself confused by FAPI. Here's where I'm still stuck: #367006-23: [meta] Field attach API integration for entity forms is ungrokable.
Comment #47
rfayI'm working on a test for this. I think we'll be able to get it worked out.
The thing that's taking the most time is trying to build a test infrastructure to be able to submit properly to an existing form. If I can get that in place, it will be very useful for ongoing tests.
Comment #48
rfayHere is the first installment of a test suite for AJAX. I added a module to work against and the processing that adds ajax_triggering_element to the $_POST.
It will work with the original patch. All the other patches still reset $form_state['values'][$ajax_triggering_element] to the default in drupal_rebuild_form(), so it doesn't work with them.
It had been a while since I returned to simpletest, so this was a good exercise. Of course I'm open to any coaching on the test technique.
The provided module can be used independent of the test for manual testing. The hidden = FALSE is commented out in this version so you can use it that way if you want to. It should be uncommented before we actually put it with a fix and start to settle in on it.
Thanks all for your work on this.
Comment #50
rfay@tha_sun, the reason #39 doesn't work is that the value assignment is inside the second if, but we don't go in there.
Comment #51
rfayI moved the basic implementation of AJAX testing into another issue: #633156: Need tests for AJAX Commands. It does not yet contain the tests in #48, as they would fail, of course, and it would never get committed :-)
But hopefully the new issue can get committed fairly soon, and then these tests can be patched into it.
The tests required by @chx regarding general form API behavior (multistep, checkboxes) are a separate project and can probably remain attached to this issue.
Comment #52
chx commentedYou are changing general multistep form API behavior so the tests indeed belong here. Note that with pifr v2 you can actually click on the failures on qa.drupal.org and see what they were.
Comment #53
rfay@chx, the tests will be here. I'm just trying to get the basic structure into core as fast as possible, especially AJAXTestCase and AJAXPost, so that that stuff doesn't become dependent on this issue. If necessary, I'll maintain in both places.
The multistep and general form tests will in fact be developed in this issue, once we have a working patch. The patch that I posted fails because it tests the case we've been talking about, which is not yet fixed. It succeeds with the original patch, because that gets the general behavior we want, but it's not the way we want to do it.
Comment #54
sunoh my - how stupid from me. Sorry :P
Comment #56
effulgentsia commentedFYI: I'm working on an alternate approach and additional tests. I'm hoping to get somewhere with it today and post it here.
Comment #57
rfayHere's sun's patch from #54 with the test I put in there.
I only ran the test against it and didn't study sun's patch, but it does not pass this test.
@effulgentsia, I rolled this just because the test had moved a little bit. AJAXPost has moved into DrupalWebTestCase in #633156: Need tests for AJAX Commands. There is also cruft in the test module here that's actually for the larger picture, but have to straddle this somehow.
AJAX Form Values In Command is the test dealing with this issue.
Comment #59
sunLet's keep this focused.
Comment #61
effulgentsia commentedThis one takes #59, adds some form rebuilding tests, but replaces the form.inc changes with my proposal. There remains a test failure and a comment within the form.inc hunk that explains it.
The new tests in this patch address what I was talking about in earlier comments that the wiping of $form_state['values'] and #value properties within drupal_rebuild_form() is BY DESIGN. What I perceive this issue to be about is that AJAX callbacks don't necessarily want to treat the form as a normal multi-step form, but rather as a special multi-step form where input values are not wiped, but are carried over from the pre-rebuild. But, they still need to be re-validated with the new $form definition.
Comment #63
sunI wonder whether this whole thing doesn't boil down to:
drupal_rebuild_form() is invoked with empty($form_state['rebuild']).
Or in other words: A rebuild is requested without $form_state['rebuild'] = TRUE.
This review is powered by Dreditor.
Comment #64
sunAlso, you cannot do this...
Form element validation handlers and value callbacks can alter the 'value' of an element based on 'input'.
A good example is http://api.drupal.org/api/function/form_type_password_confirm_value/7
This review is powered by Dreditor.
Comment #65
effulgentsia commentedOK, how about this?
Comment #66
chx commentedI do not like this progressive rebuild thing, what if you want your form to fall back elegantly, you are back to square zero. There is no need for such distinction, AJAX or not, not wiping perfectly valid values (while makign sure user input triumphs over) is a nice thing and I think I made it secure. Just please make the tests pass.
Comment #67
chx commentedCrosspost, but I still do not see the point in having two modes.
Comment #68
effulgentsia commented@chx: please look at form_test.module, the
form_multistep_test_form()function, within patch 65. Am I wrong that this is a specifically designed feature of FAPI? That step 2 can have the same form element name refer to a different element, and therefore need a clean value, because it's essentially a different form. That's the reason for 2 modes. Unless, we decide we don't want to support these kinds of multi-step form constructions, in which case developers need to know that different elements needs different names across the entire multistep sequence.Comment #69
effulgentsia commented@chx: if you like the basic structure of the overall patch, can you specifically give feedback on this block? I'm not clear on when you would ever rebuild a form that has errors. If you do, is this correct, or does it need to be set to FORM_REBUILD_CLEAN?
This review is powered by Dreditor.
Comment #70
sunComment #71
sunSo, yeah, I think all of that boils down to this one.
Comment #72
sunNot even sure whether this is about $form_state['values'] at all... anyway, fixed a stale comment.
Comment #73
effulgentsia commented@sun: any chance of you popping in on IRC so we can discuss?
Comment #74
effulgentsia commentedWhat about if a form uses $form_state['storage'], but doesn't set $form_state['rebuild']? Do we want that re-populating from input? Do we want those forms to have an option as to whether they repopulate from input? See #68. If what's in this patch is what we want, ok, I just want clarity.
What about programmatically submitted form? This won't work for them. Why not just leave $form_state['input'] alone, since this is in the else statement?
Unrelated to this issue. What does this solve? Should we create a separate issue for it?
I'm on crack. Are you, too?
Comment #75
sunAlso merged the tests
Comment #77
fago>Crosspost, but I still do not see the point in having two modes.
As mentioned in #68 keeping form values doesn't make much sense when the form changes. Well this is nice if you just reload the same form several times, but this won't make much sense for multistep forms having multiple different steps.
Thus I don't see why we should keep the form values at all. The usual multistep form workflow just lets the user care about setting the right form values (regardless whether it's an AJAX callback or not), which works fine as long as the form workflow is implemented in clean way. Yes, right now it's a mess and I think this is the real problem here.
See http://drupal.org/node/367006#comment-2272166 for a proposed clean worklfow using storage. Thus once the data is in the form storage it can be used to set the element values - thus fixing this problem.
Comment #78
fagorelated bug: #634984: Registration form breaks with add-more buttons
Comment #79
chx commentedOK then for two modes, I can see this... keep values flag? Or simply if you do not want to keep em just delete it from form_state yourself? Is it harder to write $form['#progressive'] = FALSE; than $form_state['values'] = array(); ?
Comment #80
effulgentsia commented@fago: I think we're on the same page that we need to have some solution that allows both wizard-style multi-step forms to work AND have it be possible to add non-js and js "add more" buttons to multi-step or non-multi-step forms and have those work in a sane way. I'd like to understand better your proposal regarding $form_state['storage'] as a way of solving this. Within patch 65 exists test forms in form_test.module: form_multistep_test_form() (a wizard style form) and form_progressive_rebuild_test_form() (a form with non-js "add more" buttons). The patch includes simpletests for them, but you can also enable the form_test.module and run the forms visually in the browser to see how they work. Can you post your vision for how these can be written to use the "clean workflow" that you suggest?
@chx: Where do you propose setting
$form_state['values'] = array();? In a submit handler? How can the submit handler know that it's the last submit handler running? Or do we want to establish a convention that submit handlers shouldn't read from $form_state['values'], but instead only from an element's #value, in which case, any submit handler can reset $form_state['values'] without breaking later submit handlers.Comment #81
effulgentsia commented@chx: Or, maybe instead of a submit handler clearing the values, it can do the opposite:
$form_state['rebuild_with_values'] = $form_state['values']? Is that better than $form_state['rebuild_mode'] (note, patch 65 replaced 'rebuild_progressive' with 'rebuild_mode')?Comment #82
effulgentsia commentedfago's solution may render this one irrelevant, but this patch tries to incorporate all feedback so far.
1) fago brought up a good point on #634984: Registration form breaks with add-more buttons, which is we shouldn't decide whether or not we want clean vs. carried over values based on $form_state['storage'] alone. Because, one could add an AJAX "add more" button on a multistep form.
2) This one simplifies. No need for different "modes". This uses a variant of chx's suggestion to be more direct. And sun's initial instinct that $form_state['rebuild'] is the important property to switch on.
3) No need to go through the 'input' pipeline again as is done in #75. Going through the 'input' pipeline runs into incompatibilities with "add more" functionality in conjunction with #default_value, as evidenced by the test failure. We have 'values' that have already been processed through the value callback. This uses them.
Comment #83
sun*Why* would I want to $form_state['rebuild_values'] ?
I'm very opposed to introducing yet another $form_state property.
Maybe the reasoning for the change in form_builder() in the previous patches didn't come through:
There is nothing that ensures that 'values' have been validated.
Hence, chx's idea was to make form_builder() nuke 'values' in case there was a validation error.
This review is powered by Dreditor.
Comment #84
fago>Regarding introducing possible troubles with real multistep forms:
With that I meant that an rebuild keeping values might lead to wrong values in the next step of a form. You addressed that already by making the feature optional.
So this patch looks fine, however I'm not sure whether it's a good thing to do.
* Currently forms and multistep forms work quite different. When you want to do a multistep form you have to work around quite some caveats you don't have to deal with usually. With that patch I see the differences increasing.. What if you suddenly need to write a AJAX callback for a multistep form? It won't work as usual and probably you won't find much docs about it..
* Instead of the form workflows becoming easier and working always the same there is just another magic option confusing developers...
Instead of that I'd like to see the existing form workflows cleaned up. For that I'd propose to use the form storage, see http://drupal.org/node/367006#comment-2272166 for an example. Once you put in your data into the form storage initially, you have it available any time and you can modify it any time. You have one single copy of it, thus you don't get confused with old or new form values. When constructing the form, you always populate it from your data and it will always contain the latest values. It would work that way for regular forms and it would work for real multistep forms - no difference. Having no difference means also a form_alter() can make a form multistep without breaking the form e.g. it could add an "add more" or "hide" button. Also things working always the same way ensures it's easy to add another step to an existing simple form - without having to fix the existing stuff.
Well to make the form storage that consistent to use we need to remove it's magic, so you could use it although you don't plan to do a multistep form -> see #634440: Remove auto-rebuilding magic for $form_state['storage'].
Comment #85
chx commentedI have no problems with this approach. We always said that the submit handler should stuff the values on which the next step could build into something. If you pick rebuild_values as that something, then it's automatically reused. If you, on the other hand, set rebuild_values to array() then no harm done. Minor: a nonempty rebuild_values should autotrigger form_state['rebuild'] however.
Security wise, we are good. Automatic copying of values to rebuild_values does not happen if there was a validation error. Manual copying won't commence either because submit are not fired on validation errors.
Comment #86
chx commentedI crossposted with fago. I agree with fago too :D
Comment #87
sunI fear we're not getting anywhere. Let's do a general discussion in #635552: [meta issue] Major Form API/Field API problems and go back to individual issues afterwards. Thanks.
Comment #88
effulgentsia commentedUpdate to folks following this issue: Patch 82 will need a re-roll due to #633156: Need tests for AJAX Commands landing, and to incorporate feedback. However, I'm waiting for #634440: Remove auto-rebuilding magic for $form_state['storage'] to land first, since it will also affect some of the details of this patch. After it lands, I'll re-roll this patch, and we'll be able to continue a discussion about this approach vs. #84.
@chx: if you're seeing this message, you are being solicited to review #634440: Remove auto-rebuilding magic for $form_state['storage'].
Comment #89
sunbtw, http://api.drupal.org/api/function/text_field_widget_formatted_text_value/7 goes one step further and even provides an explanation for more weirdness: Form element value callbacks don't receive values for elements with #access => FALSE.
Comment #90
effulgentsia commentedThis patch includes a re-roll for #633156: Need tests for AJAX Commands and update due to the decisions in #634440: Remove auto-rebuilding magic for $form_state['storage']. It also includes more documentation in response to #83. I changed $form_state['rebuild_values'] to $form_state['rebuild_options']['values'], because, let's face it, the whole rebuilding thing is pretty complex, so let's assume we'll need more options some day (whether in D7 or D8).
From #85:
This patch does not include that, and I'd prefer it doesn't, because this type of magic is along the lines of what we've been trying to remove in other issues. If you want a form to be rebuilt, set $form_state['rebuild'] = TRUE. If you want to turn off rebuilding, set $form_state['rebuild'] = FALSE. Let's not include other properties that can turn rebuilding on or off. In that spirit, $form_state['rebuild_options'] affects what will happen IF a form is rebuilt, without affecting WHETHER or not it's rebuilt.
Re #84: While I would like us to explore cleaning up form construction and alter functions to use $form_state['storage'] in a consistent way, and while that may be necessary in some forms for Field API integration to be grokkable, I'm skeptical that it makes sense to require this on all forms, core and contrib, especially considering that code freeze has happened and people have started porting their modules. However, I think it's important that adding an "add another item" button (not just in the context of Field API, but any contrib module that wants to add a button to a form that changes the form in some small incremental way) should be possible on all forms, and therefore, I think this patch makes sense even if we also implement the improvements fago is suggesting to forms that let you edit entities and fields.
Re #89: I'm not clear how that affects this patch. It makes sense that #access=FALSE should cause $form_state['input'] to be ignored for that element, which is the behavior in HEAD and is unchanged by this patch.
Comment #91
effulgentsia commentedSame as #90 except:
s/drupalAJAXPost/drupalPostAJAX/
Comment #92
effulgentsia commentedSorry. #90 and #91 still had extraneous hunks that should have been removed due to #633156: Need tests for AJAX Commands landing. This one should be good, but still needs code review.
Comment #93
effulgentsia commented#90 and #91 should have failed bot, but didn't because the AJAX tests didn't run. I'll report that as a separate PIFR issue. However, clicking on "view details" for #92 does show that the AJAX and form tests ran.
Comment #94
effulgentsia commentedAs noted above, #92 is fine and can proceed to being reviewed, but for anyone troubled by the fact that #90 and #91 passed bot, I opened #641038: Sometimes, tests in a buggy patch don't run and therefore don't fail, which can lead to broken HEAD.
Comment #95
fagoRe #90: I see your point, however I still think we should really avoid mystifying real multistep forms even more. (see #84)
So what could we do to make it easier to make all forms "multistep-aware"? So what about making $form_state persistent as whole, like the storage? Imo the name already suggests that the data in their persists the whole form state and as of now the $form_state is quite useless. See #641356: Cache more of $form_state when form caching is enabled.
If we would do it that way all we would need to do for the current implemented forms is to cleanup the form workflow, so that the form values get populated from the data in the form_state if it's there. That way we would enforce cleaner form workflows - having the data only once: in the form_state in contrast to having forms with X different data values (the original value e.g. $form['#node'], the form element default value, the submitted form element value)
Comment #96
rfay#92 certainly resolves the concerns that prompted this discussion in the first place. I've run it through all the AJAX situations that failed before, and it's great now.
Comment #97
sunAs mentioned before on my own, I agree with fago.
It seems like all this patch wants is to keep 'values' somewhere floating around. Many form handlers, which need to re-access the values already do something like in this new patch.
However, looking some more at this code now, I don't quite understand why we do not simply try to keep 'values'. The only difference to my earlier patches in the more recent patches is the location where 'values' are taken into account during form element input handling. Aside from that, it applies the very same logic, and the very same protection for form validation errors.
Comment #99
sunSame as #97, but fixing the failing test.
Comment #100
sunAnd here is the approach I mentioned in #97:
As in earlier patches in this issue, we simply keep $form_state['values'] as long as there are no validation errors. The difference to my earlier patches is that those values are processed in a different location in _form_builder_handle_input_element(), as in the patches by effulgentsia.
Additionally, the example multi-step form that has been added in form_multistep_test_form() was a bit strange. It tried to verify that a form element appearing identically in multiple steps with the same name would not take over a submitted value. That's entirely not what developers expect. Developers expect that the identical element will get the previously submitted value. Otherwise, you will use a different element.
If this one works, then I think we have reached a great simplification of the form processing and rebuilding workflow here.
Comment #101
fago>It tried to verify that a form element appearing identically in multiple steps with the same name would not take over a submitted value.
Yep, it needs to work that way when the next step is another form but has overlapping form element names. Thus being "real multistep".
Comment #102
sunI would never expect this behavior. And it seems to overly complicate the problem we're trying to solve.
If I'd want that, then I'd do:
Without an account/friend key - expecting that 'name' will be reset + emptied in multi-step scenarios doesn't work at all. At least not with the previous attempts in this issue, in which we tried to take over any values from a previously submitted step. That is, because there is no information about which values should be kept and which not. Hence, we would have to supply an array of form element parents to denote a list of special form elements that shall take over the value, and skip the value for all other elements not listed in there.
Doing that, however, would make this entire workflow so complex that there is no real benefit from accounting for any previously submitted values at all. It would also break the use-case of making a non-multi-step form multi-step, because one (but also potentially more than one) hook_form_alter() implementation would have to define the values that should be taken over during rebuilding, whereas a single hook_form_alter() isn't necessarily the last hook_form_alter(), and thus, some elements/values may be missing.
So. I don't really have a clue what "real multi-step" means, but the presumption that the _identical_ form element within the _same_ form should be emptied under certain circumstances but not under others seems entirely wrong to me - and is incompatible in itself with the approached solution of this issue.
Comment #104
effulgentsia commented@sun: Re #102: The problem we're trying to solve is this. FAPI has for several Drupal versions behaved such that drupal_rebuild_form() wipes $_POST (prior to Drupal 7) / $form_state['input'] (Drupal 7 improvement so that rebuilding one form doesn't affect other forms on the same page) and rebuilds the form in such a way that element #value properties and $form_state['values'] get built fresh without implicit continuity from the previous step. To have continuity, form building and processing functions are expected to put things into / take things out of $form_state explicitly. The summary and first paragraph of the PHPDoc for the function pretty much states this (though perhaps less clearly than it should).
I don't know if we consider this a good thing or not, but it's how it is. And some people who write multistep forms thinks it makes sense. Step 2 of a wizard is different than step 1, and a 'name' field in step 2 isn't necessarily the same field as the 'name' field in step 1. Similarly, a delete confirmation form is different than the form on which 'delete' was clicked, and so data submitted for the 'name' field on one isn't necessarily expected to carry over to the value of the 'name' field on the other.
At the heart of this issue is that we have specific use-cases where we DO need continuity of values. AJAX in general, and "add another item" buttons specifically (whether AJAX or not).
Which leads us to EITHER changing the behavior across the board, as you suggest, so that we change FAPI from ALWAYS rebuilding fresh to ALWAYS rebuilding with continuity, OR expanding FAPI to make it an option, as I have tried to suggest. I have submitted earlier patches trying to clarify this option ($form_state['rebuild_progressive'] in #61, $form_state['rebuild_mode'] in #65). In #79, chx agreed that two modes make sense, but suggested that instead of introducing a new property in $form_state, a form wanting to drop continuity should explicitly wipe $form_state['values'] in its submit handler. My concern with that idea is that it would require the submit handler to assume it's the last one running, because any later submit handler that runs prior to the rebuild would lose access to $form_state['values'], which would be weird. That led me to introducing a new $form_state variable to contain values for the rebuild without affecting $form_state['values'] itself prior to when drupal_rebuild_form() runs.
I think if we wanted to, we could keep polishing the name of the new $form_state[] property ('rebuild_preserve_values', 'rebuild_wipe_values', etc.) until it conveyed the concept clearly. Whatever property we introduce would be fine, because when a submit handler sets $form_state['rebuild'], it knows whether it wants the rebuilt form to have continuity or not (the handler knows whether it's operating as a "add another item" type of handler, or whether it's moving to a new step in a wizard, etc.).
However, I can also see that sun, fago, and chx are all against introducing any new property to $form_state. But that requires that we fundamentally change the design of how FAPI models multistep forms. We would have to make it clear that in ALL cases (validation errors aside), drupal_rebuild_form() maintains continuity of submitted values, and that if you're building a wizard where you don't want that, then you're responsible for unsetting $form_state['values'] in a submit handler that you've setup to run sufficiently late relative to other submit handlers that need to inspect $form_state['values'] (or better yet, just build your wizard steps to use unique element names). I'm fine with this decision if we think it's what's best for FAPI in order to keep it simple. I don't think that many people will be affected by it. But I think we have to accept that it's a pretty fundamental change to how FAPI models multistep forms.
If we're not comfortable making that kind of change to FAPI at this stage of the release cycle, then I see no way around introducing some new property in $form_state that can make it clear which of the two behaviors we want out of a particular rebuild.
Comment #105
fago>If we're not comfortable making that kind of change to FAPI at this stage of the release cycle, then I see no way around introducing some new property in $form_state that can make it clear which of the two behaviors we want out of a particular rebuild.
Well there is another way out - leave it like it's now but implement clean form workflows: Put your data into form state storage and then always use this data to populate or update values - that's it. Of course in this scenario only cleaned up forms would be "multistep-save".
#641356: Cache more of $form_state when form caching is enabled would just help migrating forms to work that way as we don't have to move everything into 'storage'. That we could just use $form_state which is often already in use - but of course it would work with storage too.
Comment #106
sun@fago: I still don't understand that proposal. We have $form_state['input'] and $form_state['values'] already. It sounds like you want to add yet another property and alter many forms so that they copy over 'input' into 'whatever', and base their form element values on 'whatever'. Which, in effect, is the same as respecting the existing 'input' first, and any potentially existing 'values' afterwards automatically. That's what the latest patch is doing, which needs no conversion and is pretty transparent when looking at the overall form processing flow in a big picture. Therefore, all forms can be multi-step and all forms can be rebuilt without major modifications to the form construction and form handling.
Comment #107
effulgentsia commentedIn #104, I said that I'm ok with a decision to change FAPI to always have continuity of valid values across a rebuild. I do think it makes sense. I don't think it's much of a burden to require multi-step forms to be written with this in mind. I don't think very many contrib modules will be negatively affected by this decision. As long as chx (as FAPI maintainer) and webchick or Dries are ok with it, let's see where it leads us. In fact, I'm still not clear whether the loss of continuity was an intentional FAPI design decision or a side-effect of needing to clear $_POST because of security issues. If continuity can be introduced in a secure way (and chx has said that the basic approach we've been taking meets that standard), maybe now is the time to get rid of that no longer wanted side-effect.
With this in mind, here's a continuation of #100. It will need some more cleanup, but I want to see what bot thinks and get feedback on the basic approach. The big change here relative to #100 is that we can't just re-use $form_state['values'] directly. That leads to unwanted side-effects (most of the test failures in #100). We need to put the old $form_state['values'] somewhere and use that and let the new $form_state['values'] get built cleanly. But, the difference between what I was proposing earlier and this new approach (based on #102) is that the "somewhere" can be somewhere private to FAPI and not intended for form implementations to use. So, in this patch, I put it into $form_state['build_info']['values'] since in prior issues it was suggested that 'build_info' should be thought of as a private FAPI property. I'm open to using a different variable (e.g., $form_state['_rebuild_values']) if 'build_info' isn't a logical place to store this information (the description of build_info is that it's for information needed to rebuild from cache, and that's NOT what's happening here).
Also, I added a temporary fix in field_add_more_submit(), which will need a better fix. Field API is one place in core that DOES assume a loss of continuity. Interestingly enough, it's the main use-case where continuity is needed, so I expect the better fix will result in the cleaning up of all sorts of convoluted code.
Comment #108
yched commented@effulgentsia: I'm not sure I understand the last paragraph in #107 - care to elaborate ?
Comment #109
effulgentsia commented@yched: Sure. I'm not sure how much of this issue you've been tracking, so I'll risk over-elaborating. One (but not the only) problem behind #367006: [meta] Field attach API integration for entity forms is ungrokable, #634984: Registration form breaks with add-more buttons, and related issues is that an "add another item" button must trigger a form rebuild. By itself that's not a problem, but the way FAPI works, element #value properties and $form_state['values'] get wiped during a rebuild, but after you click "add another item", it's desired for the rest of the form (elements unrelated to the field for which you're adding an item) to retain their values, and since FAPI doesn't do this for you, the specific form implementation must make this happen. And since Field API must work on any entity editing form, it needs to jump through hoops to make this happen. This issue is about changing drupal_rebuild_form() to maintain continuity of element #value properties and $form_state['values'] for elements that are the same in the rebuilt form. While the "add another item" of Field API is one use-case for this, this issue was originally opened to solve other AJAX-related WTFs (see top of issue).
While I believe this change will assist in making Field API integration with entity forms more grokkable, I'd like to leave that work to #367006: [meta] Field attach API integration for entity forms is ungrokable, and not take that up in this issue.
However, because the current Field API code has been written with the way drupal_rebuild_form() currently works, the change to drupal_rebuild_form() introduced in #107 causes test failures in FieldFormTestCase (see test details from #100). The test failures have to do with element names not being re-ordered by weight. So, #107 adds a hunk to wipe $form_state['values'] within field_add_more_submit(), enabling the form building functions invoked during the rebuild to receive wiped values as they currently do in HEAD. But, I see this as a temporary measure to move this issue along, and after this issue lands, I think we can take advantage of the change to drupal_rebuild_form() to clean up Field API in #367006: [meta] Field attach API integration for entity forms is ungrokable.
Comment #110
yched commented@effulgentsia: Thanks ! Great summary, and the 'temporary fix' makes more sense now.
FWIW, approach looks good to me, but I'll let you guys agree on this.
Comment #111
fago>The test failures have to do with element names not being re-ordered by weight.
That's a case where the next "step" should have different form values. So how is with those cases now dealt? Is one supposed to unset the "internal" $form_state['build_info']['values'] ?
@sun #106
No I suggest to introduce anything new. I suggest to use the a clean workflow as I already described in #367006: [meta] Field attach API integration for entity forms is ungrokable - let's explain in detail:
* If the storage isn't set, populate it with the initial entity (loaded or created from scratch) -> e.g. $form_state['storage']['node']
* Populate the form set the and initialize the form values with data from $form_state['storage']['node'].
* Once validated submit the values by updating the entity in the form storage. -> $form_state['storage']['node'] is fully updated node object now.
* If the form is rebuilt, everything works fine without any magic as the updated entity from the storage is used.
* On the final submit, just save the updated entity $form_state['storage']['node'].
I've implemented that workflow in profile2 and works fine. That way the form constructor always has full control over the actual form values, also each form handler, ajax callbacks or whatever has always easily access to the latest data.
Comment #112
sun@fago: Yes, that's what we want to do in that other issue. But that is only related to forms dealing with entities, or, stateful objects. #629252: field_attach_form() should make available all field translations on submit already implements a first mini-step towards that. However, we have to solve this problem for all forms.
@effulgentsia: $form_state['build_info'] is cached with the form when caching is enabled. I'm still not sure whether I understand why we need to copy/duplicate the values, but caching them doesn't sound right to me.
Comment #113
sunOn a related note, #642702: Form validation handlers cannot alter $form structure proves that this is required for all forms. I've added a commented out assertion in the patch over there, which should be uncommented in this patch.
Comment #114
fago@#112
We could apply this pattern to all forms, exchange $entity with your $form_data and it'll work exactly the same way...
Comment #115
effulgentsia commentedLet's see what bot thinks of this.
Comment #117
sunThe previous patches worked, because we moved the entire handling for 'values' below this condition - which also solved the "no value for #access FALSE elements" problem in one fell swoop (#access is contained in this condition).
This review is powered by Dreditor.
Comment #118
sun.
Comment #119
effulgentsia commentedRe: #117, I don't get it. The reason for moving it below was because we couldn't run 'input' through value callbacks a second time. If '#access' is FALSE, then we shouldn't override #value with anything other than what the constructor/alter/#process functions do.
This one solves the test failures.
Comment #120
sunIf an element has #access FALSE when a form is rebuilt/processed again, it has no #value, because there is no #input. The current code falls back to "Load defaults", i.e. tries to invoke a potential value callback, and if still no #value is set, takes over #default_value. That was at least a part of the essential changes in the very first patches of this issue.
Comment #121
effulgentsia commentedOK. This patch fixes it so that #access isn't checked for input from a rebuild.
I removed unnecessary test cases from the earlier patches (if we're changing the expectation of a rebuild from what I thought it was to what we've agreed to since #100, then that expectation only needs to be tested once, not in 3 redundant ways) and cleaned up the remaining test case.
I fixed the places where $form_state['rebuild'] was being set in a validation handler, based on your explanation in #642702-9: Form validation handlers cannot alter $form structure. And I updated the PHPDoc for the 'rebuild' property to match.
I cleaned up some other comments based on my evolving understanding of how this all fits together.
The main thrust of this patch (as well as the last couple) that is different from #107 and earlier is the copying of $form_state['values'] to $form_state['input'], and the initializing of 'input', 'groups', and 'values' BEFORE invoking the form constructor. To me, this makes a lot more sense. In a normal form build, during form construction and altering, 'input' contains the input to the form and 'values' is empty, and during the form_builder() phase (i.e., #process functions), 'values' starts empty and is progressively added to as form_builder() iterates through all of the elements. IMO, it makes sense to follow the same pattern during a rebuild. This enables all of the form pipeline functions to be less concerned about whether they are running in a rebuild context or not, which I think at least partially gets us to fago's vision.
Comment #122
sunugh. Until here, I really liked this patch.
Can't we avoid this anomaly by applying the same logic to 'values' like we do for 'input'?
I know that this wouldn't strictly follow your idea of 100% same processing flow, but...?
heh. We can simplify this. ;)
Just FormsRebuildTestCase. There are most likely more tests regarding further combination with form caching, form storage, etc. required in follow-up issues.
Common for all form tests, can be moved into setUp().
"Enables testing" ?
This review is powered by Dreditor.
Comment #123
effulgentsia commentedWith feedback from #122.
No. We cannot retain $form_state['values'] and use it to populate $form_state['values']. It creates some unexpected circularity in the form_builder() phase. Try debugging the test failures from #100 to see for yourself.
However, as you say yourself in #635552-21: [meta issue] Major Form API/Field API problems, form constructor functions should not have processing and validation logic, and I would extend that to mean that they should not access anything in $form_state['values']. During the construction phase, $form_state['values'] is empty (at least during initial build, and IMO, it should be this way during rebuild too). The other forms that check for 'confirm_delete' don't use $form_state['values'], and taxonomy shouldn't either. This patch fixes it in a way that's more consistent with the other ones (e.g., node deletion).
Comment #125
effulgentsia commentedSame as #123, but taking into account that objects aren't arrays in the taxonomy_form_vocabulary() hunk :)
Comment #126
effulgentsia commentedOne more improvement. As stated in #121 and #123, I feel strongly that we should clear $form_state['values'] at the beginning of drupal_rebuild_form(), have it be empty during form construction and altering, and then get populated in the form_builder() phase from $form_state['input'].
However, while #125 cleans up taxonomy_form_vocabulary() to work with that change, there are other form constructor functions that would need to be cleaned up too (for example, authorize_filetransfer_form()). I'm not sure why tests didn't fail for that form with #125 (possibly, because the necessary test doesn't exist). In any case, we're already on 100+ comments for this issue, so I'd prefer to leave that cleanup for another issue.
Therefore, this patch comments that bit out and leaves it as a todo for another issue.
Comment #127
sunI think this is almost ready to fly.
1) No colon after @todo, and, todos that wrap over multiple lines should be indented starting from the second line to make clear where the todo starts and where it ends.
2) @see
The part that tells that functions can check this property to get to know something makes me feel a bit uneasy. Technically, anything can set 'rebuild' at any time, and technically, anything can unset 'rebuild' at any time. No big deal, I guess we can keep this, even when it's a bit fragile.
Can we inline this, please? If at all, it should be $path, but I don't see a reason to abstract this here.
This should be assigned to $this->web_user, so an individual test can still login/logout if necessary.
However, we can as well remove this from this patch, because #644150: $form_state is stateless, even with enabled caching already contains this change (except for the drupal_json_decode() fix you squeezed in here).
Same as above, but this one we want to keep.
I'm on crack. Are you, too?
Comment #128
sunWe need this nice test + form constructor also for #370537: Allow suppress of form errors (hack to make "more" buttons work properly), let's get this done. :)
Comment #129
sunRe-rolled with those minor review issues.
I think this is ready to fly.
Comment #130
effulgentsia commentedAwesome. Thanks. #129 looks good to me.
Comment #131
chx commentedthis won't work. value is not a filtered copy of $_POST it can be vastly different. I do not know where this got into the patch when I said "I am ok with this" it was not there IMO.
Edit: looking at patch history, it was a separate piece just a few days ago
$form_state['build_info']['values'] = form_get_errors() ? array() : $form_state['values'];but then it slipped in...Comment #132
chx commentedHold. I am working on this.
Comment #133
chx commentedSo now i have loaded the form_test_form_rebuild_preserve_values_form and I think we have some very deep misunderstanding here again? Here is the basic form API workflow:
When you are constructing the form submitted back, what information do you have that would hold the number of textfields? You have not put anything in the form itself, there is nothing in form_state['storage'] ...
Comment #134
chx commentedI am terribly sorry for not having enough time to work on these issues more, I am trying to find a form that should work and it does not.
Comment #135
chx commentedHey! fago helped me understanding the problem is not that the count of fields is lost but that the value in the other field is lost. Ah-ha... ok working
Comment #136
fagoThe form provided in the test case is broken as it's making wrong usage of the form_state as chx described above. But apart from that it correctly tests whether the values are kept or not.
Comment #137
chx commentedOK so you want this
not to lose the value on pressing the button. I can see how this is felt as a bug.
Comment #138
chx commentedThis patch
form_state['input']. If you look at the comment it is no longer necessary becausedrupal_process_formwas removed earlier and replaced byform_builderso now the validate/submit handlers cant possibly fire. This keeps the input values as we wanted.skipped. However, the way you create a drupal_execute payload is to take a $_POST and change the values in there to your variables. So if you had a checkbox and wanted to uncheck in you needed to check in something explicit to drupal_execute while $_POST passed in nothing. (Yes drupal_execute is gone but the explanation was simpler this way).
Edit: In a separate issue we can discuss the AJAX hardening part of this patch which added an error check before calling drupal_rebuild_form. I feel that's not necessary we just need to set $form['#submit'] = array(); to make very very sure the top level submit handlers cant fire but the button level ones can. This will solve the more button issue, but we will see.
Comment #139
fagoWith that patch the values are kept, which really simplifies doing ajax stuff in 1-step forms. Wizard like forms still can set
form_state['input'] = array();, what should be documented then. But it's simple and makes sense for wizards.Also in a follow-up we should fix up and simplify the node form to make use of this functionality - as it's exactly a case for which this is intended to be used.
Comment #141
fagoRe-rolled and included the hank from sun's patch to temporary fix the field forms.
Comment #143
fagotoo silly. 'input' is our key to unset now.
Comment #145
chx commentedBrought in lots of sun tests from another issue. I think all this passes and the changes to form.inc are relatively small.
Forms need to be aware whether they want to keep user input or not. Node or field multiple for example does not -- these need additional cleanup. Note that this patch is a continuation of #138 not using the above two.
Comment #146
fagoComment #147
effulgentsia commentedSame as #145, except I re-inserted the testRebuildPreservesValues() test and corresponding form constructor from #129. However, I reworked the form_test_form_rebuild_preserve_values_form_submit_add_item() handler to match chx's solution to drupal_rebuild_form(). But this required a small change to _form_builder_handle_input_element() to allow $input to be FALSE. That change is commented in this patch.
@chx, @fago: can you confirm that this test addresses your concerns in #133 and #136?
Comment #148
sunI don't understand this. Is this an attempt to keep only some submitted form values, while chx's reset of $form_state['input'] will kill all values?
I'm on crack. Are you, too?
Comment #149
effulgentsia commented#145 is a good solution with respect to keeping input values and works well for the simplified version of this test in #137. However, if you re-insert the test from #129 into the #145 patch, then you get 1 test failure in this test, because the newly added item gets its value set to empty string instead of 'DEFAULT'. Because $form_state['input'] exists, but is NULL for the newly added item, and NULL gets passed to the value callback. This updated test (and updated hunk in _form_builder_handle_input_element()) solves this by allowing a submit handler to add FALSE within $form_state['input'], making the #145 solution work in a situation where new items are being added to the form during a rebuild.
Comment #150
effulgentsia commentedThis, by the way, is why upto #129, we weren't simply keeping $form_state['input'] -- because it doesn't correctly allow default values on newly added items. But, if we can assume that the submit handler that triggers the rebuild knows about all new items that will be added, then #147 is workable because that handler can add $input=FALSE to those items.
Comment #151
effulgentsia commentedOh, and the reason that NULL must be passed to the value callback, is because otherwise, checkboxes don't work. I still don't understand why #129 wasn't an acceptable solution, but I trust that chx has good reasons, so I'm trying to find a way to work within the design constraint of #145.
Comment #152
effulgentsia commentedPut another way, preserving a non-empty $form_state['input'] that contains data that must be sent to the value callback results in $form_state['process_input'] being true, which results in the value callback being called for each element, including elements that are new in the rebuilt form (for which there is no user submitted input), and this results in those new elements not having their default values respected. However, for checkboxes, there's no way to distinguish a lack of user input from user input of an unchecked checkbox. This is why previous patches attempted to create some solution where a rebuild would not require re-invoking value callbacks, but could use data from $form_state['values'], which does distinguish unchecked checkboxes from new checkboxes.
Comment #153
effulgentsia commentedFYI: I'm working on another patch that includes tests and comments that clarify #152. Will post it when it's ready.
Comment #154
chx commentedThanks for working on the issue.
#129 copied form_state['values'] into form_state['input']. That operation does not make too much sense alas because we derive pretty freely our value from_state['input'] and after a process callback or two form_state['values'] does not resemble really form_state['input'].
Now this groceries-movies stuff you added back. Care to tell me why did you add it back? What workflow are you testing that were not covered in my patch? Anyways, even if it's necessary the comment needs improvement " For the new item to have its value be its default value" -- this needs a mention that the default value comes from storage.
Now, the big block of text about input FALSE, second sentence, that's not really grokkable for anyone not inmately familiar with the issue we are struggling with: " However, from a form submitted with drupal_form_submit(), or during form processing, an input variable can be set to FALSE for the element's default value to be used." what about something along the lines of "When working with a multistep form user input by default is kept from one step to the next. There is a problem with the checkbox element: the lack of user input means uncheck. So if a checkbox needs to keep its state from one step to the next, put its value in form_state['storage'], assign #default_value based on that and change the relevant $form_state['input'] key to FALSE in the submit handler. The same problem might arise when programatically submitting a form." Huh :)
Comment #155
chx commentedHm, read that again. If you want something to be the default disregarding user input then why on earth you are are not setting #value instead? Or put it in form_state['storage'] and not display to the user? What's going on? What's the real case here for that input FALSE...?
Comment #156
effulgentsia commentedThanks, chx. Working on it... Will post soon.
Comment #157
effulgentsia commented#145 plus a test that demonstrates why it fails based on #152. Will discuss options on IRC.
Comment #159
effulgentsia commented#157 plus chx's refactoring of form.inc to solve the checkbox problem in a more logical way. Let's see what bot thinks. Then we can do code review.
Comment #161
effulgentsia commentedAdding back in a little code that was too aggressively removed.
Comment #163
effulgentsia commentedThis one should pass tests. Since this issue has gotten quite long, here's a (unsurprisingly, also long) summary. [EDIT: This summary has been edited from its original to be relevant to the patch in #187]
$form_state['input'] = array()from drupal_rebuild_form() is insufficient. The FormsRebuildTestCase demonstrates why. Basically, it's hard to know whether an input value of NULL means a submitted empty checkbox (whose value must be set to unchecked regardless of its default) or a newly added checkbox (whose value must be set to its default). Some other controls (multiple select) also have this annoyance.Thoughts?
Comment #164
effulgentsia commentedThis is a small improvement to #163. As I was writing the summary, I was somewhat bothered by requiring value callbacks to interpret FALSE as NULL. This patch fixes that by managing the NULL/FALSE conversion entirely within _form_builder_handle_input_element(). This means the API of a value callback is unchanged by this patch relative to HEAD. Within a value callback, $input=NULL means intentionally NULL input, and we never pass $input=FALSE to a value callback. This could mean that we can revert the "value callback" / "default value callback" split, and just pass $input=FALSE to a value callback to signify we want it to return a default value. But I kind of like the split, so left it in.
Comment #165
effulgentsia commentedRe: #164: "But I kind of like the split, so left it in."
I changed my mind. I do like the split, but let's leave it for a separate issue and keep this one focused.
Comment #166
effulgentsia commentedAgh. Sorry for all the spam, folks. This removes a couple more irrelevant hunks. If this passes bot, I'm done with it until there's feedback. I promise. For anyone joining here, summary is on #163.
Comment #167
effulgentsia commentedFixed a silly line of code being in the wrong place.
Comment #168
effulgentsia commentedI couldn't put it down. The use of FALSE to mean a different NULL than NULL just seemed wrong. Here's a patch that uses array_key_exists() to distinguish the two kinds of NULLs.
Comment #169
effulgentsia commentedTiny comment improvement. Once again, I re-promise this to be the last patch I upload tonight, as long as it passes bot. If you're joining here, issue summary is on #163.
Comment #170
rfay#169 behaves perfectly on all the Examples module tests that started this discussion. ("AJAX Example: Advanced Commands"). All of the ones that were not submit-driven (checkboxes, selects, textfields) were broken with current HEAD, and work perfectly with #169.
Thanks for the great work.
Comment #171
fagoChanges look good and it comes with thoroughly tests for the checkbox case. Good work!
minor:
form_test_storage_element_validate_value_cached sets $form_state['cache'] which isn't needed as the constructor already does it when $_REQUEST['cache'] is set.
cleanliness:
+ if (!$input_entry_exists && empty($form_state['rebuild']) && empty($form_state['programmed'])) {You use empty() for checking 'rebuild' and 'programmed', which have default values so the checks should be abbreviated by using a simple "!".
Comment #172
chx commentedUnbelievable great work! I have rewritten the comment for the form_state[input] handling. What an awesome idea to reuse _form_set_value here.
Comment #173
sun"special code"? We need to be more explicit here.
The message is clear after reading it twice, so we need to tweak the wording. Currently sounds like a "I wish" statement.
Grammar needs work here.
$form_state['rebuild'] is always defined.
Can we explain what's happening here?
Do we need to repeat this? If so, why?
I guess we should remove those assertions from this patch and move this topic into a separate issue that I want to create anyway.
This equally doesn't really belong to this issue/patch. It was required for my patch in the other issue and needs to move into the issue I'll create (regarding $form_build_id should stay the same throughout rebuilds). Not to be discussed in this issue.
I'm on crack. Are you, too?
Comment #174
chx commentedTwo quick notes: I missed the "Browsers do not submit $_POST entries " when writing mine, the two likely can and should be merged and grammarfixed.
Do we need to repeat this? No, it just got there in the many rerolls..
Comment #175
effulgentsia commentedWith feedback from #171 and #173.
Comment #176
effulgentsia commentedOops. Fixed small typo.
Comment #178
effulgentsia commentedBecause at suggestion of #171, changed code to assume 'rebuild' key exists in $form_state, had to add
$form_state = form_state_defaults()to a couple of tests. Also, had to add$form_state['cache'] = TRUE;back into form_test_storage_element_validate_value_cached() to fix test failure. Since a re-roll was needed, I cleaned up some form.inc comments too.Comment #179
effulgentsia commentedStill more minor comment tweaking. This one's final though, until there's more feedback.
Comment #181
effulgentsia commented#178 and #179 are identical with respect to field.form.inc, so I don't understand why #178 passed tests while #179 claims a syntax error. Anyway, this one is re-rolled for latest HEAD, so hopefully, bot won't choke on it.
Comment #182
sunwuhuh? Had to read that three times to understand ;)
This belonged to the other patch as well, not used in here.
I'll re-roll to fix those docs, then you review, and then you optionally tell me what I didn't get right. Deal?
Comment #183
effulgentsia commented@sun: deal! To save yourself some time, run the form storage test when you remove $form_state['cache'] = TRUE. It will fail unless you change the test.
Comment #184
fagoHm, I see why it fails. The form gets cached so the constructor isn't run the next time, thus the flag doesn't get set. Well this changed since we don't have $form['#cache'] anymore, what was cached. Thus this might be a bug dependent on we want constructors being able to set the flag. If we don't, it's fine. Anyway it's another issue and shouldn't hold up this one.
Comment #185
sunSo let's yell + kill me. :)
Comment #186
effulgentsia commented"elements that have new values" is either unclear or flat out incorrect. How about "When rebuilding a form, we can distinguish elements having NULL input from elements that were not part of the initially submitted form, and can therefore..."
This review is powered by Dreditor.
Comment #187
sunThanks!
Comment #188
effulgentsia commentedAwesome. Thanks to everyone for exploring and exposing flaws in much poorer solutions, until we finally reached one that makes sense. Since rfay, fago, and chx have already given their approval (#170-#172) except for minor issues that have all been addressed, I'm marking this RTBC.
Dries / webchick: issue summary is on #163.
Comment #189
chx commentedSo nice. Behold the power of working together, this is much better than any single of us including me could come up with. Oh joy.
Comment #190
dries commentedSpent 20 minutes with this patch, and it looked good. Committed to CVS HEAD.
Comment #191
fagoAwesome, thanks all!
Comment #192
effulgentsia commentedSo, now that we solved one more thing that was making AJAX confusing, I have a recommendation for another one: #649628: Make it easier to write AJAX-enabled forms that fully work with JavaScript disabled too. I think that issue will really help developers understand that submit logic and form rebuilding logic need to live within the appropriate form function, and not within #ajax['callback'], and I would really appreciate a review from anyone who's interested in that. Thanks!
Comment #194
yched commentedAny chance this changed the behavior of #disabled elements ?
I'm confused by the behavior I see in #687572: When disabled textfields are submitted, their values are forced to empty string instead of the default value