prototypejs / prototype

Prototype JavaScript framework
http://prototypejs.org/
Other
3.54k stars 639 forks source link

Form.serialize and multiple submit buttons #265

Closed jwestbrook closed 9 years ago

jwestbrook commented 9 years ago

previous lighthouse ticket #672 by Terry Riegel


If my markup looks like this...

<form action="blah">
 <input type="text" name="var1">
 <input type="submit" name="mybutton" value="Submit">
 <input type="submit" name="mybutton" value="Exit">
</form>

and I do a Form.serialize on it then I get something like this

var1=&mybutton=Submit&mybutton=Exit

It seems to me Form.Serialize should return either...

var1=&mybutton=Submit

or...

var1=&mybutton=Exit

Depending on the button that was used to do the submit. As it is now when I submit the serialized form the server side has no idea of which button was actually clicked.

Here is a hack I have done to circumvent the problem but it is ugly...

Updated October 5th, 2009 - A bit more elegant (if thats even possible :))

  // Part 1 of 3, This is a multipart hack for prototype.
  // First we capture all clicks on any input submit elements. This is part of an onload event handler
  var inputArray =  document.getElementsByTagName('input');
  for (i=0; i < inputArray.length; i++) {
   if (inputArray[i].type == "submit") {
    inputArray[i].isclicked=false;
    inputArray[i].onclick=addClassToSubmitWhenClicked;
   }
  }

  // Part 2 of 3, This is a multipart hack for prototype.
  // Next we define what happens when a submit button is clicked... We set isclicked.
  function addClassToSubmitWhenClicked(e) {
   var el;
   if (!e) var e = window.event;
   if (e.target) el = e.target; else if (e.srcElement) el = e.srcElement;
   if (el.nodeType == 3) el = el.parentNode;
   el.isclicked=true;
  }

  // Part 3 of 3, This is a multipart hack for prototype.
  // Lastly we add our own serializer that disables all buttons that were not clicked
  // Then we use the default prototype serializer using the fact that it ignores disabled buttons
  // Update added code to acount for input type="button"
  function riegelSerialize(a) {
   var getEls=Form.getElements(a);
   for (i=0; i < getEls.length; i++) {
     getEls[i].savetate=getEls[i].disabled;
     if (getEls[i].type=='submit' && getEls[i].isclicked == false) {
      getEls[i].disabled=true;
     }
     if (getEls[i].type=='submit' && getEls[i].isclicked ) {
      window.status=getEls[i].value+' in process...';
      window.waiting=window.status;
     }
     if (getEls[i].type=='button') {
      getEls[i].disabled=true;
     }
   }
   var getserial=Form.serialize(a);
   for (i=0; i < getEls.length; i++) {
     getEls[i].disabled=getEls[i].savestate;
     getEls[i].isclicked=false;
   }
   return getserial;
  }
jwestbrook commented 9 years ago

Andrew Dupont May 12th, 2009 @ 06:34 AM

For now let's pretend "2.0" means "sometime in the future but probably before 2.0."

jwestbrook commented 9 years ago

wakeboardguy (at gmail) May 14th, 2009 @ 08:52 PM

Where would I place this hack?

jwestbrook commented 9 years ago

wakeboardguy (at gmail) May 14th, 2009 @ 09:11 PM

To clarify, i meant part 1 mostly. Im a complete noob when it comes to prototype, so if you could give some more specifics that would be fantastic.

jwestbrook commented 9 years ago

Terry Riegel May 14th, 2009 @ 09:15 PM

Well... Part 1 would need to be part of an onload event like say the body onload event. Part 2 and 3 would just need to be in some script on your page. To utilize the hack you would simply replace an calls like... Form.serialize(a) with riegelserialize(a) Does that make sense? Terry

jwestbrook commented 9 years ago

Terry Riegel May 14th, 2009 @ 09:21 PM

something like this...

jwestbrook commented 9 years ago

Terry Riegel May 14th, 2009 @ 09:22 PM

something like this...

