0
0

フロントエンド勉強会 DOM操作

Last updated at Posted at 2024-03-05

概要

フロントエンド勉強会
各課題を通して、DOM操作の感覚を掴む。

環境構築

parcelを用いるのでtsconfigは別に設定しなくていいのだが、私好みの設定があるので入れてある。

npm init -y
npm install parcel typescript
npx tsc --init
mkdir src
touch src/index.ts src/index.html

HTMLテンプレート

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>title</title>
    <script src="./index.ts" async></script>
</head>
<body>
    <div id="main"></div>
</body>
</html>

動作確認方法

npx parcel src/index.html --open

課題

アイテム追加

「追加」ボタンを押下することで、フォームの入力値をリストに追加できる。

スクリーンショット 2023-11-03 0.30.19.png

HTML

<div id="main">
  <label for="new-item">アイテム</label>
  <input type="text" id="new-item" />
  <button id="my-button">追加</button>
  <div id="append-item-box"></div>
</div>

TypeScript

function appendDom(): void {
  const appendItemBox = document.getElementById('append-item-box')!;
  const itemInput = document.getElementById('new-item')! as HTMLInputElement;
  const pElm = document.createElement('p');
  pElm.textContent = `- ${itemInput.value}`;
  appendItemBox.appendChild(pElm);
}

window.addEventListener('load', () => {
  const myButton = document.getElementById('my-button');
  if (myButton !== null) {
    myButton.addEventListener('click', () => {
      appendDom();
    });
  } else {
    window.alert('ボタンがありません!');
  }
});

addEventListenerについて

addEventListenerにあるようにターゲットに特定のイベントが配信されるたびに呼び出される関数を設定する。
第一引数に「特定のイベント」、第二引数に「イベントが配信されるたびに呼び出される関数」を与える。
このメソッドはwindowで指定するブラウザ自体にも、DOMツリー全体を示すdocumentにも、要素それぞれのElementにもメソッドとして存在する。

特定のイベントとして用意されているのはイベントリファレンスにある通り。
ここでは

  • ページがロードされた後に発火する load
  • 要素がクリックされた時に発火する click

の2つを定義している。

ただし、このloadイベントについては、

<script src="./index.ts" defer></script>

のように scriptdefer をつける場合、必要ないことのほうが多い。

textContentinnerHTML

「textContent innerHTML」みたいに検索をかければ山ほど違いに関する記述が出てくるため飛ばすが、textContentを用いている理由はXSSを防ぐため。

注意

ここでのwindowdocumentWEB APIで、JavaScript自体の文法ではなく、ブラウザに組み込まれた機能なので注意。

補足

const myButton = document.getElementById('my-button');
if (myButton !== null) {
    myButton.addEventListener('click', () => {
        appendDom();
    });
} else {
    window.alert('ボタンがありません!');
}

ここでのみ id="my-button" が存在するかを見ている。
getElementByIdはIDが存在しないときnullを返却するため、返り値の型定義は HTMLElement | null である。
したがって型ガードを行っている。
なければalertを出している。

const appendItemBox = document.getElementById('append-item-box')!;
const itemInput = document.getElementById('new-item')! as HTMLInputElement;

それ以外は 面倒なので あるもの前提としてnon nullアサーション演算子でnullを消している。
また、getElementByIdはHTMLElement型で返却するため、より具体的な型にアサーションしている。

ちなみにだが、ここではID指定で取ってきているが、classnameでも取れる。
さらにはクエリセレクタでも取れる。

HTML**Element

確認の仕方はいくつかあるが2つ紹介。
例えばdiv要素。

  1. MDNを見る

div技術的概要
のDOM インターフェイスに書かれている。

スクリーンショット 2023-11-03 0.53.18.png

  1. createElementを用いて確認

スクリーンショット 2023-11-03 0.54.44.png

カウンター

+を押したら表示された数字がインクリメント、-を押すとデクリメントされる。
特に処理はしていないので負の値も入る。

スクリーンショット 2023-11-03 0.57.09.png

HTML

<div id="main">
  <div>
    <p>現在値</p>
    <p id="currentCount"></p>
  </div>
  <button id="decrement">-</button>
  <button id="increment">+</button>
</div>

TypeScript

window.addEventListener('load', () => {
  let currentCount = 0;
  const incrementButton = document.getElementById('increment')! as HTMLButtonElement;
  const decrementButton = document.getElementById('decrement')! as HTMLButtonElement;
  const currentCountRef = document.getElementById('currentCount')! as HTMLParagraphElement;

  currentCountRef.textContent = `${currentCount}`

  incrementButton.addEventListener('click', () => {
    currentCount++;
    currentCountRef.textContent = `${currentCount}`;
  });

  decrementButton.addEventListener('click', () => {
    currentCount--;
    currentCountRef.textContent = `${currentCount}`;
  });
});

Getter, Setterを使ったバージョン

