Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第4章 フォームはリクエストを作る

第3章では、Ruby の配列とハッシュから映画一覧の HTML を作りました。今の映画図鑑は、サーバーが用意した映画を表示できます。しかし、ブラウザから新しい映画の情報を送る方法はまだありません。

この章では、映画を登録するためのフォームを作ります。ここでの目的は、保存ではありません。フォームの HTML と実際の通信を見比べながら、送信先や HTTP メソッドがどのように決まり、入力した値がどのキーで Sinatra の params に届くのかを確認します。

この章は、第3章から続けて同じ sinatra-movies ディレクトリで進めます。第4章から読み始める場合は、第3章の最後(3.10)に示したコードを用意した状態から始めてください。

4.1 登録画面 GET /movies/new

まず、映画一覧から登録画面へ移動できるようにします。views/index.erb の説明文の下に、次のリンクを追加します。

<p>
  <a class="button-link" href="/movies/new">新しい映画を登録</a>
</p>

見た目はボタンのようにしますが、HTML としてはリンクです。リンクをクリックすると、ブラウザは GET /movies/new を送ります。まだ対応するルートがないため、このままでは登録画面を表示できません。

app.rb に、次のルートを追加します。

get "/movies/new" do
  erb :new
end

views/new.erb を作り、まずは見出しだけを書きます。

<h1>映画登録</h1>

<p>登録したい映画の情報を入力します。</p>

アプリを再起動し、/movies を開いて「新しい映画を登録」をクリックします。/movies/new で登録画面が表示されます。Network パネルでは、リンクのクリックによって GET /movies/new が送られ、200 OK の HTML が返ることを確認してください。

ここでは、リンクによる画面移動なので GET です。これから作るフォーム送信では、入力した値をサーバーへ送るために POST を使います。

4.2 formactionmethod

フォームは、ブラウザに HTTP リクエストを作らせるための HTML です。最小のフォームを見てみます。

<form action="/movies" method="post">
  <label for="title">タイトル</label>
  <input type="text" id="title" name="title">

  <button type="submit">送信内容を確認</button>
</form>

form の二つの属性が、送信されるリクエストを決めます。

属性役割このフォームでの意味
action送信先のパス/movies に送る
method送信に使う HTTP メソッドPOST で送る

HTML では method="post" と小文字で書いています。HTTP メソッドとして説明するときは POST と大文字で書きます。

このフォームの送信ボタンを押すと、ブラウザは次のようなリクエストを作ります。

POST /movies HTTP/1.1
Host: localhost:4567
Content-Type: application/x-www-form-urlencoded

title=月面喫茶

実際のリクエストには、ほかにもヘッダーが含まれます。ここでは、フォームの actionmethod、入力欄の値がリクエストに反映されることに注目してください。

4.3 nameparams のキーになる

フォーム部品には、idname がよく出てきます。

<label for="title">タイトル</label>
<input type="text" id="title" name="title">

id は、HTML の中で要素を識別するための属性です。ここでは labelfor="title"inputid="title" が対応し、ラベルと入力欄を結び付けています。

name は、フォーム送信時のキーを決める属性です。name="title" の入力欄に「月面喫茶」と入力して送信すると、Sinatra では次のように取り出せます。

params["title"]

params は、Sinatra が用意するパラメーターの入れ物です。ハッシュのようにキーを指定して値を取り出せます。本書では、フォームから送られた値を params["title"] のように文字列キーで扱います。

ここで混同しやすいのは、label の表示文字列や idparams のキーになるわけではない、という点です。キーを決めるのは name です。

4.4 映画の登録フォーム

映画図鑑では、利用者が次の 5 項目を入力します。

画面上の項目フォーム部品name
タイトルinput type="text"title
監督input type="text"director
公開年input type="text"year
ジャンルselectgenre
紹介文textareadescription

views/new.erb を次の内容に変更します。

<h1>映画登録</h1>

<p>登録したい映画の情報を入力します。</p>

