🧼 コードから click イベントを発火する手法 `el.click()` および `el.dispatchEvent(new Event('click'))` における挙動の差 / バブリング指定について
とある web ページに置いてあるボタンをコードから自動でクリックするブックマークレットをつくろうとした。
クリックリスナー登録先要素が不明だったので、それっぽいボタン要素(
el
)に目星をつけて click イベントを発火。
ここで書き方により挙動の差異が。
el.click()
→ クリック処理発生el.dispatchEvent(new Event('click'))
→ クリック処理発生せず
■ 何が違うのか
- HTMLElement.click() - Web API | MDN
上位の要素にバブルアップし、click イベントを発生させます。
-
bubbles 論理値で、イベントがバブリングするかどうかを示します。既定値は false です。
どうやらバブリングに差があるっぽい。
検証対象のページではel
の先祖要素に対しクリックリスナーが登録されていたらしく、以下の部分で違いが発生した。
el.click()
… click イベントがバブルアップし、リスナー登録された先祖要素で処理が発火。el.dispatchEvent(new Event('click'))
… 引数で渡したEvent
オブジェクトのバブリングが無指定では OFF のため、先祖要素まで click イベントが伝播せず。
■ 修正
el.dispatchEvent(new Event('click', { bubbles : true }))
→ クリック処理発生
■ 所感
自分で書いたコードならリスナー登録先が自明なので、この差に気づくこともなかったと思う。
曖昧だったバブリングについての理解が少し進む。
markdown-wasm 利用時の最小記述
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <pre class="md-input"> # title * list * list * list [https://rkgk.boy.jp](https://rkgk.boy.jp) normal text. </pre> <script> //=============================================================// // # markdown-wasm //=============================================================// (async (d = document) => { await new Promise((onload) => d.body.append( Object.assign(d.createElement(`script`), { onload, src: `https://cdn.jsdelivr.net/npm/markdown-wasm@1.2.0/dist/markdown.min.js`, }) ) ); await markdown.ready; d.querySelectorAll(`.md-input`).forEach((el) => { const md = d.createElement(`div`); (md.className = `md-output`), (md.innerHTML = markdown.parse(el.innerHTML)), el.parentNode.insertBefore(md, el), el.remove(); }); })(); </script> </body> </html>
VueのCDN版を使いつつ、TypeScriptの型定義も反映させたい
- 既存のTypeScriptプロジェクトにnpmのvueいれて
import {createApp} from 'vue'
でimport createApp({}).mount("#app")
でマウント
➡ #app
の中身が空っぽになってしまう。
CDN版だとDOMの中身を保ったままVueアプリ化されるんだけど…挙動が違った。
もともとCDN版を使おうとするも型定義どうしたらよいのかわからず諦めて、npmのvueいれた…という経緯があったが…。 なにか足りない手順とか、読み込むもの間違っているとかあるんだろうか。
結局CDN版をつかいつつ、どうにか型定義読み込む形でいくことに。
解決手順
あまりTS+Vue CDN版で一部だけVue化したい…というニーズはないのか、情報少なくかなり悩んだ。
- 型定義のためにnpmパッケージのvueをインストールして変数にimport
- interface WindowにVueを追加し、importしたvueの変数からtypeofを用いて型を指定
import * as vueProps from 'vue' declare global { interface Window { Vue: typeof vueProps } } export const fetchVueCDN = (): Promise<typeof window.Vue> => new Promise((resolve, reject) => { if (window.Vue) resolve(window.Vue) const prod = typeof env !== 'undefined' && env === 'prod' ? '.prod' : '' const s = Object.assign(document.createElement('script'), { src: `https://cdn.jsdelivr.net/npm/vue@3.2.20/dist/vue.global${prod}.js`, }) s.addEventListener('load', () => { resolve(window.Vue) }) document.body.append(s) setTimeout(() => { reject('Vueの読み込みがタイムアウトしました。') }, 30000) })
あとはこれで、CDN版使いつつ補完もきくようになった
;(async () => { const Vue = await fetchVueCDN() Vue.createApp({template:'msg:{{msg}}',setup(){return {msg:"テキストが入ります。"}}}).mount('#app') })()
参考:
TypeScript - TypescriptでimportせずにVue.jsを使う方法|teratail
余談
気軽にCDN版つかお~と思っても、TypeScriptプロジェクトではそうはいかないということに気付かされた…。
Vueの用途はこれまでjQueryでちょこちょこやってた処理の置き換え、程度にとどまっていて
vue-cliでのプロジェクト生成などはまだ試したことない。知識も浅い。.vue
使うの気が進まないんだよななんとなく…。
フルでjsフレームワークのSPAつくるというのなら、どっちかというとReactが気になるしね。
途中解決法がわからずReactいれたら、そちらはシンプルにReact,ReactDOM+その型定義をいれるだけで思った通りに動いたので、このままReact使用に移行しようと思ってた。
気持ち悪くて結局解消するまで調査していたけど…。
Reactやろうと思えるきっかけができたのはよかった。
いちいちつまづいて大変だ。フロントエンド学習身に余るなあ…。
Object.assign 活用
Object のマージ
const ab = { a: 1, b: 2 } const cd = { c: 3, d: 4 } Object.assign(ab, cd) // {a:1,b:2,c:3,d:4} ab // {a:1,b:2,c:3,d:4}
- 第一引数にあたえた変数に、後続の引数の内容がマージされるミュータブルな処理
- 変更を避けるには空のオブジェクトを与えてやるといい
Object.assign({},ab,cd)
- 変更を避けるには空のオブジェクトを与えてやるといい
💬 スプレッド構文 {...ab,...cd}
のほうが直感的なので普段この用途ではあまり使わないけど、マージしたいオブジェクトがたくさん入った配列などある場合は一括指定しやすいかもしれない
Object.assign({},...objArray)
とかで
クラスのメンバを一括設定
const obj = { a: 1, b: 2, c: 3, d: 4 } class Test { a = 0 b = 0 c = 0 d = 0 constructor(obj: { [k: string]: number }) { Object.assign(this, obj) } } console.log(new Test(obj)) //Test {a: 1, b: 2, c: 3, d: 4}
💬 かなり楽
DOM 生成
const $div1 = Object.assign(document.createElement('div'), { className: 'class-name1 class-name2', innerHTML: `テキストが入ります。`, }) // こうすればinnerHTMLの内容で要素が生成できる // 生成した要素内のひとつめを取り出して型アサーションで型指定 const $div2 = Object.assign(document.createElement('div'), { innerHTML: `<div class="class-name1 class-name2">テキストが入ります。</div>`, }).firstChild as HTMLElement console.log($div1, $div2) //<div class="class-name1 class-name2">テキストが入ります。</div> //<div class="class-name1 class-name2">テキストが入ります。</div>
- React の記法と同じように class 名の指定は
className
な点に注意。
💬 最近知ってうれしかったもの よく使ってる
(記述量は若干多いものの) PureJS だけで jQuery 感覚で新要素生成できるのがよい
要素作ってプロパティを逐一指定すればいいのだが、式の方が使い勝手良くて好き
TypeScript ループ内でオブジェクトのプロパティに変数を使ってアクセスしたい場合
CSSをtsで直書きしようとした際に問題発生。
スタイル指定をオブジェクトにしてループまわし、
要素.style[prop]
でアクセスしようとしたが、以下エラー。
インデックス式が型 'number' ではないため、要素に 'any' 型が暗黙的に指定されます。
propに型アサーションをくわえるとエラーが消えた。
propに入りうる値に合致する文字列リテラル型(のユニオン型)を指定するとエラーが出ないみたい。
document.querySelectorAll<HTMLElement>('.box').forEach(($el) => { const sobj = { background: '#00f', color: '#fff', padding: '1em', margin: '0.5em 0' } Object.entries(sobj).forEach(([prop, val]) => { // $el.style[prop] = val //インデックス式が型 'number' ではないため、要素に 'any' 型が暗黙的に指定されます。 $el.style[prop as keyof typeof sobj] = val // prop as "background" | "color"... }) })
typeof, keyof
いずれも型情報を取得する
typeof obj
でobjから型を取得( jsで型を調べるのに使うtypeofとはまた別物)
keyof 型
でその型が持つプロパティから文字列リテラルのユニオン型(String Literal Union) を取得
const obj = { a: 1, b: 2, c: 3 } type tObj = typeof obj //type tObj = { a: number; b: number; c: number;} type tObjKey = keyof tObj // type tObjKey = "a" | "b" | "c" ;['a', 'b', 'c'].forEach((v) => { // console.log(obj[v]) // エラー /* 型 'string' の式を使用して型 '{ a: number; b: number; c: number; }' に インデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。 型 'string' のパラメーターを持つインデックス シグネチャが 型 '{ a: number; b: number; c: number; }' に見つかりませんでした。 */ console.log(obj[v as "b"]) // 入りうる値に合致するリテラル型を指定するとエラーが出ないが console.log(obj[v as tObjKey]) // 正しくはこう })
参考
絵文字を含む文字列の分割
テキストを一文字ずつ配列にいれて操作する処理をつくっていたら、
"👻⛄🥩".length // 5 JSON.stringify("👻⛄🥩".split('')) // '["\ud83d","\udc7b","⛄","\ud83e","\udd69"]'
まじか
3文字にならない?
絵文字を含む場合に予期せぬ挙動になることに気づいた
Array.fromかスプレッド構文を使う
JSON.stringify(Array.from("👻⛄🥩")) JSON.stringify([..."👻⛄🥩"]) //'["👻","⛄","🥩"]'
スプレッド構文のが短くていいなと思ったんだが
型 'string' は配列型でも文字列型でもありません。反復子の反復を許可するには、コンパイラ オプション '--downlevelIteration' を使用します。
TypeScriptでとおらなかった
サロゲートペア
一文字を4バイトで表す手法
⛄って他の絵文字にくらべてそのまま使える場面が多いなと思っていたけど サロゲートペアではないからなんだなおそらく
参考
変数の再代入はなぜ避けたほうが良いのか、ようやくわかるようになってきた
2020年からjsを書く機会が急増して、これまでいかに初歩的な組み方しかしてこなかったか知った。 2020年の春ごろまではPromiseの使い方も知らなかったし、jQueryなしで書くのはハードル高いと思っていたし…。
人のコード見て顧みる
自分以上に経験少ない方のコードを引き継ぐことが多かったのも、破綻しやすいコードというものについて考える大きな要因だった。
(プログラミング関連で人と共同作業すること自体がそれまでなかった)
自身のスキルだってまだまだしょぼい自覚はあるものの、明確にこれはまずいとわかる悪手が散見され…。
お陰でどういったコードが困った事態を生み出すのか、身にしみて理解できるようになってきた。
- 別ファイルで宣言されたグローバル変数がファイル中に突然出現する
- スコープの長い関数の中で何度も変数への再代入が行われ、値の変化が追いづらい
- tt,ss,DdFunc といった、書いた本人以外には謎な命名の変数・関数が多用されている
- 同じ機能をもつ長い処理が複数個所に何度も出現する
ひとつのグローバル変数を複数関数でたらいまわして再代入繰り返すような処理が多くあり、途中で結果が予期せぬ値になってバグる…といった事例は少なくなかった…。
そんなこんなの繰り返しの中で、表題の件に思い至った。
「let使わずに書くってできるの?」
以前は「変数への再代入は極力避けたほうがよい」、というセオリーを見かけるたびなんで?と思っていたけど、 今ではできる限りconstで宣言するようにしてる。
以下のようなif文があちこちに散らばって値の変化を追いづらかったり、
(実際はlet宣言から再代入までがかなり離れた場所にある)
見た目にわかりづらいと思うシーンが頻繁にあったので…
const a = 1; let b = ""; if(flg === 1) b = "おはよう" if(flg === 2) b = "こんにちは" const c = "c";
今ではこうするようにしてる 自己流だが…
const a = 1; const b = (()=>{ if(flg === 1) return "おはよう" if(flg === 2) return "こんにちは" })(); const c = "c";
参考になりそう
↑の即時関数内にif文いれて返す方法、思い付きでやってたからこれでいいのかちょっと自信なかったけど、 記事作成中に見つけた、再代入をしないで組むテクについてかかれた記事内で同じ書き方を発見した
(ちょっと笑ってしまう内容)
JavaScriptからletを絶滅させ、constのみにするためのレシピ集 - Qiita
たくさんあってためになる。困ったら拝見しようと思います。
JavaScriptのletは本当に悪なのか - Qiita
考えがかたより過ぎないよう、こちらもあわせて…