TypeScript(HTMLは同じ)

window.addEventListener('load', () => {
  const incrementButton = document.getElementById('increment')! as HTMLButtonElement;
  const decrementButton = document.getElementById('decrement')! as HTMLButtonElement;

  const currentValueOrigin = {
    c: 0,
    currentCountRef: document.getElementById('currentCount')! as HTMLParagraphElement,
    get cnt(): number {
      return this.c;
    },
    set cnt(value: number) {
      this.c = value;
      this.currentCountRef.textContent = `${this.c}`;
    }
  };

  currentValueOrigin.cnt = 0;

  incrementButton.addEventListener('click', () => {
    const currentCount = currentValueOrigin.cnt;
    currentValueOrigin.cnt = currentCount + 1;
  });

  decrementButton.addEventListener('click', () => {
    const currentCount = currentValueOrigin.cnt;
    currentValueOrigin.cnt = currentCount - 1;
  });
});

this

thisは同オブジェクトを指す。
getおよびsetでは同じオブジェクトが持つ ccurrentCountRef にアクセスしたい。
しかし ccurrentCountRef とそのまま書いても、そのような変数は定義されていないので取ってこれない。
そこで、 this.cthis.currentCountRef として、同オブジェクトのプロパティ(メソッド)アクセスを行う。

バリデーション

入力された値に応じてバリデーション結果を出力する。

何も入力していない場合

スクリーンショット 2023-11-03 1.14.08.png

英大文字、英小文字、数字のみで構成された文字列が入力されている場合

スクリーンショット 2023-11-03 1.15.47.png

英大文字、英小文字、数字以外も文字列に含まれる場合

スクリーンショット 2023-11-03 1.16.04.png

HTML

<div id="main">
	<label for="my-input">入力(英大文字、英小文字、数字のみ)</label>
	<input type="text" id="my-input" />
	<div id="validate-hint"></div>
</div>

TypeScript

window.addEventListener('load', () => {
  const inputForm = document.getElementById('my-input')!;
  const validateHint = document.getElementById('validate-hint')!;
  inputForm.addEventListener('input', (evt) => {
    const target = evt.target;
    if (target) {
      const inputValue = (target as HTMLInputElement).value;
      if (inputValue === '') {
        validateHint.textContent = '';
      } else if (/^[A-Za-z0-9]+$/.test(inputValue)) {
        validateHint.textContent = '正常';
      } else {
        validateHint.textContent = '英大文字、英小文字、数字以外を入力しないでください。';
      }
    }
  })
});

inputイベント

入力の変更のたびにバリデーションを変えたいのでinputイベント
changeイベントもあるが、フォームからフォーカスが外れたタイミングだったりでないと発火しないので今回はinputイベントが適切。

また、addEventListenerでは第二引数にEventインターフェースの引数が与えられる。
その中にtargetがあり、ここにaddEventListenerを定義した要素の型が入るのだが、ドキュメントにある通りEventTargetとなっている。このままではHTMLInputElementにあるvalueを引っ張ってこれないのでキャストしている。

テキストエリアに改行区切りで入力されたものをリスト表示

タイトル通り。
改行区切りで文字をリスト表示する。
改行後何も入力されていなければリストに追加はしない。

スクリーンショット 2023-11-03 1.31.00.png

HTML

<div id="main">
    <label for="multi-input-area">リスト分割</label>
    <textarea id="multi-input-area" rows="5" cols="40"> </textarea>

    <div>リスト</div>
    <ul id="list"></ul>
</div>

TypeScript

function removeAllChild(el: HTMLElement) {
  while (el.firstChild) {
    el.removeChild(el.firstChild);
  }
}

window.addEventListener('load', () => {
  const textarea = document.getElementById('multi-input-area')! as HTMLTextAreaElement;
  const listBox = document.getElementById('list')! as HTMLUListElement;
  textarea.addEventListener('input', (evt) => {
    const target = evt.target;
    if (target) {
      removeAllChild(listBox);
      (target as HTMLTextAreaElement)
        .value
        .split('\n')
        .filter((val) => val.trim())
        .forEach((val) => {
          const li = document.createElement('li');
          li.textContent = val;
          listBox.appendChild(li);
        });
    }
  })
});

removeAllChild

function removeAllChild(el: HTMLElement) {
  while (el.firstChild) {
    el.removeChild(el.firstChild);
  }
}

要素内の子要素を全て消している。
removeChildで対象子要素の削除をwhilefirstChildが取れる限りぶん回している。

ちなみに、このfirstChildはCSSにも擬似クラスとして存在する。

入力されるたびにリストのDOMを全て作り直すのはかなり重たい処理になるため、昨今のUIフレームワークでは変更された箇所のみを書き換えるような仕組みになっている。
本来であれば本記事においてもそのような形にすべきなのだが、要素の削除について盛り込みたかったためそちらを優先した。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0