<form class="movie-form" action="/movies" method="post">
  <div class="form-field">
    <label for="title">タイトル</label>
    <input type="text" id="title" name="title">
  </div>

  <div class="form-field">
    <label for="director">監督</label>
    <input type="text" id="director" name="director">
  </div>

  <div class="form-field">
    <label for="year">公開年</label>
    <input type="text" id="year" name="year">
  </div>

  <div class="form-field">
    <label for="genre">ジャンル</label>
    <select id="genre" name="genre">
      <option value="アクション">アクション</option>
      <option value="コメディ">コメディ</option>
      <option value="ドラマ">ドラマ</option>
      <option value="ホラー">ホラー</option>
      <option value="SF">SF</option>
      <option value="アニメーション">アニメーション</option>
      <option value="その他">その他</option>
    </select>
  </div>

  <div class="form-field">
    <label for="description">紹介文</label>
    <textarea id="description" name="description" rows="5"></textarea>
  </div>

  <div class="form-actions">
    <button type="submit">送信内容を確認</button>
    <a href="/movies">一覧へ戻る</a>
  </div>
</form>

select の選択肢は option で作ります。この章では、画面に表示する日本語と送信される値を同じにしています。たとえば「コメディ」を選ぶと、genre の値として "コメディ" が送られます。このフォームでは、最初の選択肢である「アクション」が初期状態で選ばれています。

textarea は複数行の入力欄です。input と違い、初期値を書く場合は value 属性ではなく、開始タグと終了タグの間に書きます。この章では空のままにしておきます。

まだ入力チェックはしません。空欄のまま送るとどう届くのかも、フォームの仕組みを理解する材料になります。タイトル必須の入力チェックは第5章で扱います。

タイトル、監督、公開年、ジャンル、紹介文の入力欄と登録ボタンが並ぶ映画登録画面
図 4-1 映画の登録フォーム

4.5 POST /movies で送信値を確認する

今のまま送信ボタンを押すと、ブラウザは POST /movies を送ります。しかし、app.rb にはまだ POST /movies に対応するルートがありません。

app.rb に次のルートを追加します。

post "/movies" do
  content_type :text
  params.inspect
end

post "/movies" は、POST /movies に対応するルートです。get "/movies" と同じパスですが、HTTP メソッドが違うため別のルートとして扱われます。

params.inspect は、params の中身を確認しやすい文字列にします。この章では、送信された値が Sinatra に届いたことを確認するためだけに使います。まだ映画は保存されません。この post "/movies" は第4章だけの確認用コードで、第5章で保存処理とリダイレクトに置き換えます。

content_type :text は、レスポンスを HTML ではなくテキストとして返す指定です。第5章で安全な表示を扱う前に、利用者が入力した値を HTML として返すことを避けるため、この章の確認用レスポンスではテキストとして表示します。

アプリを再起動し、/movies/new でフォームを送信してみます。たとえば、次のように入力します。

項目入力例
タイトル星降る駅
監督田中ユイ
公開年2041
ジャンルドラマ
紹介文夜行列車の終着駅を舞台にした物語。

送信後、ブラウザには次のようなテキストが表示されます。

{"title" => "星降る駅", "director" => "田中ユイ", "year" => "2041", "genre" => "ドラマ", "description" => "夜行列車の終着駅を舞台にした物語。"}

これは完成画面ではありません。フォームから送られた値を確認するための一時的な表示です。次章で、この値を映画データとして保存する処理へ進めます。

params.inspect の表示順は重要ではありません。注目するのは、titledirectoryeargenredescription というキーと、入力した値が届いていることです。

タイトルを空欄で送ると、"title" => "" のように空文字として届きます。空文字を保存してよいかどうかは、サーバー側で確認する必要があります。タイトル必須の入力チェックは第5章で扱います。

確認用レスポンスからフォームへ戻るには、ブラウザの戻るボタンを使ってください。第5章では、送信後に別の画面へ移動する処理へ変えます。

4.6 Network パネルで Form Data を見る

