Notes

はじめに

  • これまで、HTMLとCSSを使って簡単なWebページを構築する方法、GitとGitHubを使ってコードの変更を追跡し、他の人と共同作業するための方法について説明してきました。​また、Pythonプログラミング言語に慣れ、Djangoを使用してWebアプリケーションを作成し始め、Djangoモデルを使用してサイトに情報を保存する方法を学びました。​次にJavaScriptを紹介し、それを使ってWebページをよりインタラクティブにする方法を学びました。
  • 本日は、JavaScriptとCSSを使用してサイトをさらに使いやすくするユーザインターフェース設計の共通規則について説明します。

ユーザインターフェース

ユーザインターフェイスは、Webページへの訪問者がそのページと対話する方法です。​Web開発者としての私たちの目標は、これらのインタラクションをユーザにとってできるだけ快適なものにすることであり、多くの手法があります。

シングルページアプリケーション

以前は、複数のページを持つWebサイトが必要な場合は、Djangoアプリケーションで異なるルートを使用して実現していましたが、今回から、1つのページのみをロードし、JavaScriptを使用してDOMを操作できるようになりました。これを行う主な利点の1つは、実際に変更されるページの部分のみを変更する必要があることです。​たとえば、現在のページに基づいて変更されないナビゲーションバーがある場合、ページの新しい部分に切り替えるたびにそのナビゲーションバーをレンダリングし直す必要はありません。

​JavaScriptでページ切り替えをシミュレートする例を見てみましょう。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Single Page</title>
        <style>
            div {
                display: none;
            }
        </style>
        <script src="singlepage.js"></script>
    </head>
    <body>
        <button data-page="page1">Page 1</button>
        <button data-page="page2">Page 2</button>
        <button data-page="page3">Page 3</button>
        <div id="page1">
            <h1>This is page 1</h1>
        </div>
        <div id="page2">
            <h1>This is page 2</h1>
        </div>
        <div id="page3">
            <h1>This is page 3</h1>
        </div>
    </body>
</html>

上記のHTMLには、3つのボタンと3つのdivがあることに注意してください。現時点では、divにはほんの少しのテキストしか含まれていませんが、各divには、このサイトの1ページのコンテンツが含まれていると考えることができます。次に、ボタンを使用してページを切り替えるJavaScriptを追加します。

// Shows one page and hides the other two
function showPage(page) {

    // Hide all of the divs:
    document.querySelectorAll('div').forEach(div => {
        div.style.display = 'none';
    });

    // Show the div provided in the argument
    document.querySelector(`#${page}`).style.display = 'block';
}

// Wait for page to loaded:
document.addEventListener('DOMContentLoaded', function() {

    // Select all buttons
    document.querySelectorAll('button').forEach(button => {

        // When a button is clicked, switch to that page
        button.onclick = function() {
            showPage(this.dataset.page);
        }
    })
});
single page 1

多くの場合、サイトに初めてアクセスするときに各ページのコンテンツ全体をロードするのは非効率的なため、新しいデータにアクセスするにはサーバを使用する必要があります。たとえば、ニュースサイトを初めて訪問したときに、利用可能なすべての記事を読み込む必要がある場合、サイトの読み込みに時間がかかりすぎます。この問題は、前の講義で為替レートをロードするときに使用したのと同様の方法を使用して回避できます。今回は、Djangoを使用して1ページのアプリケーションから情報を送受信する方法を見てみましょう。この仕組みを説明するために、単純なDjangoアプリケーションを見てみましょう。urls.py には2つのURLパターンがあります。

urlpatterns = [
    path("", views.index, name="index"),
    path("sections/<int:num>", views.section, name="section")
]

views.py に2つの対応するルートがあります。section ルートは整数intを受け取り、その整数に基づいたテキスト文字列をHTTP応答として返すことに注意してください。

from django.http import Http404, HttpResponse
from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request, "singlepage/index.html")

# The texts are much longer in reality, but have
# been shortened here to save space
texts = ["Text 1", "Text 2", "Text 3"]

