0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MicrosoftのWeb開発教材を使ってみた ⑦-1銀行プロジェクト【SPA/template/HTMLフォーム】

Last updated at Posted at 2022-02-20

はじめに

**「Web Development For Beginners」**というMicrosoftがGithubに公開している教材についての記事です。

教材の紹介・選んだ理由など

この教材を選んだ理由

https://github.com/microsoft/Web-Dev-For-Beginners

  • HTML/CSS/JavaScriptを触れるいい感じの教材が欲しかった
    • そこそこのボリュームがあり、作りながら学べるタイプの教材
    • 基礎的なトピックが一通り網羅されている
  • 質が高そう
    • なにせあのMicrosoftなので、きっと良いものでしょう。
  • 題材が面白そう
    • 軽く調べた感じだとチュートリアルでよくある題材として「TODOアプリ」「クイズアプリ」などがあるみたいですが、どれもどう実装するのか想像がついてしまって、余り興味がわきませんでした。
    • しかしこの教材は「テラリウム」「タイピングゲーム」「ブラウザ拡張機能」「スペースゲーム」「銀行プロジェクト」と、面白そうなトピックが並んでいます。

+α 実際に取り組んで感じたこと

  • 提供されるリファレンス・参考サイトの質が高い
    • 一例はFlexbox Froggy。🐸 を並べながら flexbox の扱いについて学べるサイトです。超わかりやすいです。

https://flexboxfroggy.com/#ja

  • 「アクセシビリティ」「ブラウザがどう動くのか」といった知識も学べる
    • 絶対やるべきだけど後回しにしがちなトピックも結構ガッツリ触れます。
    • かゆいところに手が届く感じ。
  • 多分、英語全くわからなくてもなんとかなる
    • ほとんどのレッスンは translationsというフォルダに日本語訳があります。
    • 最悪全部Deeplに突っ込めばなんとかなります。
  • Edge推しがすごい
    • Microsoftの教材なので当然ですが、デモでは基本Edgeが使われます。
  • スケッチノートがわかりやすい
    • 一部レッスンは最初にスケッチノートというイラストがあるのですが、それがすごくわかりやすいです。それに可愛い。
    • 扱うトピックについてイラストで視覚的に示してくれるので、どんな内容をやるのかざっくり把握してからレッスンに入ることが出来ます。

image.png

microsoft/Web-Dev-For-Beginners/tree/main/1-getting-started-lessons/3-accessibility より

教材の概要

各レッスンに以下の要素が含まれます。

  • スケッチノート(オプション)
    • レッスンの概要がわかりやすくまとまったイラスト
  • 補足のビデオ(オプション)
  • レッスン前の小テスト
    • 簡単なテスト
  • ステップバイステップなレッスン
  • 知識のチェック
  • レッスン後の小テスト
    • 簡単なテスト
  • チャレンジ
  • 副読本(サイト)
  • 復習と自己学習
  • 課題

チャレンジ〜は調べ物や課題をこなします。
課題については必要だと思ったものだけやりました。

教材の構成

  1. getting-started-lessons(はじめに)
    1. プログラミング言語と開発ツール
    2. アクセシビリティ
    3. Githubの基礎
  2. js-basics(JavaScript基礎)
    2. データ型
    2. 関数とメソッド
    2. 分岐処理
    2. ループ
  3. terrarium(テラリウム構築)
    3. HTMLイントロ
    3. CSSイントロ
    3. DOM操作とクロージャ
  4. typing-game(タイピングゲーム)
    4. タイピングゲームを作る(イベント管理)
  5. browser-extension(ブラウザ拡張機能)
    5. ブラウザについて
    5. API呼び出し、ローカルストレージの利用
    5. バックグラウンドタスクとパフォーマンス
  6. space-game(スペースシューティングゲーム)
    6. イントロ(Pub-Subパターン)
    6. キャンバス
    6. モーションの追加
    6. レーザー追加、衝突検出
    6. スコアの保存
    6. 終了と再起動
  7. bank-project(架空の銀行プロジェクト)
    7. WebアプリのHTMLテンプレートとルート
    7. ログインと登録フォームの構築
    7. データの取得と利用方法
    7. 状態管理の概念

