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

# 概要

[フロントエンド勉強会](https://qiita.com/masayasviel/private/0e5a403388509e98313d)
各課題を通して、[DOM](https://developer.mozilla.org/ja/docs/Glossary/DOM)操作の感覚を掴む。

# 環境構築

[parcel](https://ja.parceljs.org/)を用いるので[tsconfig](https://typescriptbook.jp/reference/tsconfig/tsconfig.json-settings)は別に設定しなくていいのだが、私好みの設定があるので入れてある。

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

## 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>
```

# 動作確認方法

```sh
npx parcel src/index.html --open
```

# 課題
## アイテム追加

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

![スクリーンショット 2023-11-03 0.30.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/2fd5abec-dd4a-e9e7-7b21-0266ca521574.png)

HTML

```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

```ts
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](https://developer.mozilla.org/ja/docs/Web/API/EventTarget/addEventListener)にあるようにターゲットに特定のイベントが配信されるたびに呼び出される関数を設定する。
第一引数に「特定のイベント」、第二引数に「イベントが配信されるたびに呼び出される関数」を与える。
このメソッドは[window](https://developer.mozilla.org/ja/docs/Web/API/Window)で指定するブラウザ自体にも、DOMツリー全体を示す[document](https://developer.mozilla.org/ja/docs/Web/API/Document)にも、要素それぞれの[Element](https://developer.mozilla.org/ja/docs/Web/API/Element)にもメソッドとして存在する。

特定のイベントとして用意されているのは[イベントリファレンス](https://developer.mozilla.org/ja/docs/Web/Events)にある通り。
ここでは

- ページがロードされた後に発火する [load](https://developer.mozilla.org/ja/docs/Web/API/Window/load_event)
- 要素がクリックされた時に発火する [click](https://developer.mozilla.org/ja/docs/Web/API/Element/click_event)

の2つを定義している。

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

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

 のように [script](https://developer.mozilla.org/ja/docs/Web/HTML/Element/script) に `defer` をつける場合、必要ないことのほうが多い。

### [textContent](https://developer.mozilla.org/ja/docs/Web/API/Node/textContent)と[innerHTML](https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML)

「textContent　innerHTML」みたいに検索をかければ山ほど違いに関する記述が出てくるため飛ばすが、textContentを用いている理由は[XSS](https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html)を防ぐため。

### 注意

ここでの[window](https://developer.mozilla.org/ja/docs/Web/API/Window)や[document](https://developer.mozilla.org/ja/docs/Web/API/Document)は[WEB API](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs)で、JavaScript自体の文法ではなく、ブラウザに組み込まれた機能なので注意。

### 補足

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

ここでのみ `id="my-button"` が存在するかを見ている。
[getElementById](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById)はIDが存在しないときnullを返却するため、返り値の型定義は `HTMLElement | null` である。
したがって型ガードを行っている。
なければ[alert](https://developer.mozilla.org/ja/docs/Web/API/Window/alert)を出している。

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

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

ちなみにだが、ここではID指定で取ってきているが、[class](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementsByClassName)や[name](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementsByName)でも取れる。
さらには[クエリセレクタ](https://developer.mozilla.org/ja/docs/Web/API/Document/querySelector)でも取れる。

### HTML**Element

確認の仕方はいくつかあるが2つ紹介。
例えば[div](https://developer.mozilla.org/ja/docs/Web/HTML/Element/div)要素。

1. MDNを見る

[div技術的概要](https://developer.mozilla.org/ja/docs/Web/HTML/Element/div#%E6%8A%80%E8%A1%93%E7%9A%84%E6%A6%82%E8%A6%81)
のDOM インターフェイスに書かれている。

![スクリーンショット 2023-11-03 0.53.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/1635ac12-3e06-5155-1450-55f578cffe4e.png)

2. [createElement](https://developer.mozilla.org/ja/docs/Web/API/Document/createElement)を用いて確認

![スクリーンショット 2023-11-03 0.54.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/a3d945f3-adad-4709-1faf-0726c83b93b7.png)

## カウンター

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

![スクリーンショット 2023-11-03 0.57.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/392b1a36-154a-655e-3fe8-a81ae4ca37b7.png)

HTML

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

TypeScript

```ts
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}`;
  });
});
```

- [変数と宣言](https://jsprimer.net/basic/variables/)
- [インクリメント](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Increment)
- [デクリメント](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Decrement)

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

TypeScript(HTMLは同じ)

```ts
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;
  });
});
```

- [getter](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/get)
- [setter](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/set)
- [Vue.js リアクティビティーの探求](https://ja.vuejs.org/guide/extras/reactivity-in-depth.html)

### [this](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this)

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

## バリデーション

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

何も入力していない場合

![スクリーンショット 2023-11-03 1.14.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/4513b563-0c4f-4d73-b9fb-cb9484047a7e.png)

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

![スクリーンショット 2023-11-03 1.15.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/4c1057fe-7a97-bcfe-a842-37644ece738b.png)

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

![スクリーンショット 2023-11-03 1.16.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/cc633bee-d8e8-1ab2-9225-d4d608b72477.png)

HTML

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

TypeScript

```ts
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 = '英大文字、英小文字、数字以外を入力しないでください。';
      }
    }
  })
});
```

- [正規表現](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_expressions)
- [RegExp.test](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test)

### inputイベント

入力の変更のたびにバリデーションを変えたいので[inputイベント](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/input_event)。
[changeイベント](https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/change_event)もあるが、フォームからフォーカスが外れたタイミングだったりでないと発火しないので今回はinputイベントが適切。

また、addEventListenerでは第二引数に[Event](https://developer.mozilla.org/ja/docs/Web/API/Event)インターフェースの引数が与えられる。
その中に[target](https://developer.mozilla.org/ja/docs/Web/API/Event/target)があり、ここにaddEventListenerを定義した要素の型が入るのだが、ドキュメントにある通り[EventTarget](https://developer.mozilla.org/ja/docs/Web/API/EventTarget)となっている。このままでは[HTMLInputElement](https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement)にあるvalueを引っ張ってこれないのでキャストしている。

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

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

![スクリーンショット 2023-11-03 1.31.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299962/a2006043-cf5c-d6c5-9712-b592357afb71.png)


HTML

```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

```ts
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);
        });
    }
  })
});
```

- `\n` : 改行コード
- [appendChild](https://developer.mozilla.org/ja/docs/Web/API/Node/appendChild)


### removeAllChild

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

要素内の子要素を全て消している。
[removeChild](https://developer.mozilla.org/ja/docs/Web/API/Node/removeChild)で対象子要素の削除を[while](https://jsprimer.net/basic/loop/#while-statement)で[firstChild](https://developer.mozilla.org/ja/docs/Web/API/Node/firstChild)が取れる限りぶん回している。

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

- [:first-child](https://developer.mozilla.org/ja/docs/Web/CSS/:first-child)

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