def section(request, num):
    if 1 <= num <= 3:
        return HttpResponse(texts[num - 1])
    else:
        raise Http404("No such section")

index.html ファイルでは、前のレッスンで学習したAJAXを利用して、特定のセクションのテキストを取得して画面に表示するようサーバに要求します。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Single Page</title>
        <style>
        </style>
        <script>

            // Shows given section
            function showSection(section) {
                
                // Find section text from server
                fetch(`/sections/${section}`)
                .then(response => response.text())
                .then(text => {
                    // Log text and display on page
                    console.log(text);
                    document.querySelector('#content').innerHTML = text;
                });
            }

            document.addEventListener('DOMContentLoaded', function() {
                // Add button functionality
                document.querySelectorAll('button').forEach(button => {
                    button.onclick = function() {
                        showSection(this.dataset.section);
                    };
                });
            });
        </script>
    </head>
    <body>
        <h1>Hello!</h1>
        <button data-section="1">Section 1</button>
        <button data-section="2">Section 2</button>
        <button data-section="3">Section 3</button>
        <div id="content">
        </div>
    </body>
</html>
Single page 2

これで、HTMLページ全体を再ロードすることなく、サーバから新しいデータをロードできるサイトを作成できました。

しかし、私たちのサイトの欠点の1つは、URLの情報が少なくなったことです。上のビデオを見ればわかるように、セクションを切り替えてもURLは変わりません。 JavaScriptHistory API を使用すると、この問題を解決できます。このAPIを使用すると、ブラウザ履歴に情報をプッシュし、URLを手動で更新できます。このAPIを使用する方法を見てみましょう。前のものと同じDjangoプロジェクトがありますが、今回はHistory APIを使用するようにスクリプトを変更したいと考えています。

// When back arrow is clicked, show previous section
window.onpopstate = function(event) {
    console.log(event.state.section);
    showSection(event.state.section);
}

function showSection(section) {
    fetch(`/sections/${section}`)
    .then(response => response.text())
    .then(text => {
        console.log(text);
        document.querySelector('#content').innerHTML = text;
    });

}

document.addEventListener('DOMContentLoaded', function() {
    document.querySelectorAll('button').forEach(button => {
        button.onclick = function() {
            const section = this.dataset.section;

            // Add the current state to the history
            history.pushState({section: section}, "", `section${section}`);
            showSection(section);
        };
    });
});

上記の showSection 関数では、history.pushState 関数を使用しています。この関数は、3つの引数に基づいて、ブラウズ履歴に新しい要素を追加します。

  1. 状態(state)に関連付けられたデータ
  2. ほとんどのWebブラウザで無視されるタイトルパラメータ
  3. URLの表示内容

上記のJavaScriptで行ったもう1つの変更は、onpopstate パラメーターの設定です。このパラメーターは、ユーザが戻る矢印をクリックしたときに何を行うかを指定します。ここでは、ボタンを押したときに前のセクションを表示します。これでサイトは少し使いやすくなりました。

single page with URL change

スクロール

ブラウザーの履歴を更新してアクセスするために、windowと呼ばれる重要なJavaScriptオブジェクトを使用しました。他にも、例えば、以下のような、サイトを見やすくすることができる、windowプロパティがあります。

  • window.innerWidth:ウィンドウの幅 (ピクセル単位)
  • window.innerHeight:ウィンドウの高さ (ピクセル単位)
inner measures

ウィンドウは現在ユーザに表示されている内容を表しますが、document はWebページ全体を参照します。多くの場合、ウィンドウよりもはるかに大きいため、ユーザはページのコンテンツを表示するために上下にスクロールする必要があります。スクロールを操作するには、他の変数にアクセスします。

  • window.scrollY:ページの先頭からスクロールしたピクセル数
  • document.body.offsetHeight:ドキュメント全体の高さ (ピクセル単位) 
Scrolling measures

