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
このメソッドの定義は以下のとおりです。
tsgetElementById(elementId: string): HTMLElement | null;
要素ID文字列を渡すと、HTMLElementまたはnullを返します。このメソッドは、最も重要な型の1つであるHTMLElementを導入します。これは、他のすべての要素インターフェースの基本インターフェースとして機能します。たとえば、コード例でのp変数はHTMLParagraphElement型です。また、このメソッドはnullを返す可能性があることに注意してください。これは、メソッドが実行前に指定された要素を実際に検出できるかどうかを事前に確実に知ることができないためです。コードスニペットの最後の行では、新しいオプションチェーン演算子を使用してappendChildを呼び出しています。
Document.createElement
このメソッドの定義は(非推奨の定義は省略しました)以下のとおりです。
tscreateElement<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つのマップされた値は次のとおりです。
tsinterface 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メソッドは以下のように定義されています。
tsappendChild<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勧告へのリンクを次に示します。
出典