取り組む際に気をつけたこと

  • コピペ/写経にならないようにする
    • サンプルコードと実装の解説が一緒になっているので、理解したつもりになってコピペしがちです。
    • まず一通り目を通してから、なるべく自分の頭で考えて実装するようにしました。
  • 全部完璧にやろうとしない
    • 「12週間、24レッスンのカリキュラム」と銘打たれているように、出される課題や副教材を全てこなそうと思うとかなりボリュームがあります。
      • そのため、現時点で必要だと思うカリキュラムにのみ取り組みました。

〜②JavaScript基礎まで【導入/アクセシビリティ/JavaScript の基礎】
③テラリウム構築 【HTML・CSS基礎/DOM操作/クロージャ】
④タイピングゲーム 【JavaScriptのイベント処理】 
⑤-1ブラウザ拡張機能 【ブラウザの仕組み/拡張機能作成の導入】
⑤-2ブラウザ拡張機能 【API/LocalStorage/BackGround/Performance】
⑥スペースシューティングゲーム 【ゲーム開発の基礎/Pub-Sub/Canvas/衝突検出】
⑦-1銀行プロジェクト【SPA/template/HTMLフォーム】 本記事
⑦-2銀行プロジェクト【ログイン/データ管理/状態管理】


記事の目的

  • 学習のアウトプット
  • 教材を使ってみたところかなり良かったので、その紹介

注意点

自身の学習のアウトプットがメインなので、理解できているところ(他言語と共通の箇所など)は省いています。
また、課題やtipsについても結構省きます。
この教材に興味を持った方はぜひご自分で取り組んでみてください。

7 Bank-project

Node.jsを用いて架空の銀行を構築していきます。

image.png

やること

  1. Web アプリの HTML テンプレートとルート
  2. ログインと登録フォームの構築
  3. データの取得と利用方法
  4. 状態管理の概念

学習の目的

  • ルーティングと HTML テンプレートを使ったマルチページサイトのアーキテクチャの足場の作り方を学ぶ
  • フォームの構築と検証ルーチンの渡し方について学ぶ
  • アプリのデータの出入り、データの取得方法、保存方法、廃棄方法
  • アプリの状態を保持する方法とプログラムで管理する方法を学ぶ

HTMLテンプレートとルート

イントロ

ブラウザにJavaScriptが登場して以来、Webサイトはこれまで以上にインタラクティブ・複雑になっている。

インタラクティブとは、相互に作用する、対話的な、双方向の、相乗効果の、などの意味を持つ英単語。ITの分野では、情報の送り手と受け手の関係が固定的ではなく、その場で互いにやり取りできる状態を指す。
情報システムやソフトウェアでは、利用者の操作や入力に対してシステムが即座に反応を返し、相互にやり取りをする中で処理を進めていくような操作方式をインタラクティブであるという。
インタラクティブ - IT用語辞典

Webアプリケーションは高度にインタラクティブ(=ユーザとの対話が多く生じる)なので、アクションが実行される度に全ページがリロードされてしまうと、ユーザを待たせることになってしまう。

そのため、JavaScriptによるDOM操作でHTMLを直接更新し、よりスムーズなUXを提供する。

このレッスンではHTMLテンプレートを使用して、ページ全体をリロードすることなく表示・更新出来る複数の画面を作成、Webアプリ作成の基礎を構築していく。

準備

npx lite-server コマンドを使用してローカルのWebサーバを作成する。

lite-serverって何?

lite-serverのリポジトリ

BrowserSyncは、超高速・軽量の開発サーバーとして、私たちが望むことのほとんどを実現してくれます。静的コンテンツを提供し、変更を検出し、ブラウザをリフレッシュし、多くのカスタマイズを提供します。
SPA を作成する場合、ブラウザにのみ知られているルートがあります。例えば、/customer/21はAngularアプリのクライアントサイドルートかもしれません。このルートを手動で入力したり、Angularアプリのエントリポイントとして直接リンクした場合(別名ディープリンク)、Angularがまだロードされていないため、静的サーバーはリクエストを受け取ります。サーバーはルートにマッチするものを見つけられず、404を返します。この場合の望ましい動作は、index.html(または定義したアプリの開始ページ)を返すことです。BrowserSyncは、自動的にフォールバックページを許可しません。しかし、カスタムミドルウェアを使用することは可能です。そこで、lite-serverの出番です。

lite-serverは、BrowserSyncの周りをカスタマイズしたシンプルなラッパー。
SPAを簡単に提供することができる。