これらの測定したサイズを次のように比較すると window.scrollY + window.innerHeight >= document.body.offsetHeight 、ユーザがページの最後までスクロールしたかどうかを判断できます。たとえば、次のページでは、ページの下部に達したときに背景色が緑色に変わります。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Scroll</title>
        <script>

            // Event listener for scrolling
            window.onscroll = () => {

                // Check if we're at the bottom
                if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {

                    // Change color to green
                    document.querySelector('body').style.background = 'green';
                } else {

                    // Change color to white
                    document.querySelector('body').style.background = 'white';
                }

            };

        </script>
    </head>
    <body>
        <p>1</p>
        <p>2</p>
        <!-- More paragraphs left out to save space -->
        <p>99</p>
        <p>100</p>
    </body>
</html>
scroll green white

無限スクロール

ページの最後で背景色を変更できても、あまり使い道がありませんが、無限スクロールを実装する場合は、ページの最後であることを検出する必要があります。たとえば、ソーシャルメディアサイトで、すべての投稿を一度にロードする必要がない場合は、最初の10件をロードし、ユーザが一番下に達したら次の10件をロードします。これを実現するDjangoアプリケーションを見てみましょう。このアプリは urls.py に2つのパスを持っています。

urlpatterns = [
    path("", views.index, name="index"),
    path("posts", views.posts, name="posts")
]

views.py に2つの対応するビューがあります。

import time

from django.http import JsonResponse
from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request, "posts/index.html")

def posts(request):

    # Get start and end points
    start = int(request.GET.get("start") or 0)
    end = int(request.GET.get("end") or (start + 9))

    # Generate list of posts
    data = []
    for i in range(start, end + 1):
        data.append(f"Post #{i}")

    # Artificially delay speed of response
    time.sleep(1)

    # Return list of posts
    return JsonResponse({
        "posts": data
    })

posts ビューには、startend の2つの引数が必要です。このビューでは、独自のAPIを作成し、URL localhost:8000/posts?start=10&end=15 にアクセスしてテストすることができます。次のJSONを返します。

{
    "posts": [
        "Post #10",
        "Post #11", 
        "Post #12", 
        "Post #13", 
        "Post #14", 
        "Post #15"
    ]
}

ここで、サイトが読み込む index.html テンプレートでは、本文に空の div とスタイル設定だけから始めます。最初に静的ファイルを読み込み、次に static フォルダー内のJavaScriptファイルを参照することに注意してください。


{% load static %}
<!DOCTYPE html>
<html>
    <head>
        <title>My Webpage</title>
        <style>
            .post {
                background-color: #77dd11;
                padding: 20px;
                margin: 10px;
            }

            body {
                padding-bottom: 50px;
            }
        </style>
        <script scr="{% static 'posts/script.js' %}"></script>
    </head>
    <body>
        <div id="posts">
        </div>
    </body>
</html>

JavaScriptでは、ユーザがページの最後までスクロールするのを待ってから、APIを使ってさらに投稿を読み込みます。

// Start with first post
let counter = 1;

// Load posts 20 at a time
const quantity = 20;

// When DOM loads, render the first 20 posts
document.addEventListener('DOMContentLoaded', load);

// If scrolled to bottom, load the next 20 posts
window.onscroll = () => {
    if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
        load();
    }
};

// Load next set of posts
function load() {

    // Set start and end post numbers, and update counter
    const start = counter;
    const end = start + quantity - 1;
    counter = end + 1;

    // Get new posts and add posts
    fetch(`/posts?start=${start}&end=${end}`)
    .then(response => response.json())
    .then(data => {
        data.posts.forEach(add_post);
    })
};

// Add a new post with given contents to DOM
function add_post(contents) {

    // Create new post
    const post = document.createElement('div');
    post.className = 'post';
    post.innerHTML = contents;

    // Add post to DOM
    document.querySelector('#posts').append(post);
};

これで無限スクロールするサイトができました!

infinite scroll

アニメーション

サイトをもう少し面白くする別の方法は、アニメーションを追加することです。CSSはスタイリングを提供するだけでなく、HTML要素をアニメーション化することも容易にします。

