FORM 要素の部品の要素の値を object型にして返す

必要になって書いてみた。プログラム書くのに時間がかかりすぎ。

使い方のメモ

let
  f0 = new ExpForm (document.forms[0]),
  f1 = new ExpForm (document.forms[1]),
  obj = f0.getValue ();

//form0からform1に複写
f1.setValue (obj);
  • FORM要素の対象となる部品は、INPUT, TEXTAREA, SELECT とする。
  • type属性が['submit', 'reset', 'button', 'image', 'fieldset', 'file']のものは除外する
  • setValue()時に、もし name属性が同じ部品で、type属性が数種類あった場合には、以下の優先順に並び替えて指定値を代入する
    (プログラム的には、その指定値(配列)のバッファから設定するたびに削除しながらループしている)
select-multiple > checkbox > select-one > radio > その他
  • getValue()の時に type属性が select-multiple, checkbox が含まれる場合、必ず配列にして返す
  • null での設定は、空白文字("")に置き換える

追加修正

  • setValue() を実行する時 checkbox の設定が追加上書きされていたので、追加されないように修正(新規に設定)。
  • setValue() を実行する時、同名の部品の数より、設定しようとする値の配列の数が少ない場合、途中で終了していたので最後までループするように修正。その場合は、空文字を代入することにした

サンプルとして同じフォームを2つ作りボタンでコピーしてみる

<!DOCTYPE hrml>
<head>
  <meta charset="utf-8">
  <title>ExpForm Test Code</title>
  <style>
