この記事はどちらかというとreactを使ったtypescriptでevent.targetオブジェクトを扱う際の作法の話で、es5からTSになったからといってevent.targetオブジェクト自体が特段変わったわけではないです。
でも「Reactビギナーズガイド」3章のExcelコンポーネントを作る上で、event.targetの型をどのように記述したらいいのかちゃんと理解するまでに2時間ほどハマり、色々探してもジャストな記事がなかったのでまとめます。
event.targetのよくある誤解(というか私がしていた誤解)とおさらい
ここからは恥ずかしながら私の勘違い集です。
一旦reactやtypescriptからは離れて、素のhtmlやjavascriptを用いてevent.targetについての理解をし直します。
【よくある誤解1】onChangeイベントは<input>要素や<select>要素といったFormで扱う要素にしか指定できない、という誤解
「onchangeは値をチェンジ可能な要素にしか指定できないでしょ」
いえいえ、divやtable、なんならbodyにだってonChangeイベントは記述できます。
<html>
<head>
<script type="text/javascript">
function hoge(ev){
console.log("changed!");
}
</script>
</head>
<body onchange="hoge(event);">
<div class="parent">
<input type="text"/>
</div>
</body>
</html>
子孫要素のinputの値が変わるたびコンソールにchanged!と表示されます。
【よくある誤解2】event.targetにはイベントハンドラがアタッチされたDOM要素が入っている、という誤解
上記sample1.htmlのhogeを次のように変更します。
function hoge(ev){
console.log(ev.target);
}
「ふんふん、bodyにonchangeが書かれていて、そこでhogeが呼ばれているから、event.targetには、<body>の要素が格納されているはずだ」
いえいえ、event.target
にはイベントが発火したDOM要素が格納されます。
子孫要素のinputの値を変えてみると、このハンドラが呼ばれて、hogeが実行されます。
コンソールに書き出された結果は
<input type="text">
ちなみにイベントハンドラがアタッチされたDOM要素を取得したい場合は、event.currentTarget
になります。
【よくある誤解3】event.target.value
はinput要素がtargetに格納されている時のみ実行される、という誤解
「divとかbodyにはvalue属性がないから、ev.target.valueって書いたらev.targetがinput属性の時だけ処理してくれるはず。だって次のコードちゃんと動くし...」
<html>
<head>
<script type="text/javascript">
function huga(ev){
var input = ev.target;
input.value = input.value + "!";
}
</script>
</head>
<body onclick="huga(event);">
<div>
<input type="text" value="click it!"/>
</div>
</body>
</html>
確かに、ブラウザで確認するとテキストボックスをクリックするたびに"!"がどんどん増えていってきちんと動いているように見えます。
テキストボックス以外の箇所をクリックしても増えていかないです。
しかし、テキストボックス以外のところをクリックしてもきちんとイベントは実行されています。
huga関数でconsole.logを仕込み、input.valueの値をのぞいてみます。
function huga(ev){
var input = ev.target;
input.value = input.value + "!";
console.log(input.value); // 追加
}
テキストボックス以外のところをクリックすると
undefined!
undefined!!
undefined!!!
といったように、どんどんundefinedに"!"が増えていきます。
なんだかこれはすごい怒られているように感じますね...。
きちんと他のところがクリックされたことも念頭に置いてコードを書かなくてはいけませんね。
#本題、React + typescriptでイベントオブジェクトをどう書いていったらいいか
オブジェクトの理解がなくても、any使えば簡単なのですが、それではtypescript使っている意味がないので、きちんとオブジェクトを理解してtypescriptで書きましょう。
次のコンポーネントをたたきにして、調べていきます。
import * as React from 'react';
class EventTest extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
this.clickHandler = this.clickHandler.bind(this); // ⑤
}
clickHandler(ev: React.MouseEvent<HTMLDivElement>) { // ②
const input = ev.target as HTMLInputElement; // ③
if (input.tagName === 'INPUT') { // ④
input.value = input.value + '!';
}
}
render(): JSX.Element {
return (
<div onClick={this.clickHandler}> // ①
<input type="text" defaultValue="click it"/>
</div>
);
}
}
①イベントハンドラをjsxにて登録
このdivがクリックされたら②へ進みます。
②引数にevオブジェクトと型を指定
まず、型React.MouseEvent
について解説します。
ReactのEventSystem
typescriptでイベントオブジェクトを扱う前に、オブジェクトの体系がどのようになっているのかを理解する必要があります。
以下githubにどのようにイベントオブジェクトのインターフェースとメンバが載っています。
###全ての元となるSyntheticEvent<T>
全てのイベントオブジェクトはSyntheticEventを継承しています。
SyntheticEventには以下のメンバがあります。
interface SyntheticEvent<T> {
bubbles: boolean;
/**
* A reference to the element on which the event listener is registered.
*/
currentTarget: EventTarget & T;
cancelable: boolean;
defaultPrevented: boolean;
eventPhase: number;
isTrusted: boolean;
nativeEvent: Event;
preventDefault(): void;
isDefaultPrevented(): boolean;
stopPropagation(): void;
isPropagationStopped(): boolean;
persist(): void;
// If you thought this should be `EventTarget & T`, see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239
/**
* A reference to the element from which the event was originally dispatched.
* This might be a child element to the element on which the event listener is registered.
*
* @see currentTarget
*/
target: EventTarget;
timeStamp: number;
type: string;
}
React.FormEventはSyntheticEventをただ継承しているだけです。
interface FormEvent<T> extends SyntheticEvent<T> {
}
React.MouseEventはSyntheticEventを継承しメンバがいくつか加わっています。
interface MouseEvent<T> extends SyntheticEvent<T> {
altKey: boolean;
button: number;
buttons: number;
clientX: number;
clientY: number;
ctrlKey: boolean;
/**
* See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
*/
getModifierState(key: string): boolean;
metaKey: boolean;
nativeEvent: NativeMouseEvent;
pageX: number;
pageY: number;
relatedTarget: EventTarget;
screenX: number;
screenY: number;
shiftKey: boolean;
}
例えば③あたりでev.screenX
やev.pageX
と書くとマウスがクリックされた座標を取得することができます。
今回のサンプルではクリックイベントなのでReact.MouseEvent型にしましたが、クリックイベント特有のプロパティを取得する必要がないので、②の型のところはReact.FormEventと書いても、React.SyntheticEventと書いても問題なく動作します。
まあでもonClickなのに型がFormEventなのは気持ち悪いのでonClickなどはMouseEventとして型を定義するのが良いでしょう。
どのイベントにどの型を当てはめればいいかは以下の公式ドキュメントが詳しいです。
https://reactjs.org/docs/events.html
###ジェネリック<xx>に指定する型
ここはcurrentTargetに入るHTMLElementを指定します。
http://definitelytyped.org/docs/flipsnap--flipsnap/interfaces/htmlelement.html
に記載されている型になります。
EventTest.tsxの場合はdiv要素にonclickがあるのでHTMLDivElementを指定します。
HTMLDivElementなどは全てHTMLElementを継承しているため、ジェネリックにはHTMLElementと書くこともできます。
③ HTMLInputElementへのキャスト
ここでev.targetをHTMLInputElementへキャストする必要があります。
キャストの仕方はtypescriptでは2パターンあります。
var x = <any> foo;
//上と下は同じ
var x = foo as any;
上のパターンはjsxを使っていると、シンタックスエラーとして怒られるため、react + typescriptを書いている場合は、下のas演算子を使わなくてはいけません。
ev.targetに入っているオブジェクトの型はEventTarget型です。
メンバはaddEventListener,dispatchEvent,removeEventListenerしかありませんが、これをHTMLElementの派生型にキャストすることができます。
HTMLInputElement型にキャストすることによりvalueプロパティが使用できるようになります。
なお、次のようにしてもこのサンプルでは同等の動作をします。
const input = ev.currentTarget.firstElementChild as HTMLInputElement;
ev.currentTarget
でonclickがアタッチされたdiv要素が取得できます。
ev.curentTarget.firstChild
というプロパティもありますが、これはNode型のため次の④のifでtagNameプロパティが使えません。
④どのオブジェクトがクリックされた時の処理かを指定
どのオブジェクトがクリックされた時の処理なのかを指定します。
divがクリックされた時もこのイベントは発火するので、きちんとinputがクリックされた時のみ処理をしてあげるように分岐します。
ここは色々な書き方があると思います。
##⑤コンストラクタでのイベントメソッドのbind(this)はコンポーネントのメソッドを使うには必須
サンプルではthis.setStateやクラス変数へのアクセスは行いませんでしたが、基本的には、イベントでsetStateが呼ばれることになると思うので、⑤のようにイベントメソッドにthisをバインドしてあげます。
こうすると、clickHandler内で使われるthisがEventTestコンポーネント自体を参照します。
サンプル自体はthisを呼んでいないので、⑤がなくても動作します。
#まとめ
- react + typescriptでanyを使わなくても型をきちんと理解すればイベントオブジェクトで型指定が行える。
- ev.targetを扱うときにはHTMLElementやその派生型へキャストする必要がある
- ev.currentTargetはキャストをしなくても扱える
- React.SyntheticEventはすべてのイベントオブジェクトから継承される
- ReactでonClickを使うときはReact.MouseEvent型を使う
色々と調べまくった甲斐があり、typescriptとreact共にかなり理解が進んできました。
こうやって地道なところから理解していくと最初は苦しいですが、あとあと楽になりますね。
それにしても丸一日勉強して30ページぐらいしか進んでいない...orz