CSSでアニメーションを作成するには、次の形式を使用します。この形式では、アニメーション固有の開始スタイルと終了スタイル( to と from )、または継続時間内のさまざまな段階のスタイル( 0% から100% まで)を含めることができます。たとえば、次のようになります。

@keyframes animation_name {
    from {
        /* Some styling for the start */
    }

    to {
        /* Some styling for the end */
    }
}

または:

@keyframes animation_name {
    0% {
        /* Some styling for the start */
    }

    75% {
        /* Some styling after 3/4 of animation */
    }

    100% {
        /* Some styling for the end */
    }
}

次に、アニメーションを要素に適用するために、animation-nameanimation-duration (秒単位)、animation-fill-mode (通常 forwards)を含めます。たとえば、最初にページに入ったときにタイトルが大きくなるページを次に示します。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Animate</title>
        <style>
            @keyframes grow {
                from {
                    font-size: 20px;
                }
                to {
                    font-size: 100px;
                }
            }

            h1 {
                animation-name: grow;
                animation-duration: 2s;
                animation-fill-mode: forwards;
            }
        </style>
    </head>
    <body>
        <h1>Welcome!</h1>
    </body>
</html>
Growing title

以下の例は、数行を変更するだけで見出しの位置を変更する方法を示しています。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Animate</title>
        <style>
            @keyframes move {
                from {
                    left: 0%;
                }
                to {
                    left: 50%;
                }
            }

            h1 {
                position: relative;
                animation-name: move;
                animation-duration: 2s;
                animation-fill-mode: forwards;
            }
        </style>
    </head>
    <body>
        <h1>Welcome!</h1>
    </body>
</html>
Moving header

次に、中間CSSプロパティの設定についても説明します。アニメーションの途中で任意の比率でスタイルを指定できます。下の例では、タイトルを左から右に移動した後、左に戻します。そのためには、上の例のアニメーションのみを変更します。

@keyframes move {
    0% {
        left: 0%;
    }
    50% {
        left: 50%;
    }
    100% {
        left: 0%;
    }
}
back and forth

アニメーションを複数回繰り返したい場合は、animation-iteration-count を1より大きい数値 (無限アニメーションの場合は infinite ) に変更します。アニメーションのさまざまな設定を変更するためのアニメーションプロパティが多数あります。

CSSに加えて、JavaScriptを使用してアニメーションをさらに制御できます。移動ヘッダの例 (無限の繰り返し) を使用して、アニメーションを開始および停止するボタンを作成する方法を示します。すでにアニメーション、ボタン、見出しがある場合は、次のスクリプトを追加してアニメーションを開始および一時停止できます。

document.addEventListener('DOMContentLoaded', function() {

    // Find heading
    const h1 = document.querySelector('h1');

    // Pause Animation by default
    h1.style.animationPlayState = 'paused';

    // Wait for button to be clicked
    document.querySelector('button').onclick = () => {

        // If animation is currently paused, begin playing it
        if (h1.style.animationPlayState == 'paused') {
            h1.style.animationPlayState = 'running';
        }

        // Otherwise, pause the animation
        else {
            h1.style.animationPlayState = 'paused';
        }
    }

})
play/pause animation

では、アニメーションに関する新しい知識を、以前作成した投稿ページに適用する方法を見てみましょう。具体的には、投稿を読んだ後に非表示にしたいとします。先ほど作成したDjangoプロジェクトとまったく同じですが、HTMLとJavaScriptが少し異なっているとします。まず、add_post 関数を変更し、今度は投稿の右側にボタンを追加します。

// Add a new post with given contents to DOM
function add_post(contents) {

    // Create new post
    const post = document.createElement('div');
    post.className = 'post';
    post.innerHTML = `${contents} <button class="hide">Hide</button>`;

    // Add post to DOM
    document.querySelector('#posts').append(post);
};