BrowserSyncって何?

テキストエディターでHTML,CSS,JavaScriptのコードを編集して、結果を確認するのにWebブラウザーにフォーカスを移してF5で更新して……
というWeb開発のワークフローを簡素化するプログラムです。
BrowserSyncはファイルの変更を監視して、変更を即座にブラウザーに反映させることができます。
しかもCSSファイルの編集はページの再読み込みをすることなく反映されます。
複数ブラウザーを立ち上げていてもOK!モバイルブラウザーも自動で同期します!

参考 : BrowserSyncの使い方

SPAって何?

シングルページアプリケーションとは、Webアプリケーションの構成法の一つで、Webブラウザ側でページの移動を行わず、最初に読み込んだWebページ上のスクリプトがサーバとの通信や画面遷移を行う方式。
Webサービスとして従来のソフトウェアのような高度な機能を実装する選択肢の一つとしてよく知られており、有力なJavaScriptフレームワークの中にもシングルページアプリケーションの構成を基本とするもの(Angular、Vue.js、Reactなど)がある。
SPA - IT用語辞典

参考 : SPA(SinglePageApplication)の基本 - Qiita

  • その名の通り、1つのHTMLで構成されるWebアプリ。
  • 最初のリクエストのみWebページ全体を読み込む。
  • JavaScriptでDOMを操作して必要な場所(差分)だけ更新する。

従来との違い

従来(サーバーサイドレンダリング)

1. ユーザ操作
2. サーバーにリクエスト
3. サーバーでWEBページを生成
4. クライアント側へWEBページを送信
5. サーバーから受け取ったWEBページを描画

SPA

1. 初期ページ読み込み(初期描画)
2. ユーザ操作
3. サーバーにリクエスト
4. サーバーからクライアント側へ差分データを送信
5. 差分のみを更新

まとめると、

  • BrowserSyncはWebアプリの開発を楽にしてくれる開発サーバー。
  • lite-serverはBrowserSyncをSPAに対応な形にカスタマイズしたラッパー。
  • SPAは1つのHTMLを読み込み、JavaScriptなどにより必要な箇所だけページを更新する手法。

ボイラープレート

以下のHTMLボイラープレートから実装を始める。

<!DOCTYPE html><html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bank App</title>
  </head>
  <body>
    <!-- ここで作業 -->
  </body>
</html>

ボイラープレートとは?

コンピュータプログラミングでは、殆ど、または全く変化することなく、複数の場所で繰り返される定型コードのセクションのこと。冗長な言語を使用する場合、プログラマーはコードを少しだけ書くだけでも多くのコードを作成する必要がある。このような定型コードはボイラープレートと呼ばれる。
ボイラープレートコード - Wikipedia

なお、langは日本ならja
langを設定しておくと自動翻訳の恩恵を受けれる、プログラムが言語を認識してくれる、などのメリットがあるらしい。

HTMLテンプレート

Webページに複数の画面を作成したい場合、通常は表示したい画面ごとに1つのHTMLファイルを作成する。

この手法の問題点として、以下が挙げられる。

  • 画面切り替え時HTML全体を再読み込みする必要があり、非効率
  • 画面間のデータ共有が難しい

もう1つのアプローチとして、

アプリ全体でHTMLファイルを1つだけ持ち、<template>要素を使って複数のHTMLテンプレートを定義する手法が考えられる。

テンプレートはブラウザに表示されない再利用可能なHTMLブロック。
JavaScriptを使い実行時にインスタンス化する。

実装

<div id="app">Loading...</div>

この要素の内容が置き換えられる。
そのため、アプリの読み込み中(=置き換えられていない時)に表示される読み込みメッセージやインジケータを入れておくと良い。

ログインページのHTMLテンプレートを追加する。

<template id="login">
  <h1>Bank App</h1>
  <section>
    <a href="/dashboard">Login</a>
  </section>
</template>

次に、ダッシュボードページ用のHTMLテンプレートを追加する。

  • タイトルとログアウトリンクのあるヘッダー
  • 銀行口座の当座預金残高
  • 表に表示されるトランザクションのリスト
<template id="dashboard">
  <header>
    <h1>Bank App</h1>
    <a href="/login">Logout</a>
  </header>
  <section>
    Balance: 100$
  </section>
  <section>
    <h2>Transactions</h2>
    <table>
      <thead>
        <tr>
          <th>Date</th>
          <th>Object</th>
          <th>Amount</th>
        </tr>
      </thead>
      <tbody></tbody>
    </table>
  </section>