<script>
  document.observe("dom:loaded", function(){
  // Apply unobtrusive Javascript to form buttons
  // Part 1 of 3, This is a multipart hack for prototype.
  // First we capture all clicks on any input submit elements.
  var inputArray =  document.getElementsByTagName('input');
  for (iItem=0; iItem < inputArray.length; iItem++) {
    if (inputArray[iItem].type == "submit") {inputArray[iItem].observe('click',addClassToSubmitWhenClicked);}
  }
 });

  // Part 2 of 3, This is a multipart hack for prototype.
  // Next we define what happens when a submit button is clicked... We add a classname 'active'
  function addClassToSubmitWhenClicked(event) {
    var element = Event.element(event);
    element.addClassName('active');
  }

  // Part 3 of 3, This is a multipart hack for prototype.
  // Lastly we add our own serializer that disables all buttons that were not clicked
  // Then we use the default prototype serializer using the fact that it ignores disabled buttons
  function riegelSerialize(a) {
   var getEls=Form.getElements(a);
   for (iItem=0; iItem < getEls.length; iItem++) {
     if (getEls[iItem].type=='submit' && getEls[iItem].className.indexOf('active')==-1) {
      getEls[iItem].disabled=true;
     }
   }
   var getserial=Form.serialize(a);
   return getserial;
  }

</script>
jwestbrook commented 9 years ago

Joe Kuan June 10th, 2009 @ 08:11 PM

Why not use a hidden field in the form and onclick in the submit button to record the action. onclick='this.form.hidden_field.value = this.value;' Then it is simply var obj = $('form').serialize(true); obj['submit'] = obj['hidden_field']; Or am I missing something? Joe

jwestbrook commented 9 years ago

Terry Riegel June 10th, 2009 @ 08:21 PM

Seems like your proposal would work also, but doesn't seem to be generic enough. It may be able to be generalized, but I would like to see a complete solution fleshed out before weighing in on the merits of one over the other. Terry

jwestbrook commented 9 years ago

Joe Kuan June 10th, 2009 @ 09:46 PM

http://joekuan.wordpress.com/2009/06/10/prototype-simple-javascript-workaround-for-forms-with-multiple-submit-buttons/ Hope this helps Joe

jwestbrook commented 9 years ago

Terry Riegel June 11th, 2009 @ 01:55 PM

Very nice writeup. If you don't mind adding javascript to your markup and also adding a hidden field to every form then your solution is workable. I was trying to create a solution that didn't require any changes to the markup or the server. What version of prototype are you using. In your example it says the serialize just returns the first button, in the copy I have it returns all of the buttons. i.e. submit=Save&submit=Exit Either way I hope this little bug gets fixed soon. I don;t care how its fixed as long as it is done in an unobtrusive way and doesn;t require me to fiddle with my markup. :)

jwestbrook commented 9 years ago

ronin-37489 (at lighthouseapp) June 14th, 2009 @ 09:13 PM

I don't think it makes sense to adjust Form.serialize or any prototype core code to accommodate this. This isn't a bug, and making doing this enhancement will only clutter up the code. Form.serialize is doing it's job perfectly: it is serializing all the elements of the form regardless of what it is. Terry, with all due respect, your form code is the one that's the odd case. People typically don't use a submit button to "Exit" the form. HTML submit buttons are for submitting the form (ie. giving the server your data to do stuff with it). Your "Exit" should be a link. Anyone who wishes to do what you're doing should and must do the hackery (using what you suggested or anyone of the other good ideas on this ticket) themselves. My 2 cents. Thanks for reading :-)

jwestbrook commented 9 years ago

Terry Riegel June 15th, 2009 @ 03:20 PM

To Ngam, What? Your comments do not make any sense. Are you suggesting that web browsers got it wrong on how form submissions are to take place. Because I have never seen a single browser submit a form the way Form.serialize does. So when you say prototype gets it right then you must be aware of the fact that they are not doing it the way all other browsers do. I have not read the RFC on the subject but it seems to me if all the web browsers agree then they probably have done it right. So when prototype does it differently then I am not sure how you can defned that as correct behavior. The big elephant in the room is that Form.serialize is supposed to do what the browser would do before submitting a form. In fact it is not doing that. It is doing something quite different when it comes to Submit buttons. Your assumtion that a particular button should never be used is shortsighted. I can think of several cases where a "Exit" or "Cancel" button would also need the form to be sunbmitted to be processed correctly. But if your hang up is with the word "Exit" then what if the buttons were "Previous" and "Next" then the problem is still there. How does the server know if the user hit "Next" or if the user hit "Previous"? If the form is submitted by the browser then the server knows what button was pressed. If the form is submitted using Form.serialize then the server DOES NOT KNOW what button was pressed. Or what if the two buttons are "Update Cart" and "Checkout" then the server needs to know what the user wants to do? The fact that I have submitted "hackery" is irrelevant to the problem. The problem exists and I have to create a "hack" to get proper behaviour. This is a bug and does need to be fixed. I am not suggesting that my sokution is the best or the most elegant just that it is a hack to the problem.