ここでは、hide ボタンをクリックしたときに投稿を非表示にする方法を説明します。これを行うには、ユーザがページの任意の場所をクリックするたびにトリガーされるイベントリスナを追加します。次に、引数としてeventを取る関数を作成し、event.target 属性を使用してクリックされたものにアクセスできるようにします。parentElement クラスを使用して、DOM内の特定の要素の親を取得することもできます。

// If hide button is clicked, delete the post
document.addEventListener('click', event => {

    // Find what was clicked on
    const element = event.target;

    // Check if the user clicked on a hide button
    if (element.className === 'hide') {
        element.parentElement.remove()
    }
    
});
naive hide

これで、非表示ボタンを実装したことがわかりますが、これはあまり美しくありません。削除する前に、ポストをフェードアウトさせて縮小させることもできます。これを行うには、まずCSSアニメーションを作成します。下のアニメーションでは、75%の時間をかけて不透明度 opacity を1から0に変更しています。これにより、ポストは徐々にフェードアウトします。その後、残りの時間はすべての height 関連の属性を0に移動し、投稿を事実上縮小して、見えなくします。

@keyframes hide {
    0% {
        opacity: 1;
        height: 100%;
        line-height: 100%;
        padding: 20px;
        margin-bottom: 10px;
    }
    75% {
        opacity: 0;
        height: 100%;
        line-height: 100%;
        padding: 20px;
        margin-bottom: 10px;
    }
    100% {
        opacity: 0;
        height: 0px;
        line-height: 0px;
        padding: 0px;
        margin-bottom: 0px;
    }
}

次に、このアニメーションを投稿のCSSに追加します。最初にアニメーション再生状態 animation-play-state を一時停止 paused に設定します。これは、投稿が既定で非表示にならないことを意味します。

.post {
    background-color: #77dd11;
    padding: 20px;
    margin-bottom: 10px;
    animation-name: hide;
    animation-duration: 2s;
    animation-fill-mode: forwards;
    animation-play-state: paused;
}

最後に、非表示 hide ボタンをクリックしてアニメーションを開始し、投稿を削除します。これを行うには、上からJavaScriptを編集します。

// If hide button is clicked, delete the post
document.addEventListener('click', event => {

    // Find what was clicked on
    const element = event.target;

    // Check if the user clicked on a hide button
    if (element.className === 'hide') {
        element.parentElement.style.animationPlayState = 'running';
        element.parentElement.addEventList
    }
    
});
Pretty hide

このように、Hideボタンの機能が大幅に改善されました。

React

この時点で、より複雑なWebサイトにどれだけのJavaScriptコードを組み込む必要があるか想像できます。CSSフレームワークとしてBootstrapを採用し、実際に書かなければならないCSSの量を削減したように、JavaScriptフレームワークを採用することで、実際に書かなければならないコードの量を減らすことができます。最も一般的なJavaScriptフレームワークの1つはReactというライブラリーです。

このコースでは、これまで命令型プログラミング手法を使用してきました。この手法では、実行するステートメントのセットをコンピュータに与えます。たとえば、HTMLページのカウンタを更新するには、次のようなコードを使用します。

表示:

<h1>0</h1>

ロジック:

let num = parseInt(document.querySelector("h1").innerHTML);
num += 1;
document.querySelector("h1").innerHTML = num;

Reactを使えば、宣言型プログラミングを使うことができ、表示したいものを説明するコードを書くだけで、どのように表示するかを気にする必要がなくなります。Reactでは、カウンタは次のようになります。

表示:

<h1>{num}</h1>

ロジック:

num += 1;

Reactフレームワークは、それぞれが基礎となる状態(state)を持つことができるコンポーネントの概念に基づいて構築されています。コンポーネントは、投稿やナビゲーションバーなどのWebページに表示されるもので、状態はそのコンポーネントに関連付けられた変数のセットです。Reactの長所は、状態が変化すると、それに応じてReactが自動的にDOMを変更することです。

