データベースに記録するテーブルの構造を可変にしたい

データ(レコード)サイズが大きいわけではない。
テーブル構造(フィールド)を頻繁に変更するかもしれない。

ということで、JSON形式にして文字列のまま保管できるようにしたい。
HTMLのFORMの中の要素も可変にしたい

JSONを利用して(テーブル式)フォームを形成する

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title></title>

  <meta http-equiv="content-language" content="ja">
  <meta http-equiv="Cache-Control" content="no-cache">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <meta name="format-detection" content="telephone=no">
  <link rel="stylesheet" href="form_table.css" type="text/css">
  <style>
  </style>
</head>

<body>
<form method="post" enctype="multipart/form-data" id="FF">
  <table id="T"></table>
  <button onclick="console.log($F.getValue(),FF); return false;">フォームの値を取得</button>
  <input type="submit" value="submit">
</form>

<script>
const JSON = [
  {title:['世帯名',{rowspan:1}], name:'SETAI'},
  {title:'住所', name:'POSTCODE', attr:{placeholder:'〒'}},
  {name:'ADD' },
  {name:'ADD' },

  {title:'電話', name:'TEL'},
  {name:'TEL' },
  {name:'TEL' },

  {title: '性別', name:'SEIBETU', type: 'radio', option: [
    ['男性','man', 0,1],
    ['女性','woman'],
    ['不詳','',1,0],
  ]},

  {title: '言語', name:'GENGO', type: 'checkbox', option: [
    ['PHP','PHP'],
    ['JavaScript','JS',1],
    ['BASIC','BASIC'],
  ]},

  {title: 'IQ', name:'IQ', type: 'select', option: [
    ['--選択して下さい--','',0],
    ['頭が良い','120'],
    ['普通','100'],
    ['無理','80'],
  ]},

  {title: '地区', name:'TIKU', type: 'datalist', option: [
    '種市','小子内','有家','中野','大野',
  ]},

  {title: 'Memo', name:'MEMO', type: 'textarea', attr:{rows:10}},



//  {title: '', name:'', value: '', type: null, option: [], attr: {}},

];



class $F {
  /*
    独自のJSON形式で模したフォーム要素を具現化してテーブル形式にフォームを構成する
  */
  static mkTable (aryArg = [], tbody = document.createElement ('tbody')) {
    const E = (n,a={},c=[],e)=> (e=document.createElement(n),Object.entries(a).forEach(([a,b])=>b !== false && e.setAttribute(a,b)),e.append(...c),e);

    let tr, th, attr, cnt = 0;
    for (let obj of aryArg) {
      let t = obj.title;
      tr = tbody.insertRow ();
      if (null != t) {
        if (1 < cnt)
          th.rowSpan = cnt;

        tr.append (th = document.createElement ('th'));
        if (Array.isArray (t)) {
          [t, attr] = t;
          Object.entries (attr).forEach (_=> th.setAttribute (..._));
        }
        th.textContent = t;

        cnt = 1;
      } else
        cnt++;
      
      this.mkInput (obj, tr.insertCell ());
    }
    return tbody;
  }

