Ajax を利用してアプリを作るとき、location.hash の値と連動して動く。
onHashChange を使用せずに済む。
とある条件でページをスクリプトで生成するプログラムがあるとする。
その条件が変化するごとに location.hash に書き出し、コールバック(生成プログラム)に条件を渡して呼び出す
外部からある条件下でページを生成したい場合 location.hash に条件を書いて呼び出す
インスタンス化した時点の条件を標準とし、それと違う用件だけを location.hash に記述する
2022-07-09 更新
/*----------------------------------------------
onHashChange を使用せず
現在の状態を location.hash に反映させる(逆も可)
対象となるFORM の要素群には id もしくは name 属性が必要である
----------------------------------------------*/
class ApplicationNavigator {
#defaultState = null;//初期設定時の状態を基本とする
constructor (form = document.querySelector ('form'), cbFunc = null, cbObj = null, ...cbArgs) {
this.form = form;
this.cbFunc = cbFunc;
this.cbObj = cbObj;
this.cbArgs = cbArgs;
this.elements = [...form.querySelectorAll ('input[id], input[name], select[id], select[name], textarea[id], textarea[name]')];
this.#defaultState = this.constructor.getValues (this.elements);
this.hash = location.hash;
if (cbFunc)
form.addEventListener ('change', this, false);
}
execute (e) {
if ('function' === typeof this.cbFunc) {
this.cbFunc.apply (this.cbObj || this, [this.status, ...this.cbArgs]);
location.hash = this.hash;
}
return this;
}
reset () {
this.status = this.#defaultState;
return this;
}
handleEvent (event) {
let e = event.target;
if (this.elements.includes (e))
this.execute (e);
}
get status () {//全ての状態を返す
return this.constructor.getValues (this.elements);
}
set status (obj = { }) {//statusには実態がない(その都度要素から収集)
this.constructor.setValues (this.elements, Object.assign ({ }, obj));
this.execute ();//コールバック関数を呼び出す
}
get hash () {
let a = this.#defaultState;
return '#' +
Object.entries (this.status)
.filter (([k,v])=> a[k] != v)
.map (b=> b.join`=`)
.join('&');
}
set hash (hash) {
this.status = this.constructor.parseParms (hash);
}
//______________________
static parseParms (str = '') {
let
reg = /(?:^#)?(\D\w*)(\[\])?=(.*?)(?:\&|\;|$)/g,
pieces = new Map,
piece;
while (piece = reg.exec (str)) {
let [, key, isAry, val] = piece;
key = decodeURIComponent (key);
val = decodeURIComponent (val);
if (isAry)
val = [val];
if (pieces.has (key))
pieces.set (key, [].concat (pieces.get (key), val));
else
pieces.set (key, val);
}
return Object.fromEntries (pieces);
}
//要素の値を取得してオブジェクトにして返す
//name属性が複数な場合配列として記憶する
static getValues ([...es]) {
const
excludeType = ['fieldset', 'reset', 'submit', 'image', 'file','button'],//除外する要素
obj = new Map;
for (let e of es) {
let type = e.type, key, value;
if (excludeType.includes (type))
continue;
if (['checkbox', 'radio'].includes (type) && !e.checked)
continue;//checked属性がfalsse は除外する
if (key = e.id || e.name) {//keyがあるものだけ有効
//最初から配列とする
if ('select-multiple' === type)
value = [...e.options].reduce ((a, b)=> (b.selected && a.push (b.value), a), [ ]);
else if ('checkbox' === type)
value = [value];
else
value = e.value;
if (obj.has (key))//すでに登録されたものは配列
value = [].concat (obj.get (key), value);
obj.set (key, value);
}
}
return Object.fromEntries (obj);//オブジェクト化
}
//指定要素群に値を代入する
static setValues ([...es], obj = { }) {
const
priority = ['radio', 'checkbox', 'select-one', 'select-multiple'],//優先順位
prioritize = (a, b)=> priority.indexOf (a.type) - priority.indexOf (b.type);
let hs = es.reduce ((a,e)=> (a[e.id||e.name] = (a[e.id||e.name] || []).concat(e), a), { });//名前毎にhashにする
//名前毎の中で優先順に並び替え
Object.values (hs).forEach (v=> v.sort (prioritize));
//select要素のmultipleのすべてをリセットする
es.filter (e=> e.type === 'select-multiple').flatMap (e=> [...e.options]).forEach (e=> e.selected = false);
//es.filter (e=> e.hasAttribute ('defaulValue')).forEach (e=> e.value = e.defaultValue);
for (let [name, vals] of Object.entries (obj)) {//※valsはコピーしたもの
let es = hs[name] || [ ];
vals = [].concat (vals);
while (vals.length) {
let val = vals.shift ();
for (let e of es) {
switch (e.type) {
//想定のtype順で設定する
case 'select-multiple':
for (let op of e.options) {
if (op.value == val) {
op.selected = true;
break;
}
}
break;
case 'select-one' : e.value = val; break;
case 'checkbox' : case 'radio' : e.checked = e.value === val; break;
default : e.value = val; break;
}
}
}
}
}
}