はじめに

formをsubmitすると、formに属する全要素のデータがサーバに送られる。
特定の要素を除外してformをsubmitすることは通常はできないため、このような目的には、本来であればformを複数に分割して各処理ボタンを押した時に必要な要素のみサーバに送られるようにすべきである。

特定の要素を除いてsubmitしたいという目的が無くても、通信データ量の観点からみると、やはり適切に分割された複数のformがある方がいい。

とはいっても何らかの事情で、全部入りのform一個だけしかない場合などでも、一部のデータを送らないようにするためにはどうすればいいか考えてみた。

Ajaxによる解決

解決方法としてまず考えられるのが、Ajax通信でpostする方法。

jquery#serialize, serializeArrayでformを文字列にシリアライズするときに、特定のnameを除く方法 で書いたように、postするデータを作成するときに特定の要素を省いてあげればいい。

しかし、アプリのつくり上、Ajax通信ではなく通常のsubmitでpostしなければならないとなった場合、通常のJavaScript、HTMLの仕組みでは対応できない。

jQueryでformを作成してPOST(submit)する で、動的にformを作成すればいいのかとヒントを得たので作成してみる。

使用したメインのJavaScript関数は cloneNode(include_child) で、同じ機能が .clone( [withDataAndEvents ] ) という関数でjQueryにあったので、jQuery無と有で両方を試した。

jQuery無のバージョン

function submit(excludeIds) {
    var srcNode = document.getElementsByTagName("form")[0];
    // formをコピーする。引数にtrueを入れているため、子要素を含む。
    var copyNode = srcNode.cloneNode(true);

    // 除外elementリスト
    var excludes = [];
    for (var i = 0; i < copyNode.length; i++) {
        // 複製したformに含めたくないものを除外elementリストに詰める。
        if (excludeIds.indexOf(copyNode[i].id) > -1) {
            excludes.push(copyNode[i]);
        }

        // IDは一意であるべきなので、複製したものには独自のネーミングルールを適用する。
        if (copyNode[i].id != "undefined" && copyNode[i].id != "") {
            copyNode[i].id += "copy_";
        }

        // ブラウザによって、radioとselect等の選択した値がコピーされないので、対処
        // 以下はIE8で動作確認済み
        if (typeof(copyNode[i].checked) != 'undefined') {
            copyNode[i].checked = srcNode[i].checked;
        }
        if (copyNode[i].tagName.toLowerCase() == 'select') {
            for (var si = 0; si < srcNode[i].options.length; si++) {
                copyNode[i].options[si].selected = srcNode[i].options[si].selected;
            }
        }
    }
    // 除外elementすべてを取り除く
    for (var i = 0; i < excludes.length; i++) {
        removeElement(excludes[i]);
    }

    // submitのためだけにコピーしているので、非表示にする。
    copyNode.style.display = "none";
    // コピーしたformをbodyに追加
    document.body.appendChild(copyNode);

    copyNode.submit();
}

function removeElement(node) {
    node.parentNode.removeChild(node);
}

参考/引用

removeElement(node)の出典元:http://stackoverflow.com/questions/13763/how-do-i-remove-a-child-node-in-html-using-javascript
cloneNodeで選択状態をうまくコピーできない問題の参考元:http://www.programming-magic.com/20071205185601/

jQuery有のバージョン

// Textarea and select clone() bug workaround | Spencer Tipping
// Licensed under the terms of the MIT source code license
// Motivation.
// jQuery's clone() method works in most cases, but it fails to copy the value of textareas and select elements. This patch replaces jQuery's clone() method with a wrapper that fills in the
// values after the fact.
// An interesting error case submitted by Piotr Przyby?: If two <select> options had the same value, the clone() method would select the wrong one in the cloned box. The fix, suggested by Piotr
// and implemented here, is to use the selectedIndex property on the <select> box itself rather than relying on jQuery's value-based val().
(function(original) {
    jQuery.fn.clone = function() {
        var result = original.apply(this, arguments),
            my_textareas = this.find('textarea').add(this.filter('textarea')),
            result_textareas = result.find('textarea').add(result.filter('textarea')),
            my_selects = this.find('select').add(this.filter('select')),
            result_selects = result.find('select').add(result.filter('select'));

        for (var i = 0, l = my_textareas.length; i < l; ++i) $(result_textareas[i]).val($(my_textareas[i]).val());
        for (var i = 0, l = my_selects.length; i < l; ++i) {
            for (var j = 0, m = my_selects[i].options.length; j < m; ++j) {
                // patch for multiselect
                //         if (my_selects[i].options[j].selected === true) {
                //                  result_selects[i].options[j].selected = true;
                //         }
                result_selects[i].options[j].selected = my_selects[i].options[j].selected;
            }
        }
        return result;
    };
})(jQuery.fn.clone);

function submit(excludeIds) {
    var srcNode = $("form:first");
    var copyNode = srcNode.clone(true);

    $.each(excludeIds, function(index, excludeId) {
        var excludes = copyNode.find("#" + excludeId);
        $.each(excludes, function(index, elem) {
            $(elem).remove();
        });
    });
    copyNode.find("[id]").map(function (index, elem) {
        var idHolder = $(elem);
        idHolder.attr("id", "copy" + idHolder.attr("id"));
    });

    copyNode.css("display", "none");
    copyNode.appendTo(document.body);

    copyNode.submit();
}

参考/引用

cloneの拡張コード出典元:https://github.com/spencertipping/jquery.fix.clone
cloneの拡張コード参考元:http://stackoverflow.com/questions/742810/clone-isnt-cloning-select-values

jQuery版も処理の流れは、jQuery無のバージョンと同じだが、clone特有の選択状態をうまくコピーできないバグを修正したコード(jquery.fix.clone)があったので、それを利用した。
ただ jquery.fix.clone は、select要素にmultiple属性が指定されている場合(multiselect)を考慮していないようだったので、一部修正を加えた。
multiselectの場合の具体的な影響は以下の通り。(IE8で検証)

  1. ある値(A)を選択して、サーバに送る。
  2. 画面がリフレッシュし、selectのAの初期値がselectedになる。
  3. Aの選択をクリアし、別の値(B)を選択する。
  4. cloneすると、Aの初期値はselectedのためclone先もselected、Bは(selected == true)の条件に当てはまるのでclone先にはselectedが適用される。
  5. 正解はBのみselectedなのに、AもBもselectedになる。

jQuery版をここからさらに改良するとすれば、submit(excludeIds)の引数部分がポイントとなる。 今回作成した関数のinterfaceを、jQuery無のバージョンと有のバージョンで合わせるため(引数がidの文字列の配列)に、jQuery版のコードで find("#" + excludeId) と汚いコードになったが、別にinterfaceを合わせる必要はない。 submit(excludeSelectors)としてあげれば、jQuery的な汎用性のある関数になる。