Reactを使う方法はいくつかあります (これにはFacebookが公開している人気の create-react-app コマンドも含まれます) が、今日はHTMLファイルで直接始めることに焦点を当てます。これを行うには、次の3つのJavaScriptパッケージをインポートする必要があります。

  • React:コンポーネントとその動作を定義します。
  • ReactDOM:Reactコンポーネントを取得してDOMに挿入します。
  • Babel:これからReactで記述する言語である JSX から、ブラウザが解釈できるプレーンなJavaScriptに翻訳します。JSXはJavaScriptに非常に似ていますが、コード内でHTMLを表現する機能など、いくつかの追加機能があります。

最初のReactアプリケーションを作成します。

<!DOCTYPE html>
<html lang="en">
    <head>
        <script src="https://unpkg.com/react@17/umd/react.production.min.js" crossorigin></script>
        <script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js" crossorigin></script>
        <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
        <title>Hello</title>
    </head>
    <body>
        <div id="app"></div>

        <script type="text/babel">
            function App() {
                return (
                    <div>
                        Hello!
                    </div>
                );
            }

            ReactDOM.render(<App />, document.querySelector("#app"));
        </script>
    </body>
</html>

これは私たちの最初のReactアプリなので、このコードの各部分が何をしているかを詳しく見てみましょう。

  • titleの上の3行では、React、ReactDom、Babelの最新バージョンをインポートします。
  • 本文には、app というidを持つ1つの div を含めます。ほとんどの場合、これを空のままにして、下のReactのコードに入力します。
  • type="text/babel" を指定するscriptタグを含めます。これは、それ以降のスクリプトがBabelを使用して変換する必要があることをブラウザに通知します。
  • 次に、App というコンポーネントを作成します。ReactのコンポーネントはJavaScriptクラスとして表現されます。
  • そのコンポーネントは、DOMにレンダリングしたい内容を返します。この場合は、<div>Hello!</div> を返します。
  • スクリプトの最後の行では、2つの引数を取る ReactDOM.render 関数を使用します。
    1. レンダリングするコンポーネント
    2. コンポーネントが描画されるDOM内の要素

コードが何をしているのか理解できたので、結果のWebページを見てみましょう。

welcome hello react

Reactの便利な機能の1つは、他のコンポーネント内のコンポーネントをレンダリングできることです。これを説明するために、Hello という別のコンポーネントを作成します。

function Hello(props) {
    return (
        <h1>Hello</h1>
    );
}

次に、Appコンポーネント内に3つの Hello コンポーネントを描画します。

function App() {
    return (
        <div>
            <Hello />
            <Hello />
            <Hello />
        </div>
    );
}

次のようなページが表示されます。

Three hellos

これまでのところ、コンポーネントはすべてまったく同じであるため、それほど興味深いものではありません。これらのコンポーネントにプロパティ(React用語でprops )を追加することで、コンポーネントの柔軟性を高めることができます。たとえば、3人の人に挨拶したいとします。HTMLタグに似たメソッドで、これらの人の名前を渡せます。

function App() {
    return (
        <div>
            <Hello name="Harry" />
            <Hello name="Ron" />
            <Hello name="Hermione" />
        </div>
    );
}

その後、props.PROP_NAME を使用してpropsにアクセスできます。次に、中括弧を使用してこれをJSXに挿入します。

function Hello(props) {
    return (
        <h1>Hello, {props.name}!</h1>
    );
}

このページには、3つの名前が表示されます。

Three names

では、Reactを使って、JavaScriptを使って最初に作ったカウンター・ページを再実装する方法を見てみましょう。全体の構成は変わりませんが、App コンポーネント内で、React のuseState フックを利用して、コンポーネントのstate を追加します。useState の引数は、state の初期値で、0 に設定します。この関数は、state を示す変数とstate を更新する関数を返します。

const [count, setCount] = React.useState(0);

次に、関数がレンダリングを行う箇所を説明します。ここでは、ヘッダーとボタンを定義します。ボタンをクリックした場合に、React がonclick属性を使用して処理するイベントリスナーを以下のように追加します。