</template>

なお、<template>をコメントアウトすれば実際にどのように見える確認することが出来る。

JavaScriptでテンプレートを表示する

<template> に設定したHTMLをインスタンス化して表示するためにはJavaScriptのコードを追加する必要がある。

通常次の3ステップで行われる。

  1. document.getElementByIdなどを利用してDOM内のテンプレート要素を取得
  2. cloneNode を使用してテンプレート要素のクローンを作成
  3. appendChild などを利用して、可視要素の下のDOMにアタッチする

なぜクローンする必要がある?

クローンせずに template を利用してみる

  • 呼び出す前

#document-fragment がいて、中に要素が入っている。

image.png

  • 呼んだ後

中身がなくなった。
image.png

| MDNを見てみると、以下の記述が。

ただし、 HTMLTemplateElementの contentプロパティは、読み取り専用の DocumentFragmentで、テンプレートが表現する DOM サブツリーを保持しています。

DocumentFragmentについて調べてみる。

DocumentFragment

DocumentFragment - Web API | MDN

  • 直訳すると文書の断片。
  • 基本的な使い方として、以下のような流れで利用する。
    1. 空のDocumentFragment を作る
    2. その中に独立したDOMツリーを構成
    3. appendChild などを利用してDOMに追加する
  • ↑の操作を行うと断片内のノードはDOMに移動、空の DocumentFragment が残る。
  • 大量の要素をDOMに追加したい時などに利用する。

まとめると、

  • template 要素の content プロパティは DocumentFragment を利用している。
  • DocumentFragment は大量の要素をDOMに一気に追加したい時などに使う。
  • 呼び出した際に DocumentFragment の中身は移動してしまうため、複数回呼び出したい時はコピーを作って利用する。

実装

function updateRoute(templateId) {
  const template = document.getElementById(templateId);
  const view = template.content.cloneNode(true);
  const app = document.getElementById('app');
  app.innerHTML = '';
  app.appendChild(view);
}
  • updateRoute 関数では上記の3ステップと同じことをしている。
    1. document.getElementByIdなどを利用してDOM内のテンプレート要素を取得
    2. cloneNode を使用してテンプレート要素のクローンを作成
    3. appendChild などを利用して、可視要素の下のDOMにアタッチする
  • node.cloneNode
    • 構文はvar dupNode = node.cloneNode(deep);
    • 子孫ノードも含めて複製したい場合はdeep引数にtrueを渡す。

✅ innerHTML="" の目的は何?やらないとどうなる?

Element.innerHTML

主な用途は、要素の中身の取得・設定。
つまり、ここでは””を設定することでid=appの要素の中身を削除している。
これをしないと、要素の中身(”Loading...”)が残ったままになってしまう。

ルートの作成

Webアプリの文脈では、URLを表示すべき画面にマッピングする操作をルーティングと呼ぶ。

複数のHTMLファイルを持つWebサイトではファイルパスがURLに反映されるため、自動的に行われる。

mywebsite/index.html
mywebsite/login.html
mywebsite/admin/index.html

ルートに mywebsite を指定して Web サーバを作成した場合、URL のマッピングは以下のようになる。

https://site.com            --> mywebsite/index.html
https://site.com/login.html --> mywebsite/login.html
https://site.com/admin/     --> mywebsite/admin/index.html

しかし、今回作成するWebアプリではすべての画面を含む1つのHTMLファイルを使用しているので、このデフォルトの動作は役に立たない。

そのため、マッピングを手動で作成し、JavaScriptにより表示されるテンプレートの更新を実行する必要がある。

実装

URLパスとテンプレート間のマッピングを実装するために、シンプルなmapを実装する。

連想配列のことをmapと呼ぶこともあるらしい

const routes = {
  '/login': { templateId: 'login' },
  '/dashboard': { templateId: 'dashboard' },
};

さらに、updateRoute関数を少し修正する。

引数にtemplateIdを直接渡すのではなく、現在のURLを見て、rotesにより対応するtemplateIdを取得するようにする。

URLからパス部分だけを取得するには、window.locationpathnameを使う。

// https://qiita.com/settings/profileで使ってみる

