location.hash と webapl を連動させるライブラリ
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; } } } } } }