🌟

JSのクラスメソッドをonclickに設定するときにつまずいたこと

2023/02/21に公開

JS(TS)のクラスメソッドを HTMLButtonElement の onclick に設定するときにつまずいて 1 時間ぐらい溶けたので備忘録として残しておきます。

前提

webpack + babel を使ってトランスパイルをしています。

package.json
package.json
{
    ...
    "scripts": {
        "build": "webpack --config webpack.config.prod.js",
        "dev": "webpack --config webpack.config.dev.js --watch"
    },
    ...
    "browserslist": [
        "defaults,not ie >= 0"
    ]
}
.babelrc
.babelrc
{ "presets": ["@babel/preset-env"] }
babel.config.js
babel.config.js
module.exports = (api) => {
    api.cache(true);
    return {
        presets: [
            [
                "@babel/preset-env",
                {
                    useBuiltIns: "usage",
                    corejs: 3,
                },
            ],
            "@babel/typescript",
        ],
    };
};
webpack.config.js

webpack.config.dev.jswebpack.config.prod.jsは下のをベースに少しいじったものです。

webpack.config.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

/** @type import('webpack').Configuration */
module.exports = {
    mode: 'none',

    entry: './ts/index.ts',

    target: ["web", "es5"],

    output: {
        path: path.join(__dirname, "dist"),
        filename: "index.js"
    },

    module: {
        rules: [{
            test: /\.ts$/,
            use: ["babel-loader"],
            exclude: /node_modules/,
        }]
    },

    resolve: {
        extensions: [".ts", ".js", ".tsx"]
    },

    plugins: [
        new CleanWebpackPlugin()
    ]
};

状況

実装をだいぶ省略して書くとこんな感じのものになります。

GetElement
export module GetElement {
    export function getHTMLInputElement(id: string): HTMLInputElement | never {
        const element = document.getElementById(id);
        if (!element) throw Error(`element ${id} does not exist`);
        return element as HTMLInputElement;
    }
}
Hoge.ts
import { GetElement } from './GetElement';

export class Hoge {
    hogeInputId: string;
    constructor(hogeInputId: string) {
        this.hogeInputId = hogeInputId;
    }
    action(): void {
        const hoge = GetElement.getHTMLInputElement(this.hogeInputId).value;
    }
}
index.ts
import { Hoge } from './Hoge';
import { GetElement } from './GetElement';

window.onload = () => {
    const hoge = new Hoge('hoge');

    GetElement.getHTMLButtonElement('hogeButton').onclick = hoge.action;
};

これをブラウザーで開いてボタンを押すと次のようなエラーが出ました。

エラー文
Uncaught Error: element "undefined" not found
    at getHTMLElement
    at getHTMLElementAs
    at Object.getHTMLInputElement
    at HTMLButtonElement.action

hogeの定義時にはちゃんと id を定義にしているのでundefinedになるのはおかしいと思います(小並感)。
なぜこうなってるのか不明なのでログに書き出してみました。

hoge.ts
--- a/ts/hoge.ts
+++ b/ts/hoge.ts
@@ -4,8 +4,10 @@ export class Hoge {
     hogeInputId: string;
     constructor(hogeInputId: string) {
         this.hogeInputId = hogeInputId;
+        console.log(this);
     }
     action(): void {
+        console.log(this);
         const hoge = GetElement.getHTMLInputElement(this.hogeInputId).value;
     }
 }

するとコンストラクタ内の方では正しく自分が吐き出されているものの、actionの方では HTML の要素が出てきました。

console.log(this);
<input class="monofont validate" id="hoge" type="text" placeholder="..." value="..." required aria-required>

こんなこともしてみました。
変数の名前をほかのもの(hoge2)に変えてhoge,hoge2をログに出す。

index.ts
--- a/ts/index.ts
+++ b/ts/index.ts
@@ -2,7 +2,9 @@ import { Hoge } from './Hoge';
 import { GetElement } from './GetElement';

 window.onload = () => {
-    const hoge = new Hoge('hoge');
+    const hoge2 = new Hoge('hoge');

-    GetElement.getHTMLButtonElement('hogeButton').onclick = hoge.action;
+    GetElement.getHTMLButtonElement('hogeButton').onclick = () => hoge2.action();
+    console.log(hoge);
+    console.log(hoge2);
 };

するとhogeはさっきの HTML の要素、hoge2は Hoge のインスタンスでした。

訳が分からずいろいろしてみましたが、なかなか解決できませんでした。

解決策

無名関数で覆う

--- a/ts/index.ts
+++ b/ts/index.ts
@@ -4,5 +4,5 @@ import { GetElement } from './GetElement';
 window.onload = () => {
     const hoge = new Hoge('hoge');

-    GetElement.getHTMLButtonElement('hogeButton').onclick = hoge.action;
+    GetElement.getHTMLButtonElement('hogeButton').onclick = () => hoge.action();
 };

これだけで解決しました。

問題点

  • 1.onclick に直接クラスメソッドをくっつけたこと
  • 2.html の id は同名の変数が作成されることを知らなかったこと

1

勝手な考察ですが、おそらく onclick に渡された関数にはその要素がバインドされるのでしょうか。それでthisが上書きされているのだと思います。
thisが失われたわけではないと思います。(∵ ログに HTML 要素が出ている)

2

これに関しては調べたら出てきました。
https://qiita.com/nakajmg/items/c895105afae95bfa8fae

個人的な感想ですがこの機能いらないと思います。混乱するので。

Discussion

ログインするとコメントできます