console.log(window.location.pathname)
// ==>  /settings/profile
/** HTMLテンプレートの表示を更新する */
function updateRoute() {
  const path = window.location.pathname; // ==> /login or /dashboard
  const route = routes[path];

  const template = document.getElementById(route.templateId);
  const view = template.content.cloneNode(true);
  const app = document.getElementById('app');
  app.innerHTML = '';
  app.appendChild(view);
}

URLを入力してみる。

image.png

image.png

動作はOK。

✅ URLに未知のパスを入力するとどうなる?どうすれば対処できる?

未知のパスを入力するとHTMLは当然更新されず、以下のエラーが発生する。

image.png

対処

未知のパスが入力された
routesに存在しないキーが渡される
routeundefinedになる
undefinedをキャッチして、エラー用のHTMLテンプレートを表示すれば良いのでは?

  • routeserror を追加
const routes = {
    '/login': { templateId: 'login' },
    '/dashboard': { templateId: 'dashboard' },
    '/error': { templateId: 'error' }
};
  • undefined の時の分岐作成
  • ついでにrouteletに変更
let route = routes[path];

if (!route) route = routes['error']
  • 対応するテンプレートを作成
<template id="error">
    <h1>Error!</h1>
    <p>Sorry, something went wrong.</p>
    <p>Check the url and try again.</p>
</template>

image.png

OK。

ナビゲーションの追加

次のステップとして、URLを手動で変更することなくページ間を移動出来るようにする。
これには、次の2つができる必要がある。

  1. 現在のURLを更新
  2. 新しいURLに基づいてテンプレートを更新

2番は既に実装したので、1番を実現する方法を考える。

history.pushState を使う。これはHTMLをリロードせずにURLを更新して、閲覧履歴に新しいエントリを作成できる。

✅ HTMLアンカー要素<a href>は単独でハイパーリンクを作る事ができるが、デフォルトではHTMLをリロードさせる。そのため、preventDefaultを使用する必要がある。

実装

function navigate(path) {
  window.history.pushState({}, path, path);
  updateRoute();
}

この関数は与えられたパスに基づいて現在のURLを更新する。
また、履歴レコードに指定したURLを追加する。

パスが定義されたルートにマッチしない場合、ログインページにリダイレクトするようにする。