フォーム送信は、画面だけでなく Network パネルでも確認します。

Chrome で /movies/new を開き、Network パネルを開いた状態でフォームを送信してください。リクエスト一覧から movies を選びます。確認する項目は次のとおりです。

見る場所確認すること
HeadersRequest Method が POST である
HeadersRequest URL のパスが /movies である
HeadersStatus Code が 200 OK である
HeadersRequest Headers に Content-Type: application/x-www-form-urlencoded が含まれる
HeadersResponse Headers に Content-Type: text/plain が含まれる
PayloadForm Data に titledirectoryeargenredescription がある
Responseparams.inspect のテキストが返っている

Network パネルの表示名は Chrome のバージョンによって少し変わることがあります。Form Data が見つからない場合は、選択した POST /movies の Payload を確認してください。

フォームの HTML と Network パネルを対応させると、次のようになります。

HTMLNetwork パネル / Sinatra
<form action="/movies">Request URL が /movies
<form method="post">Request Method が POST
name="title"Form Data と params のキー title
入力したタイトルForm Data と params["title"] の値

ここで、id="title" やラベルの「タイトル」という文字が送信キーになるわけではないことをもう一度確認してください。送信キーは name="title" で決まります。

入力済みの映画登録フォームと、同じ値が title、director、year、genre、description の Form Data として並ぶ Payload の比較
図 4-2 フォームの入力欄と Form Data の対応

POST にしただけで、送信内容が秘密になるわけではありません。Network パネルを見れば、このように送信された値を確認できます。ここでは安全性ではなく、フォームがどのリクエストを作るのかを観察しています。

4.7 HTML フォームが直接送れるメソッドは GET と POST

HTTP リクエストとしてサーバーへ送る HTML フォームでは、methodget または post を指定するのが基本です。

GET は、情報の取得に使います。第4章の最初に作った GET /movies/new は、登録フォームを表示するためのリクエストです。

POST は、サーバーへデータを送るときに使います。この章の POST /movies は、映画フォームの値を送るリクエストです。まだ保存はしていませんが、送信の意味としては POST を使います。

後の章では、既存の映画を更新するために PATCH、削除するために DELETE を使います。ただし、HTML フォームは PATCHDELETE を直接送れません。そのため、第7章で method override という仕組みを使います。

ここでは、次の区別を押さえてください。

操作この章で使う HTTP メソッド
登録画面を表示するGET
登録フォームの値を送るPOST

POST で送ったからといって、それだけでデータが保存されるわけではありません。保存するには、サーバー側で受け取った値をデータとして追加する処理が必要です。それを次章で作ります。

4.8 この章のコードを確認する

この章の最後に、追加したファイルとルートを確認します。

追加・変更したもの役割
GET /movies/new映画登録フォームを表示する
views/new.erb映画登録フォームの HTML
POST /moviesフォームから送られた値を一時的に確認する
views/index.erb登録画面へのリンクを追加する
public/stylesheets/application.cssフォームの見た目を整える

章終了時点の app.rb は次の状態です。

require "sinatra"

movies = [
  {
    "title" => "月面喫茶",
    "director" => "山田アキラ",
    "year" => "2042",
    "genre" => "SF",
    "description" => "月面にある小さな喫茶店を舞台にした物語。"
  },
  {
    "title" => "北風のリズム",
    "director" => "佐藤ミナ",
    "year" => "2038",
    "genre" => "ドラマ",
    "description" => "雪の町で古い楽器を修理する人々を描く。"
  },
  {
    "title" => "週末ロケット",
    "director" => "鈴木トオル",
    "year" => "2040",
    "genre" => "コメディ",
    "description" => "町工場の仲間たちが小さなロケット作りに挑む。"
  }
]

get "/" do
  redirect "/movies"
end

get "/movies" do
  @movies = movies
  erb :index
end

get "/movies/new" do
  erb :new
end

post "/movies" do
  content_type :text
  params.inspect
end

