DOM操作
HTMLElement
型の探求
標準化されてから20年以上が経ち、JavaScriptは大きく進化しました。2020年現在、JavaScriptはサーバー、データサイエンス、さらにはIoTデバイスでも使用できますが、最も一般的なユースケースはWebブラウザであることを忘れてはなりません。
ウェブサイトはHTMLやXMLドキュメントで構成されています。これらのドキュメントは静的であり、変化しません。ドキュメントオブジェクトモデル(DOM)は、ブラウザによって実装されたプログラミングインターフェースであり、静的なウェブサイトを機能的にします。DOM APIを使用すると、ドキュメントの構造、スタイル、コンテンツを変更できます。このAPIは非常に強力なため、動的なウェブサイトの開発をさらに容易にするために、無数のフロントエンドフレームワーク(jQuery、React、Angularなど)が開発されています。
TypeScriptはJavaScriptの型付きスーパーセットであり、DOM APIの型定義を提供します。これらの定義は、デフォルトのTypeScriptプロジェクトであればすぐに利用できます。lib.dom.d.tsの2万行を超える定義の中で、特に注目すべきはHTMLElement
です。この型は、TypeScriptによるDOM操作の基盤となります。
DOM型定義のソースコードはこちらで確認できます。
基本例
簡略化されたindex.htmlファイルがあるとします。
html
<!DOCTYPE html><html lang="en"><head><title>TypeScript Dom Manipulation</title></head><body><div id="app"></div><!-- Assume index.js is the compiled output of index.ts --><script src="index.js"></script></body></html>
<p>Hello, World!</p>
要素を#app
要素に追加するTypeScriptスクリプトを見てみましょう。
ts
// 1. Select the div element using the id propertyconst app = document.getElementById("app");// 2. Create a new <p></p> element programmaticallyconst p = document.createElement("p");// 3. Add the text contentp.textContent = "Hello, World!";// 4. Append the p element to the div elementapp?.appendChild(p);
index.htmlページをコンパイルして実行すると、結果のHTMLは次のようになります。
html
<div id="app"><p>Hello, World!</p></div>
Document
インターフェース
TypeScriptコードの最初の行は、グローバル変数document
を使用しています。変数を検査すると、lib.dom.d.tsファイルのDocument
インターフェースによって定義されていることがわかります。コードスニペットには、getElementById
とcreateElement
の2つのメソッド呼び出しが含まれています。
Document.getElementById
このメソッドの定義は以下のとおりです。
ts
getElementById(elementId: string): HTMLElement | null;
要素ID文字列を渡すと、HTMLElement
またはnull
を返します。このメソッドは、最も重要な型の1つであるHTMLElement
を導入します。これは、他のすべての要素インターフェースの基本インターフェースとして機能します。たとえば、コード例でのp
変数はHTMLParagraphElement
型です。また、このメソッドはnull
を返す可能性があることに注意してください。これは、メソッドが実行前に指定された要素を実際に検出できるかどうかを事前に確実に知ることができないためです。コードスニペットの最後の行では、新しいオプションチェーン演算子を使用してappendChild
を呼び出しています。
Document.createElement
このメソッドの定義は(非推奨の定義は省略しました)以下のとおりです。
ts
createElement<K extends keyof HTMLElementTagNameMap>(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K];createElement(tagName: string, options?: ElementCreationOptions): HTMLElement;
これはオーバーロードされた関数定義です。2番目のオーバーロードは最もシンプルで、getElementById
メソッドと非常によく似ています。任意のstring
を渡すと、標準のHTMLElementを返します。この定義により、開発者は独自のHTML要素タグを作成できます。
たとえば、document.createElement('xyz')
は<xyz></xyz>
要素を返し、HTML仕様で指定されている要素ではありません。
興味のある方は、
document.getElementsByTagName
を使用してカスタムタグ要素を操作できます。
createElement
の最初の定義では、高度なジェネリックパターンを使用しています。ジェネリック式から始め、チャンクに分割して理解するのが最適です。<K extends keyof HTMLElementTagNameMap>
。この式は、HTMLElementTagNameMap
インターフェースのキーに制約されたジェネリックパラメーターK
を定義します。マップインターフェースには、指定されたすべてのHTMLタグ名とその対応する型インターフェースが含まれています。たとえば、最初の5つのマップされた値は次のとおりです。
ts
interface HTMLElementTagNameMap {"a": HTMLAnchorElement;"abbr": HTMLElement;"address": HTMLElement;"applet": HTMLAppletElement;"area": HTMLAreaElement;...}
一部の要素は固有のプロパティを示さず、HTMLElement
を返すだけですが、他の型には固有のプロパティとメソッドがあるため、それらの特定のインターフェース(HTMLElement
から拡張または実装する)を返します。
では、createElement
定義の残りの部分について説明します。(tagName: K, options?: ElementCreationOptions): HTMLElementTagNameMap[K]
。最初の引数tagName
は、ジェネリックパラメーターK
として定義されています。TypeScriptインタープリターは、この引数からジェネリックパラメーターを推論するのに十分なほど賢いです。つまり、開発者はメソッドを使用する際にジェネリックパラメーターを指定する必要はありません。tagName
引数に渡された値はK
として推論され、定義の残りの部分で使用できます。これがまさに起こることです。戻り値HTMLElementTagNameMap[K]
は、tagName
引数を取り、それを用いて対応する型を返します。この定義により、コードスニペットのp
変数はHTMLParagraphElement
型になります。そして、コードがdocument.createElement('a')
であれば、HTMLAnchorElement
型の要素になります。
Node
インターフェース
document.getElementById
関数はHTMLElement
を返します。HTMLElement
インターフェースはElement
インターフェースを拡張し、Element
インターフェースはNode
インターフェースを拡張します。このプロトタイプの拡張により、すべてのHTMLElements
は標準メソッドのサブセットを使用できます。コードスニペットでは、Node
インターフェースで定義されたプロパティを使用して、新しいp
要素をウェブサイトに追加します。
Node.appendChild
コードスニペットの最後の行はapp?.appendChild(p)
です。前のdocument.getElementById
のセクションで詳述されているように、実行時にapp
がnullになる可能性があるため、ここでオプションチェーン演算子が使用されています。appendChild
メソッドは以下のように定義されています。
ts
appendChild<T extends Node>(newChild: T): T;
このメソッドは、ジェネリックパラメーターT
がnewChild
引数から推論されるという点で、createElement
メソッドと同様に機能します。T
は、別の基本インターフェースNode
に制約されています。
children
とchildNodes
の違い
これまで、このドキュメントでは、HTMLElement
インターフェースがElement
から拡張され、Element
インターフェースがNode
から拡張されることを詳述してきました。DOM APIには、子要素の概念があります。たとえば、次のHTMLでは、p
タグはdiv
要素の子です。
tsx
<div><p>Hello, World</p><p>TypeScript!</p></div>;const div = document.getElementsByTagName("div")[0];div.children;// HTMLCollection(2) [p, p]div.childNodes;// NodeList(2) [p, p]
div
要素を取得した後、children
プロパティは、HTMLParagraphElements
を含むHTMLCollection
リストを返します。childNodes
プロパティは、ノードの同様のNodeList
リストを返します。各p
タグは依然としてHTMLParagraphElements
型ですが、NodeList
にはHTMLCollection
リストには含まれない追加のHTMLノードを含めることができます。
p
タグの1つを削除してテキストを維持することで、HTMLを変更します。
tsx
<div><p>Hello, World</p>TypeScript!</div>;const div = document.getElementsByTagName("div")[0];div.children;// HTMLCollection(1) [p]div.childNodes;// NodeList(2) [p, text]
両方のリストがどのように変化するかを確認してください。children
は現在<p>Hello, World</p>
要素のみを含んでおり、childNodes
は2つのp
ノードではなくtext
ノードを含んでいます。NodeList
のtext
部分は、テキストTypeScript!
を含むリテラルNode
です。children
リストには、HTMLElement
とは見なされないため、このNode
は含まれていません。
querySelector
メソッドとquerySelectorAll
メソッド
これらのメソッドはどちらも、より固有の制約に適合するDOM要素のリストを取得するための優れたツールです。これらはlib.dom.d.tsで次のように定義されています。
ts
/*** Returns the first element that is a descendant of node that matches selectors.*/querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;querySelector<E extends Element = Element>(selectors: string): E | null;/*** Returns all element descendants of node that match selectors.*/querySelectorAll<K extends keyof HTMLElementTagNameMap>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;querySelectorAll<K extends keyof SVGElementTagNameMap>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;
querySelectorAll
の定義はgetElementsByTagName
に似ていますが、新しい型NodeListOf
を返します。この戻り値型は、標準のJavaScriptリスト要素のカスタム実装です。おそらく、NodeListOf<E>
をE[]
に置き換えると、非常に似たユーザーエクスペリエンスになります。NodeListOf
は、length
、item(index)
、forEach((value, key, parent) => void)
、および数値インデックスのみを実装しています。さらに、このメソッドはノードではなく要素のリストを返し、これは.childNodes
メソッドからNodeList
が返していたものです。これは矛盾しているように見えるかもしれませんが、インターフェースElement
がNode
から拡張されていることに注意してください。
これらのメソッドがどのように機能するかを確認するには、既存のコードを次のように変更します。
tsx
<ul><li>First :)</li><li>Second!</li><li>Third times a charm.</li></ul>;const first = document.querySelector("li"); // returns the first li elementconst all = document.querySelectorAll("li"); // returns the list of all li elements
さらに詳しく知りたいですか?
lib.dom.d.ts型定義の最も優れた点は、Mozilla Developer Network(MDN)ドキュメントサイトに注釈された型を反映していることです。たとえば、HTMLElement
インターフェースは、MDNのこのHTMLElementページで説明されています。これらのページには、使用可能なすべてプロパティ、メソッド、場合によっては例もリストされています。ページのもう1つの優れた点は、対応する標準ドキュメントへのリンクを提供することです。HTMLElementに関するW3C勧告へのリンクを次に示します。
出典