jwestbrook commented 9 years ago

Terry Riegel June 15th, 2009 @ 04:19 PM

To Ngan Pham, Sorry I misspelled your name in my last post. I'll try to stick with cut and paste in the future. :) Terry

jwestbrook commented 9 years ago

krsmes July 16th, 2009 @ 11:48 PM

Tag changed from “form” to “form, grails, serialize” @ronin... this is a problem in cases where you can't control the server. A normal (non-Ajax) form with multiple buttons will only submit the value of the button that was clicked, not an array of all button values. But when serializing and submitting the form via Ajax using prototype the server gets an array of all button values and has no way to identify which one is the 'real' value. Grails (www.grails.org) uses prototype and this is long standing existing "bug" where remote forms can't be mapped to the proper action: http://www.grails.org/Ajax: "There is a problem with Prototype ... that means Grails can not determine which button has been pressed when a formRemote tag includes more than one submit button." They have a workaround that works in some cases, but I think it is a reasonable to have a version of serialize that only contains one value for 'submit' instead of an array.

jwestbrook commented 9 years ago

Rafał Michalski October 10th, 2009 @ 12:40 AM

I think this issue is partially addressed in 1.6.1: see docs http://api.prototypejs.org/dom/form.html#serialize-class_method however there is still problem with submit buttons of the same name, where only the first one is included. so first we need to change submit button names:

<form id="myform" action="javascript:void(0)">
 <input type="text" name="var1">
 <input type="submit" name="mybuttonsubmit" value="Submit">
 <input type="submit" name="mybuttonexit" value="Exit">
</form>

then we can easily write something like:

(function() {
    var submit;
    $('myform').observe('submit', function(event) {
        alert(
            Event.element(event).serialize({ hash: false, submit: submit.name })
        ); //or do something else ...
    }).select('input[type="submit"]').invoke('observe', 'click', function(event) {
        submit = Event.element(event);
    });
})();

If we wanted to keep submit names doubled, then Form#serializeElements needs enhancement. It should be modified to allow checking not only for the name of submit input element but also it's value something like: { submit: $('my_submit_button_id') } The Form#serializeElements should check internally if submit value is a String ( key == submit ) or is an Element ( element == submit ). so we could write code like this:

(function() {
    var submit;
    $('myform').observe('submit', function(event) {
        alert(
            Event.element(event).serialize({ hash: false, submit: submit })
        ); //or do something else ...
    }).select('input[type="submit"]').invoke('observe', 'click', function(event) {
        submit = Event.element(event);
    });
})();
jwestbrook commented 9 years ago

Rafał Michalski October 10th, 2009 @ 01:05 AM

So the change to Form#serializeElements would be VERY simple:

if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
      submit !== false && (!submit || key == submit || element == submit) && (submitted = true)))) {
jwestbrook commented 9 years ago

Terry Riegel June 1st, 2010 @ 06:41 PM

What is the status of this ticket? Will form.serialize do submissions the way a web browser does? The proposed solutions (except my hack) so far require additional markup or retooling existing forms and scripts to get what a browser does naturally.

jwestbrook commented 9 years ago

mhitza July 21st, 2010 @ 08:17 AM

Importance changed from “” to “” My opinion as well is that modifications to Javascript shouldn't be done. Instead you should do a workaround in your application.

var serialized = $('form').serialize();
serialized = serialized.gsub(/submitWhichYouDoNotWant=.*?&/, '');
jwestbrook commented 9 years ago

Terry Riegel July 21st, 2010 @ 01:21 PM

My opinion as well is that modifications to Javascript shouldn't be done.

Thats to bad. All browsers serialize forms the same way. Prototype does it differently. So server side applications should have logic that understands what browsers do and alternate logic that understands how prototype does it.

jwestbrook commented 9 years ago

mhitza July 21st, 2010 @ 07:43 PM

You are right Terry, I've misunderstood you on the first read; maybe due to late night work. I would suggest that the best approach would be to name the two submit buttons differently. Than according to the current implementation you can just do:

var serialized = $('form').serialize({ 'submit': 'mybutton' });
jwestbrook commented 9 years ago

Terry Riegel July 21st, 2010 @ 11:20 PM

Sure, there are several things that could be done to sidestep the bug including what I have already done with riegelserialize() and other suggestions found in this thread. But it still leaves us with the same situation. I reported it here as a bug because prototype does things differently than what browsers do. As an interesting resource you can see how prototype does things compared to other libraries... http://www.malsup.com/jquery/form/comp/ With my browser (Safari) the comparison shows prototype's serialize deviating in three areas. My feeling is that prototype's serialize "should" work EXACTLY the same way a browser does. This allows for progressive enhancement. I have written my own serialize code that addresses this issue and some of the other issues on the malsup site, but I love prototype and love building stuff with it, but I would like to have the confidence that serialize isn't doing things differently. Anyhow sorry for the rant.

jwestbrook commented 9 years ago

KKI July 26th, 2010 @ 02:01 AM

Hi Terry and all ! You write, and malsup site and JQuery site to, that JQuery serialize only the clicked submit button. But there is no such code in jquery-1.4.2.js Submit buttons are not serialized, if the form is submitted or not. If you want to submit only the pressed button, you have to call serialize with the button key or with the button element (and for that, you have to modify serializeElements as in Rafał Michalski October 10th, 2009 @ 01:05 AM post

jwestbrook commented 9 years ago

Andrew Kaspick August 9th, 2010 @ 08:59 PM

Just ran into this issue with prototype. Normal form submission works perfectly with multiple submit buttons that have different names, but once it's changed to submit via ajax with prototype I always get the first button in the posted params. Summary... bug in prototype. I'd like to see a fix for this in the next release if possible. I'll have to either hack in a temporary solution for now or move to jquery for this since it sounds like jQuery gets this right. :/

jwestbrook commented 9 years ago

Andrew Dupont August 15th, 2010 @ 02:39 AM

Assigned user set to “Andrew Dupont” I'm in favor of the approach that adds a submit option to Form.Element#serialize. Anyone feel like writing the patch? (And, if you're feeling extra-nice, perhaps also a simple drop-in script that existing Prototype users can use to monkeypatch the existing behavior?)

jwestbrook commented 9 years ago

Andrew Kaspick August 15th, 2010 @ 08:28 AM

Just an fyi, we're adding some support to rails.js for this in RoR until it's supported directly in Prototype... https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/5356

jwestbrook commented 9 years ago

Andrew Dupont August 23rd, 2010 @ 12:18 AM

That patch looks good. Marking this for inclusion in 1.7.1.

jwestbrook commented 9 years ago

Clément Hallet January 19th, 2011 @ 11:08 AM An other trick, trying to fully simulate a default browser behavior :

<form action="blah">
 <input type="text" name="var1">
 <input type="submit" name="mybutton" value="Submit">
 <input type="submit" name="mybutton" value="Exit">
</form>
form.observe('submit', function(event) {
    event.preventDefault();
    new Ajax.Updater(form, form.getAttribute('action'), {
        method: form.getAttribute('method'),
        parameters: form.serialize({submit: event.explicitOriginalTarget.name})
    })

});

event.explicitOriginalTarget is the button from which the submit occurs. It could be cases where there is no button.

jwestbrook commented 9 years ago

Clément Hallet January 25th, 2011 @ 10:20 AM

The previous trick worked only with Firefox because explicitOriginalTarget is availbale only under Gecko based browser. An alternate syntax working with all browsers would be :

var form = $('myform')

// catch clicks on submit
form.observe('click', function(event) {
    var submit = event.findElement('input[type=submit]');
    if(submit == undefined) {
        return;
    }
    // temporary disable other submits
    form.select('input[type=submit]').each(function(elem) {
        if(elem != submit) {
            elem.disabled = true;
        }
    });
});

// catch submit
form.observe('submit', function(event) {
        // data contains only the clicked submit
        var data = form.serialize();

        // and re-enable the disabled submits
    form.select('input[type=submit]').each(function(elem) {
        elem.disabled = false;
    });
});
savetheclocktower commented 9 years ago

This is supported with the submit option of Form.serialize.