th { text-align: left; background: #ddd;}
  </style>
<body>

<form id="fm1">
  <h3>原本のフォーム</h3>
  <table border="1">
    <thead>
      <tr>
        <th>Elements Type
        <th>name
        <th>Default Values
    <tbody>
      <tr>
        <th>text
        <td>aaa
        <td><input type="text" name="aaa" value="0">

      <tr>
        <th>checkbox
        <td>bbb
        <td>
          <label><input type="checkbox" name="bbb" value="1">Val=1</label>
          <label><input type="checkbox" name="bbb" value="2">Val=2</label>

      <tr>
        <th>radio
        <td>ccc
        <td>
          <label><input type="radio" name="ccc" value="3">Val=3</label>
          <label><input type="radio" name="ccc" value="4" checked>Val=4</label>

      <tr>
        <th>select
        <td>ddd
        <td>
          <select name="ddd">
            <option value="5">5
            <option value="6" selected>6
          </select>

      <tr>
        <th>select-multiple
        <td>eee
        <td>
          <select name="eee" multiple>
            <option value="7" selected>7
            <option value="8">8
            <option value="9" selected>9
          </select>

      <tr>
        <th>hidden
        <td>fff
        <td>
          <input type="hidden" name="fff" value="10">

      <tr>
        <th>textarea
        <td>ggg
        <td>
          <textarea name="ggg" cols="30" rows="3">11</textarea>

      <tr>
        <th>All
        <td>hhh
        <td>
          <input type="text" name="hhh" value=""><br>
          <textarea name="hhh" cols="10" rows="3"></textarea><br>
          <label><input type="checkbox" name="hhh" value="3">Val=3</label>
          <label><input type="checkbox" name="hhh" value="4">Val=4</label><br>
          <select name="hhh" multiple>
            <option value="5">5
            <option value="6">6
            <option value="7">7
          </select><br>
          <input type="text" name="hhh" value="">

      <tr>
        <th>submit, reset, button
        <td>za[a-c]
        <td>
          <input type="button" name="zaa" value="copy_values">
          <input type="submit" name="zab" value="submit">
          <input type="reset" name="zac" value="reset">
    </tbody>
  </table>

</form>


<form id="fm2">
  <h3>コピーされるフォーム<h3>
  <table border="1">
    <thead>
      <tr>
        <th>Elements Type
        <th>name
        <th>Default Values
    <tbody>
      <tr>
        <th>text
        <td>aaa
        <td><input type="text" name="aaa" value="0">

      <tr>
        <th>checkbox
        <td>bbb
        <td>
          <label><input type="checkbox" name="bbb" value="1">Val=1</label>
          <label><input type="checkbox" name="bbb" value="2">Val=2</label>

      <tr>
        <th>radio
        <td>ccc
        <td>
          <label><input type="radio" name="ccc" value="3">Val=3</label>
          <label><input type="radio" name="ccc" value="4">Val=4</label>

      <tr>
        <th>select
        <td>ddd
        <td>
          <select name="ddd">
            <option value="5">5
            <option value="6">6
          </select>

      <tr>
        <th>select-multiple
        <td>eee
        <td>
          <select name="eee" multiple>
            <option value="7">7
            <option value="8">8
            <option value="9">9
          </select>

      <tr>
        <th>hidden
        <td>fff
        <td>
          <input type="hidden" name="fff" value="10">

      <tr>
        <th>textarea
        <td>ggg
        <td>
          <textarea name="ggg" cols="30" rows="3"></textarea>

      <tr>
        <th>All
        <td>hhh
        <td>
          <input type="text" name="hhh" value=""><br>
          <textarea name="hhh" cols="10" rows="3"></textarea><br>
          <label><input type="checkbox" name="hhh" value="3">Val=3</label>
          <label><input type="checkbox" name="hhh" value="4">Val=4</label><br>
          <select name="hhh" multiple>
            <option value="5">5
            <option value="6">6
            <option value="7">7
          </select><br>
          <input type="text" name="hhh" value="">

      <tr>
        <th>submit, reset, button
        <td>za[a-c]
        <td>
          <input type="button" name="zaa" value="copy_values">
          <input type="submit" name="zab" value="submit">
          <input type="reset" name="zac" value="reset">
    </tbody>
  </table>

</form>

<script>
{
  const
    TARGET_PARTS = ['INPUT', 'TEXTAREA', 'SELECT'],//対象となる form要素の部品
    EXCLUDE_TYPE = ['submit', 'reset', 'button', 'image', 'fieldset', 'file'];//部品から除外する


  //引数の要素が formの部品なのか?
  function isParts (e) {
    return (e)
    ? (TARGET_PARTS.includes (e.tagName))
      ? ! EXCLUDE_TYPE.includes (e.type)
      : false
    : false;
  }


  //引数がノードリストなのか?
  function isNodeList (arg) {
    let rst = false;

    if (arg)
      if (! ('tagName' in arg))
        if ('length' in arg)
          if ('item' in arg)
            rst = arg.item instanceof Function;

    return rst;
  }


  //要素から使用可能な要素を取り出して配列化する
  function pickupParts (es) {
    return (isNodeList (es) ? Array.from (es): [es]).filter (isParts);
  }


  //引数の型を返す
  function typeOf (arg) {
    return Object.prototype.toString.call (arg).slice (8, -1);
  }


  //配列の中を文字列に変換する
  function insideArrayToString (ary) {
    return ary.map (v => String (v));
  }


  //OPTION@SELECTの値を取得する
  function getOptionValue (opt) {
    return opt.hasAttribute ('value')
    ? opt.value
    : opt.text;
  }


  //同じ名前を持つ複数の部品がある場合並び替える
  function sortByType (es) {
    let
      select_multiple = [ ],
      checkbox = [ ],
      select_one = [ ],
      radio = [ ],
      other = [ ];

    es.forEach (e => {
      switch (e.type) {
      case 'select-multiple' : select_multiple.push (e); break;
      case 'checkbox' : checkbox.push (e); break;
      case 'select-one' : select_one.push (e); break;
      case 'radio' : radio.push (e); break;
      default : other.push (e); break;
      }
    });

    return [ ].concat (select_multiple, checkbox, select_one, radio, other);
  }


  /* checkbox をセットする */
  function setCheckbox (target = null, ...arg) {
    if (null === target)
      throw new Error ('要素がありません');

    let
      rst = [ ];// checked できなかったリストを返す

    arg.forEach (a => {
      let values = [ ];

      switch (typeOf (a)) {
      case 'Number' : case 'String' : values = [a]; break;
      case 'Array'                  : values = a; break;
      default                       : throw new Error ('無効な引数の型が指定されました'); break;
      }

      let
        idx = values.indexOf (target.value),
        flag = -1 < idx;

      if (target.checked = flag)
        values.splice (idx, 1);

      rst = rst.concat (values);//selected できなかった値をリスト化
    });

    return rst;
  }


  //SELCT要素の optionを設定
  function setSelected (target = null, ...args) {
    if (null === target)
      throw new Error ('要素がありません');

    let
      rst = [ ],// selected できなかったリストを返す
      opts = target.options;

    args.forEach (arg => {
      let values = [ ];

      switch (typeOf (arg)) {
      case 'Number' : case 'String' : values = [arg]; break;
      case 'Array'                  : values = arg; break;
      default                       : throw new Error ('無効な引数の型が指定されました'); break;
      }

      for (let i = 0, opt; opt = opts[i]; i++) {
        if (values.length) {
          let
            val = opt.hasAttribute ('value') ? opt.value: opt.text,
            idx = values.indexOf (val),
            flag = -1 < idx;

          if ((opt.selected = flag))
            values.splice (idx, 1);
        }
      }

      rst = rst.concat (values);//selected できなかった値をリスト化
    });

    return rst;
  }



  //部品の値を返す
  function value (es) {
    let
      singleFg = true,
      rst = [ ],
      i, e;

    es = pickupParts (es);
    if (! es.length)
      throw new Error ('指定された部品要素がありません');

    for (i = 0; e = es[i]; i++) {
      let type = e.type;
      if (singleFg) {
        if ('select-multiple' === type || 'checkbox' === type)
          singleFg = false;
      }

      switch (type) {
      case 'select-multiple' :
        Array.from (e.options)
          .forEach (opt => opt.selected ? rst.push (getOptionValue (opt)): null);
        break;

      case 'select-one' :
        rst.push (getOptionValue (e.options[e.selectedIndex]));
        break;

      case 'checkbox' :
      case 'radio' :
        if (e.checked)
          rst.push (e.hasAttribute ('value') ? e.value: 'on');
        break;

      default :
        rst.push (e.value);
        break;
      }
    }

    if (1 < rst.length)
      singleFg = false;

    return singleFg ? rst[0]: rst;
  }



  // formの部品に値をセットする
  function setVal (es, vals) {

    es = sortByType (pickupParts (es)); //部品はソート
    if (0 === es.length)
      throw new Error ('指定された部品要素がありません');
    if (null === vals)
      vals = '';

    switch (typeOf (vals)) {
    case 'Number' : case 'String' : vals = [vals]; break;
    case 'Array'                  : break;
    default :
      throw new Error ('無効な引数の型が指定されました');
      break;
    }

    vals = insideArrayToString (vals);

    for (let i = 0, e; e = es[i]; i++) {

      switch (e.type) {

      case 'select-multiple' : case 'select-one' :
        vals = setSelected (e, vals);
        break;

      case 'checkbox' : case 'radio' :
        vals = setCheckbox (e, vals);
        break;

      default :
        e.value = value.length ? vals.shift (): '';
        break;
      }
    }
  }



  class ExpForm {

    /*
      引数を省略すると documentにある最初の form要素が対象となる
    */
    constructor (form = document.forms[0]) {
      if ('FORM' !== form.tagName)
        throw new Error ('FORM要素ではありません');

      this.form = form;
    }

    //__________________________

    /*フォームの部品の値を取得する
      引数の型により戻り値の型が変化する
      number => 部品番号の名前を元に値を返す
      string => 部品名を元に値を返す
      array  => 配列の値で指定された番号もしくは部品名で値を取得し、それらの配列順で値を返す
      object => オブジェクトのキーを元に値を取得し、オブジェクトで返す
    */
    getValue (arg = this.getEmptyObject ()) {
      let
        len = this.form.elements.length,
        e, es, name, rst;

      switch (typeOf (arg)) {


      case 'Number' :
        if (isNaN (arg))
          throw new Error ('無効な数値が指定されました');
        if (arg < 0)
          arg += len;

        e = this.form.elements[arg];//番号の要素の名前と同じ全要素を対象とする
        if (! e)
          throw new Error ('指定された部品要素がありません');
        if (! (name = e.name))
          throw new Error ('フォームの部品要素に名前がありません');

        rst = value (this.form.elements[name]);
        break;

      case 'String' :
        rst = value (this.form.elements[arg]);
        break;

      case 'Array' :
        rst = arg.map (this.getValue, this);
        console.log ("rstary=",rst);
        break;

      case 'Object' :
        rst = Object.keys (arg).reduce ((a, b) => (a[b] = this.getValue (b), a), {});
        break;

      default :
        throw new Error ('引数の型が無効です');
      }


      return rst;
    }



    //__________________________

    /*
    form の各部品に値をセットする

    同名複数の部品がある場合 select-multiple, checkbox, その他の部品順に値を代入していく
    代入された値は、配列から削除され余ったものは捨てられる

    使い方
    書式A setValue ('aaa', 123); //=>name属性が 'aaa'の最初の要素に 123を代入する
      B setValue ('aaa', [123, 456, 789]); //=>'aaa'の複数の要素に代入する
      C setValue ({aaa: 123, bbb:[456,789]}); オブジェクト型の構造のまま代入する
    */

    setValue (arg, arg2) {
      if (! arguments.length)
        throw new Error ('引数の数が足りません');

      let
        es = this.form.elements,
        name;

      switch (typeOf (arg)) {


      case 'Number' :
        if (isNaN (arg))
          throw new Error ('無効な数値が指定されました');
        if (arg < 0)
          arg += len;

        e = es[arg];//番号の要素の名前と同じ全要素を対象とする
        if (! e)
          throw new Error ('指定された部品要素がありません');
        if (! (name = e.name))
          throw new Error ('フォームの部品要素に名前がありません');

        rst = setVal (es[name], arg2);
        break;


      case 'String' :
        setVal (es[arg], arg2);
        break;


      case 'Array' :
        rst = arg.map ((n, i) => this.setValue (n, arg2[i]));
        break;


      case 'Object' :
        Object.keys (arg).map ((key, i) => setVal (es[key], arg[key]));
        break;


      default :
        throw new Error ('引数の型が無効です');
      }
    }


    //__________________________

    //form要素の部品の(重複しない)nameリストを返す
    getNames () {
      return [... new Set (Array.from (this.form.elements).filter (isParts).map (e => e.name))];
    }

    //__________________________

    //nameをキーとする空のオブジェクトを作る
    getEmptyObject (obj = { }, defVal = null) {
      return this.getNames ().reduce ((a, b) => (a[b] = defVal, a), {});
    }


    reset () {
      this.form.reset ();
    }


    //__________________________

    //SELECT, DATALIST, OPTGROUP の子要素の option を配列を元に書き換える
    static replaceOptions (target, aryValue, aryText, defaultValue, selectedValue) {
      //引数確認
      if (arguments.length < 2)
        throw new Error ('引数が足りません');
      if (! /^(SELECT|DATALIST|OPTGROUP)$/.test (target.tagName))
        throw new Error ('利用できる要素ではでありません');
      if (! Array.isArray (aryValue))
        throw new Error;
      if ('undefined' === typeof aryText)
        aryText = aryValue;
      if (aryValue.length !== aryText.length)
        throw new Error ('値とテキストの配列の数が違います');

      let
        doc = target.ownerDocument,
        fgm = doc.createDocumentFragment (),
        opt = doc.createElement ('option'),
        sdv, dfv;

      switch (typeOf (defaultValue)) {
      case 'Number' : case 'String' : dfv = [defaultValue]; break;
      case 'Array'                  : dfv = defaultValue; break;
      case 'Undefined'              : dfv = false; break;
      default                       : throw new Error ('無効な引数の型が指定されました'); break;
      }

      switch (typeOf (selectedValue)) {
      case 'Number' : case 'String' : sdv = [selectedValue]; break;
      case 'Array'                  : sdv = selectedValue; break;
      case 'Undefined'              : sdv = false; break;
      default                       : throw new Error ('無効な引数の型が指定されました'); break;
      }

      //使用する配列全てを文字列化
      aryValue = insideArrayToString (aryValue);
      aryText = insideArrayToString (aryText);
      if (dfv) dfv = insideArrayToString (dfv);
      if (sdv) sdv = insideArrayToString (sdv);

      //子要素の削除
      for (let e; e = target.firstChild; )
        target.removeChild (e);

      //オプションの作成
      aryValue.forEach ((v, i) => {
        let o = opt.cloneNode (false);
        o.text = aryText[i];
        o.value = v;
        if (dfv) o.defaultSelected = dfv.include (v);
        if (sdv) o.selected = sdv.include (v);
        fgm.appendChild (o);
      })

      target.appendChild (fgm);
    }
  }

  //__________________________

  this.ExpForm = ExpForm;

//  this.isNodeList = isNodeList;
}



</script>