return (
    <div>
        <h1>{count}</h1>
        <button onClick={updateCount}>Count</button>
    </div>
);

最後にupdateCount 関数を定義します。その際、setCount関数を利用し、引数にはstate の新しい値を指定します。

function updateCount() {
    setCount(count + 1);
}

これで、機能するカウンターサイトができました!

counter

足し算

Reactフレームワークの感触をつかんだので、ここで学んだことを利用して、ユーザが足し算の問題を解くことができるゲームのようなサイトを構築してみましょう。最初に、他のReactページと同じ設定で新しいファイルを作成します。このアプリケーションの構築を始めるにあたり、この状態で何を追跡しておきたいかを考えてみましょう。ユーザがページを開いている間に変更される可能性があるものはすべて含める必要があります。状態を次のように設定します。

  • num1:足し算の最初の数値。
  • num2:足し算の2番目の数値。
  • response:ユーザが入力した内容。
  • score:ユーザが正しく回答した質問の数。

State は、次の情報を含むJavascript オブジェクトとすることができます。

const [state, setState] = React.useState({
    num1: 1,
    num2: 1,
    response: "",
    score: 0
});

ここで、state の値を用いて、基本的なユーザーインターフェースを表示します。

return (
    <div>
        <div>{state.num1} + {state.num2}</div>
        <input value={state.response} />
        <div>Score: {state.score}</div>
    </div>
);

サイトの基本レイアウトは次のようになります。

Addition layout

この時点では、入力ボックスの値は現在空の文字列であるstate.response として固定されているため、ユーザは入力ボックスに何も入力できません。これを修正するには、onChange 属性を入力ボックスに追加し、updateResponse という関数を値に設定します。

onChange={this.updateResponse}

次に、updateResposne 関数を定義する必要があります。この関数は、関数をトリガしたイベント(event)を引数として取り込み、response を入力の現在の値に設定します。この関数は、ユーザが入力した内容をstateに保存します。

function updateResponse(event) {
    setState({
        ...state,
        response: event.target.value
    });
}

次に、ユーザが問題を送信する機能を追加します。最初に別のイベントリスナーを追加し、次に記述する関数にリンクします。

onKeyPress={inputKeyPress}

次に、inputKeyPress 関数を定義します。この関数では、まず Enter キーが押されたかどうかを確認してから、答えが正しいかどうかを確認します。ユーザが正しい場合は、スコアを1だけ増やし、次の問題の乱数を選択し、responseをクリアします。responseが間違っている場合は、スコアを1減らして回答をクリアします。

function inputKeyPress(event) {
    if (event.key === "Enter") {
        const answer = parseInt(state.response);
        if (answer === state.num1 + state.num2) {
            // User got question right
            setState({
                ...state,
                score: state.score + 1,
                response: "",
                num1: Math.ceil(Math.random() * 10),
                num2: Math.ceil(Math.random() * 10)
            });
        } else {
            // User got question wrong
            setState({
                ...state,
                score: state.score - 1,
                response: ""
            })
        }
    }
}

アプリケーションの仕上げとして、ページにスタイルを追加しましょう。アプリケーションのすべてを中央に配置し、問題を含む divに問題 problem の id を追加し、styleタグに次のCSSを追加することで、問題を大きくします。

#app {
    text-align: center;
    font-family: sans-serif;
}

#problem {
    font-size: 72px;
}

最後に、10ポイントを獲得したら、ゲームに勝つ機能を追加しましょう。これを行うには、以下のように条件を追加し、ポイントが10になるとまったく異なるものを返します。

if (state.score === 10) {
    return (
        <div id="winner">You won!</div>
    );
}

勝利をよりエキサイティングにするために、代替 div にもスタイルを追加します。

#winner {
    font-size: 72px;
    color: green;
}

では、アプリケーションを見てみましょう。

finished

今日のレッスンはこれで終わりです!次回は、大規模なWebアプリケーションを構築するためのベストプラクティスについて説明します。