この post "/movies" は第4章だけの確認用レスポンスです。第5章では、params.inspect を返すのではなく、受け取った値を映画データとして保存し、別の画面へ移動する処理へ置き換えます。

views/index.erb は次の状態です。

<h1>映画一覧</h1>

<p>登録されている映画を一覧で表示します。</p>

<p>
  <a class="button-link" href="/movies/new">新しい映画を登録</a>
</p>

<div class="table-scroll">
  <table class="movie-table">
    <thead>
      <tr>
        <th scope="col">タイトル</th>
        <th scope="col">公開年</th>
        <th scope="col">ジャンル</th>
      </tr>
    </thead>
    <tbody>
      <% @movies.each do |movie| %>
        <tr>
          <td><%= movie["title"] %></td>
          <td><%= movie["year"] %></td>
          <td><%= movie["genre"] %></td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>

views/new.erb は次の状態です。

<h1>映画登録</h1>

<p>登録したい映画の情報を入力します。</p>

<form class="movie-form" action="/movies" method="post">
  <div class="form-field">
    <label for="title">タイトル</label>
    <input type="text" id="title" name="title">
  </div>

  <div class="form-field">
    <label for="director">監督</label>
    <input type="text" id="director" name="director">
  </div>

  <div class="form-field">
    <label for="year">公開年</label>
    <input type="text" id="year" name="year">
  </div>

  <div class="form-field">
    <label for="genre">ジャンル</label>
    <select id="genre" name="genre">
      <option value="アクション">アクション</option>
      <option value="コメディ">コメディ</option>
      <option value="ドラマ">ドラマ</option>
      <option value="ホラー">ホラー</option>
      <option value="SF">SF</option>
      <option value="アニメーション">アニメーション</option>
      <option value="その他">その他</option>
    </select>
  </div>

  <div class="form-field">
    <label for="description">紹介文</label>
    <textarea id="description" name="description" rows="5"></textarea>
  </div>

  <div class="form-actions">
    <button type="submit">送信内容を確認</button>
    <a href="/movies">一覧へ戻る</a>
  </div>
</form>

第4章では、public/stylesheets/application.css に次の CSS を追加しました。

a {
  color: #1f4f5f;
}

.button-link,
button {
  display: inline-block;
  border: 1px solid #1f4f5f;
  border-radius: 4px;
  padding: 8px 14px;
  color: #ffffff;
  font: inherit;
  text-decoration: none;
  background: #1f4f5f;
  cursor: pointer;
}

.button-link:hover,
button:hover {
  background: #173c48;
}

.movie-form {
  max-width: 640px;
  margin-top: 24px;
}

.form-field {
  margin-bottom: 18px;
}

.form-field label {
  display: block;
  margin-bottom: 6px;
  font-weight: 700;
}

.form-field input,
.form-field select,
.form-field textarea {
  box-sizing: border-box;
  width: 100%;
  border: 1px solid #c9c9c9;
  border-radius: 4px;
  padding: 8px 10px;
  font: inherit;
  background: #ffffff;
}

.form-field textarea {
  resize: vertical;
}

.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-top: 24px;
}

最後に、次の問いを自分の言葉で確認してください。

  • フォームの送信先は、どの HTML 属性で決まりますか。
  • params["genre"] のキー genre は、どの HTML 属性から来ていますか。
  • POST /movies のレスポンスに値が表示されても、まだ映画が保存されていないのはなぜですか。

次章では、params.inspect で確認した値を使い、JSON ファイルへ映画を保存します。

さらに学ぶ

この章では、フォームから POST リクエストを送り、Sinatra の params で受け取るところまでを扱いました。さらに詳しく学ぶ場合は、次の観点を調べてください。

  • フォームデータは既定で application/x-www-form-urlencoded という形式で送信されます。
  • ファイルアップロードでは multipart/form-data を使いますが、本書では扱いません。
  • HTML フォームの method には dialog もありますが、HTTP リクエスト送信としての GET / POST とは目的が異なります。

参考資料