function updateRoute() {
  const path = window.location.pathname;
  const route = routes[path];

  if (!route) {
    return navigate('/login');
  }

  ...

ログインにリダイレクトで良かったのか・・・

リンククリック時の動作。
リンクからURLを取得、デフォルトのリンク動作を防ぐ。

/** リンククリック時に実行される */
function onLinkClick(event) {
    // リンクのデフォルト動作(HTML更新)を防ぐ
    event.preventDefault();
    navigate(event.target.href);
}

HTMLにバインディングを追加する。

<a href="/dashboard" onclick="onLinkClick(event)">Login</a>
...
<a href="/login" onclick="onLinkClick(event)">Logout</a>
History.pushState()
構文

history.pushState(state, title[, url])

履歴に指定したURLを追加する。
ページの再読み込みは発生しない。

引数

state
popState イベント(後述)が持つ event.state の値。
履歴レコードとセットで生成されるオブジェクト。

title
理論上はブラウザのタブタイトルを変更するが、現在ほとんどのブラウザで無視されている。

url
ここで指定したURLを追加。

ブラウザの戻るボタン/進むボタンの扱い

戻るボタンを押して履歴を確認してみると、以下のようになっている。

image.png

戻るボタンをクリックしてみると、現在のURLは変更されるが、同じテンプレートが表示され続けている。

これは、このアプリが pushState 呼び出しの度にURLを更新する必要があることは知っているが、
履歴が変わる度に updateRoute を呼び出す必要があることを知らないから。

history.pushStateドキュメントを見てみると、状態が変化した場合にはpopstateイベントが発生することがわかる。コレを使ってこの問題を解決していく。

popstate イベントについて

WindowEventHandlers.onpopstate - Web API | MDN

popstate イベントは、同じ文書の2つの履歴項目の間で、アクティブな履歴項目が変わるたびにウィンドウに発行される。
アクティブな履歴項目が history.pushState() を呼び出したことで作成されたり、 history.replaceState() を呼び出したことで影響されたりした場合、 popstate イベントの state プロパティが履歴項目の状態オブジェクトのコピーを保持する。
メモ : history.pushState() 又は history.replaceState() を呼び出すことは、 popstate イベントのトリガーにはなりません。 popstate イベントは、戻るボタンをクリックしたり (又は JavaScript で history.back() を呼び出したり)、同じ文書で2つの履歴項目間を移動したりするように、ブラウザーのアクションを実行することのみがトリガーになります。

戻る/進むボタンなどでページ遷移した場合、 popState イベントを捕まえてページを更新してやる必要があるということ。

実装

onpopstateupdateRoute をアタッチするだけ。

/**
 * popstateイベント発生時updateRouteを呼び出し。
 * このイベントは戻る/進むボタンによるページ遷移などで発生する。
*/
window.onpopstate = () => updateRoute();
updateRoute();

課題 ルーティングの改善

以下の2機能を追加。routes 宣言で追加された新しいルートでも動作すること。

  • テンプレートが変更された時にタイトルが更新されるようにする
  • ダッシュボードページが表示される度にコンソールに「Dashboard is shown」と表示される

新しいルートでも動作するようにする

/** HTMLテンプレートの表示を更新する */
function updateRoute() {
    const path = window.location.pathname;
    const route = routes[path];
    // 未知のパスが入力された場合ログインページにリダイレクトする
    if (!route) return navigate('/login')

    const template = document.getElementById(route.templateId);

updateRoute (上記)を参考に、 route.templateId を参照する。

route.templateId を複数回使うので 出来ればtemplateId を変数として参照したい。

だが、存在しない routetemplateId を探しに行くと Uncaught TypeError: Cannot read properties of undefined (reading 'templateId') が出る。このエラーをキャッチしてまで templateId を変数に入れる必要はない・・・と思う。

タイトルの更新

document.title で取得・更新出来る。

image.png

/** ページタイトルを更新する */
function updateTitle(route) {
    document.title = route.templateId;
}

後は updateRoute 内で呼び出せば良い。

ダッシュボード表示の通知

同じように route.tempalteId を利用、 updateRoute 内で呼び出す。
関数名は少し迷ったが、「ダッシュボードの表示を通知」とした。

/** ダッシュボード表示時、コンソールに通知 */
function notifyDashboardDisplay(route) {
    if (route.templateId === 'dashboard') console.log('Dashboard is shown.');
}

image.png

OK。


ログインと登録フォームの構築

イントロダクション

最近のWebアプリは、複数のユーザが同時にアクセスできることから、各ユーザの個人方法を個別に保存・どの情報を表示するか選択する仕組みが必要となる。

そこで、各ユーザーがアプリ上で銀行口座を作成できるようにする。
このパートでは、HTMLフォームを使用してログイン・登録機能を追加する。
その中で、プログラムでサーバーAPIにデータを送信する方法、ユーザ入力の基本的な検証ルールを定義する方法を見ていく。

銀行アプリのサーバーAPIは既に用意されているので、それを利用する。

銀行APIの詳細

image.png

  1. api フォルダに移動
  2. npm install
  3. npm start → サーバーはポート5000で待ち受けを開始する。

curl http://localhost:5000/api を実行することで、サーバーが正常に動作していることを確認できる。

curl http://localhost:5000/api
# -> "Bank API v1.0.0"ならOK

銀行APIはNode.js+Expressで構築されています。

フォームとコントロール

<form> 要素を使うことでHTMLのセクションをカプセル化し、
ユーザがUIコントロールを利用してデータを入力・送信出来るようになる。

<form> で使われる中で最も一般的なUIは <input> <button>

input

例えば、<input>を使えばユーザ名を入力できるフィールドを作成できる。

<input id="username" name="username" type="text">

参考 : <input>: 入力欄(フォーム入力)要素 - MDN

  • name : フォームデータを送信する際のプロパティとして利用
  • id : <label>をフォームコントロールに関連付けるために使用
    • 要素 >> ラベルについて - MDN
    • <input> <label> を関連付けると、ラベルと入力欄を結びつける事ができる=スクリーンリーダーが正確に入力欄を説明できるようになる
<!-- アクセシブルではない -->
<p>名前を入力してください: <input id="name" type="text" size="30"></p>

<!-- 暗黙的なラベル -->
<p><label>名前を入力してください: <input id="name" type="text" size="30"></label></p>

<!-- 明示的なラベル -->
<p><label for="name">名前を入力してください: </label><input id="name" type="text" size="30"></p>

✅  は Empty element(空要素) - MDN であることに注意。
空要素とは、子ノードを持つことが出来ないもの。閉じタグは基本禁止。

button

<button> 要素は少し特殊。
type 属性を指定しないと、ボタンが押されたときに自動的にフォームデータをサーバに送信する。

以下にtype の値を示す。

  • submit<form>内のデフォルトの値で、ボタンはフォームの送信アクションをトリガーする
  • reset : ボタンはすべてのフォームコントロールを初期値にリセットする
  • button : ボタンが押されたときのデフォルトの動作を割り当てないで、JavaScript を使ってカスタムアクションを割り当てることができる

実装

<template id="login">
  <h1>Bank App</h1>
  <section>
    <h2>Login</h2>
    <form id="loginForm">
      <label for="username">Username</label>
      <input id="username" name="user" type="text">
      <button>Login</button>
    </form>
  </section>
</template>

ラベル要素の導入には以下のようなメリットがある。

  • フォームが読みやすくなる
  • スクリーンリーダーユーザーに役立つ
  • ラベルをクリックすると関連する入力に直接フォーカスできる → タッチスクリーンベースのデバイスでも操作しやすい

ログインフォームの下に登録フォームを置く。

<h2>Register</h2>
<form id="registerForm">
  <label for="user">Username</label>
  <input id="user" name="user" type="text">
  <label for="currency">Currency</label>
  <input id="currency" name="currency" type="text" value="$">
  <label for="description">Description</label>
  <input id="description" name="description" type="text">
  <label for="balance">Current balance</label>
  <input id="balance" name="balance" type="number" value="0">
  <button>Register</button>
</form>
  • value により与えられた入力に対してデフォルト値を定義できる
  • balance の入力は number

サーバーへのデータ送信

標準的なUIができたので、次にデータをサーバへ送信する。
現在のUIを使って値を送信してみると、URLが次のように変化する。

after-input-url

<form> のデフォルトのアクションは、「現在のサーバのURLにGETメソッドを使って送信、フォームのデータをURLに直接追加」。
これにはいくつか欠点がある。

  • 送信されるデータサイズの制限(2000文字程度)
  • データをURLで直接見れてしまう(パスワードなどの漏洩)
  • ファイルのアップロードでは動作しない

これらの制限を受けずデータをサーバに送信するため、POSTメソッドを使うようにする。

✅ POST はデータを送信するために最も一般的に使用される方法だが、いくつかの特定の場合GET を使用するほうが望ましい。例えば、検索フィールドの実装。

実装

登録フォームに action method を追加する。

<form id="registerForm" action="//localhost:5000/api/accounts" method="POST">

登録フォームを利用して情報を送信してみる。
すると、JSONレスポンスが表示される。(サーバにリダイレクトされちゃってるけど)

image.png

なお、既存のユーザ名を入れたらちゃんとエラーを吐いた。
image.png

ページを再読み込みせずデータを送信する

上で使用したアプローチには少し問題がある。
フォームを送信するときサーバのURLにリダイレクトされること。

ページのリロードを強制せずフォームデータをサーバに送信するために、JavaScriptを使用する。
<form>action にURLを記述する代わりに、javascript: で任意のJavaScriptコードを使用する。

これを使う場合、これまでブラウザが自動的に行なっていた

  • フォームデータ取得
  • フォームデータを適切なフォーマットに変換、エンコードする
  • HTTPリクエストを作成してサーバに送信

を自力で実装する必要がある。

実装

<form id="registerForm" action="javascript:register()">
function register() {
  const registerForm = document.getElementById('registerForm');
  const formData = new FormData(registerForm);
  const data = Object.fromEntries(formData);
  const jsonData = JSON.stringify(data);
}

やっていること

  • getElementById でフォーム要素取得

  • FormData を使ってフォームからキー:値のペアを取得

  • Object.fromEntriesを使用してデータを通常のオブジェクトに変換

    • キーと値の組み合わせのリストをオブジェクトに変換している
  • 最後に、データをJSONにシリアライズ

    • シリアライズとは

    オブジェクトまたはデータ構造が、ネットワークまたはストレージ(例えば、アレイバッファまたはファイルフォーマット)上の転送に適したフォーマットに変換されるプロセス。
    たとえば JavaScript では、JSON.stringify()を呼び出して、オブジェクトを JSON文字列にシリアライズできます。

    Serialization - MDN

POSTでユーザを登録するため、createAccount という関数を作成。

async function createAccount(account) {
  try {
    const response = await fetch('//localhost:5000/api/accounts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: account
    });
    return await response.json();
  } catch (error) {
    return { error: error.message || 'Unknown error' };
  }
}
fetchAPIの利用

JSONデータをサーバに送信するためにfetchを使う。
fetchは2つの引数を取る。

  • サーバのURL
  • リクエストの設定
    • メソッドをPOSTに
    • リクエストのための body を提供
    • JSONをサーバに送りたいので、Content-type ヘッダを application/json に設定
response.json

サーバはリクエストに対してJSONで応答するので、await response.json()を使ってJSONの内容を解析し、結果のオブジェクトを返す。

json メソッドResponseオブジェクト(Fetchのインターフェース、レスポンスを表す) のbody を解析、JavaScriptのオブジェクトとして返す。

実装続き

次に、register関数にコードを追加してcreateAccountを呼び出す。

const result = await createAccount(jsonData);

ここではawaitを利用しているので、register関数の前にasyncキーワードを追加する。
最後に、結果を確認するためのログを追加する。

最終的なコードは以下のようになる。

async function register() {
  const registerForm = document.getElementById('registerForm');
  const formData = new FormData(registerForm);
  const jsonData = JSON.stringify(Object.fromEntries(formData));
  const result = await createAccount(jsonData);

  if (result.error) {
    return console.log('An error occured:', result.error);
  }

  console.log('Account created!', result);
}

データの検証

ユーザ名を設定せず新規アカウントを登録しようとすると、サーバがエラーを返している事がわかる。

サーバにデータを送信する前にフォームデータの検証を行い、有効なリクエストを送信していることを確認すると良い。
HTMLフォームコントロールは、組み込みのバリデーションを複数提供している。

required: フィールドには入力する必要があります。そうでない場合は、フォームを送信することができません
minlength と maxlength: テキストフィールドの最小文字数と最大文字数を定義します
min と max: 数値フィールドの最小値と最大値を定義します
typenumberemailfile や その他の組み込み型 のような、期待されるデータの種類を定義します。この属性はフォームコントロールの視覚的なレンダリングを変更することもできます
pattern: これを使用すると、入力されたデータが有効かどうかをテストするための 正規表現 パターンを定義することができます
ヒント: CSS 疑似クラス :validと :invalidを利用して、フォームコントロールの見た目を有効か無効かによってカスタマイズすることができます。

実装

ユーザ名と通貨は必須フィールドで、その他は任意。

requiredフォームラベルのテキスト の両方を使用して必須パラメータであることを示す。

<label for="user">Username (required)</label>
<input id="user" name="user" type="text" required>
...
<label for="currency">Currency (required)</label>
<input id="currency" name="currency" type="text" value="$" required>

ユーザの入力に対して制限をつける。

<input id="user" name="user" type="text" maxlength="20" required>
...
<input id="currency" name="currency" type="text" value="$" maxlength="5" required>
...
<input id="description" name="description" type="text" maxlength="100">

サーバにデータを送信する前に実行されるバリデーションのことをクライアントサイドのバリデーションと呼ぶ。

ただし、サーバにデータを送信せずに全てのチェックを実行できるとは限らないことに注意。例えば、サーバにリクエストを送らずに同じユーザ名のアカウントが存在するかどうかを確認することは出来ない。

このようなサーバー上で実行される追加のバリデーションはサーバーサイドのバリデーションと呼ばれる。

通常は両方を実装する必要がある。

  • クライアントサイドのバリデーションを使用するとUXが向上する。
  • サーバーサイドのバリデーションは操作するユーザデータが健全で安全であることを確認するため非常に重要。

CodePenを利用して色々なformを見てみると良い。

課題 スタイルを設定する

Before

image.png

After

image.png

  • flex使用
  • label を小さく

学んだこと

  • SPAの概要
  • <template>DocumentFragmentの利用
  • ルーティング、ナビゲーション
  • popstateイベントの利用
  • フォームとUIコントロール
  • サーバーへのデータ送信
  • クライアントサイドのバリデーション
0
0
1

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?