  /*
    フォーム要素を生成

  */
  static mkInput (mold, parent = new DocumentFragment ()) {
    // Object.assign では list属性を変更できないので setAttribute を利用
    const E = (n,a={},c=[],e)=> (e=document.createElement(n),Object.entries(a).forEach(([a,b])=>b !== false && e.setAttribute(a,b)),e.append(...c),e);

    let {name, value = '', attr = {}, option = [], type = 'text'} = mold;
    let e = [], id;
    type = mold.type = type.toLowerCase ();

    switch (type) {
    case 'textarea' :
      e.push (E ('textarea', Object.assign({name, value},attr))); break;

    case 'select' :
      attr.type ='select-one';
    case 'select-multiple' : case 'select-one' :
      e.push (E('select', Object.assign({name, value}, attr), option.map (_=> new Option (..._))));
      break;

    case 'checkbox' : case 'radio' :
      e.push (...(option.map (([a,b,c,d=c])=>
        E('label', attr, [E('input', {name, type, value:b, checked:!!c, defaultChecked:!!d}),a]))) 
      );
      break;
    
    case 'datalist':
      id = 'dl-' + name;
      e.push (
        E('input', Object.assign ({name, value, list:id}, attr)), //type は attr で上書き?
        E('datalist', {id}, option.map(_=> new Option(_)))
      );
      break;
    
    case 'image' :
      e.push (E('input', Object.assign ({name, type, value, src:value}, attr)));
      break;

    
    default :
      e.push (E('input', Object.assign ({name, type, value}, attr)));
      break;
    }

    parent.append (...e);
    return parent;
  }

  
  /*
    フォーム要素群の値をオブジェクト型にして返す
    同じname属性が複数 or select-multipleの場合 or checkbox は配列で返す
  */
  static getValue (root = document.body, query = ':is(select,textarea,input)[name]:not([type="button"],[type="reset"],[type="submit"])') {
    //フォームの要素の値を返す
    const V = e=> {
      switch (e.type) {
        case'select-multiple': return [...e.selectedOptions].map(o=>o.value);
        case'checkbox': if(! e.checked) return []; break;
        case'radio': if (! e.checked) return; break;
      }
      return e.value;
    };
  
    //__

    let obj = { };

    for (let e of root.querySelectorAll (query)) {
      let val = V (e);
      let name = e.name;

      if (obj.hasOwnProperty (name)) {
        if (null == val) continue;
        let x = obj[name];
        if (null != x)
          val = [].concat (x, val);
      }

      obj[name] = val;
    }
    return obj;
  }


  /*
  */
  //オブジェクト型の値をフォーム要素群に転置
  static setValue (obj , root = document.body, query = ':is(select,textarea,input)[name]:not([type="button"],[type="reset"],[type="submit"])') {
    //要素に値を設置
    const V = (e, val)=> {
      let idx;
      if (Array.isArray (val)) {//配列の場合は、転置ごとにその値を削除する
        switch (e.type) {
          case 'select-multiple' :
            for (let op of e.options) {
              idx = val.indexOf (e.value);
              if (-1 < idx) {
                val.splice (idx, 1);
                e.selected = true;
              } else
                e.selected = false;
            }
            break;
          case 'radio' : case'checkbox' :
            idx = val.indexOf (e.value);
            if (-1 < idx) {
              val.splice (idx, 1);
              e.checked = true;
            } else
              e.checked = false;
            break;
          default :
            e.value = val.shift () || '';
            break;
        }
      }
      else {//転置用の値が単一ならば、複数の同じname属性の要素に同じ値を転置する
        switch (e.type) {
          case 'select-multiple' :
            e.options.forEach (o=> o.selected = o.value == val);
            break;
          case 'radio' : case'checkbox' :
            e.checked = e.value == val;
            break;
          default :
            e.value = val || '';
            break;
        }
      }
    };

    //__

    let
      val = Object.assign ({ }, obj),//copy
      es = root.querySelectorAll (query);

    this.reset ();
    for (let e of es)
      V (e, val[e.name]);
  }

  static reset (root = document.body, query = ':is(select,textarea,input)[name]:not([type="button"],[type="reset"],[type="submit"])') {
    let es = root.querySelectorAll (query);
    for (let e of es) {
      switch (e.type) {
        case 'select-one' : case 'select-multiple' :
          for (let op of e.options)
            op.selected = op.defaultSelected;
          break;
        case 'radio' : case 'checkbox' :
          e.checked = e.defaultChecked;
          break;
        default :
          e.value = e.defaultValue || '';
      }
    }
  }
}

$F.mkTable (JSON,T);
$F.setValue ({MEMO: 'abc', TIKU:'中野', GENGO:['PHP','BASIC'], SEIBETU:'woman',ADD:[,2,]});
//alert(8);
$F.reset ();

</script>

ちょっと変更