開発者ドキュメント

Reactフロントエンドを使用してRubyonRailsプロジェクトをセットアップする方法

###序章

Ruby on Rails は、人気のあるサーバー側のWebアプリケーションフレームワークであり、このチュートリアルの執筆時点では、GitHubに42,000を超える星があります。 GitHub Basecamp SoundCloud Airbnb 、など、今日Web上に存在する多くの人気のあるアプリケーションに電力を供給しますX156X]Twitch。 Ruby on Railsは、プログラマーの経験とその周りに築き上げられた情熱的なコミュニティに重点を置いており、最新のWebアプリケーションを構築および保守するために必要なツールを提供します。

React は、フロントエンドのユーザーインターフェイスを作成するために使用されるJavaScriptライブラリです。 Facebookに支えられて、今日Webで使用されている最も人気のあるフロントエンドライブラリの1つです。 Reactは、仮想ドキュメントオブジェクトモデル(DOM)コンポーネントアーキテクチャ、状態管理などの機能を提供し、フロントエンド開発のプロセスをより組織的かつ効率的にします。

Webのフロントエンドがサーバー側のコードとは別のフレームワークに移行しているため、Railsの優雅さとReactの効率を組み合わせることで、現在のトレンドに基づいた強力で最新のアプリケーションを構築できます。 Railsテンプレートエンジンの代わりにReactを使用してRailsビュー内からコンポーネントをレンダリングすることにより、アプリケーションは、Ruby on Railsの表現力を活用しながら、JavaScriptとフロントエンド開発の最新の進歩の恩恵を受けることができます。

このチュートリアルでは、お気に入りのレシピを保存し、Reactフロントエンドで表示するRubyonRailsアプリケーションを作成します。 終了すると、 Bootstrap でスタイル設定されたReactインターフェースを使用して、レシピを作成、表示、および削除できるようになります。

このアプリケーションのコードを確認したい場合は、 DigitalOcean CommunityGitHubでこのチュートリアルのコンパニオンリポジトリを確認してください。

前提条件

このチュートリアルに従うには、次のものが必要です。

ステップ1—新しいRailsアプリケーションを作成する

このステップでは、Railsアプリケーションフレームワーク上にレシピアプリケーションを構築します。 最初に、新しいRailsアプリケーションを作成します。このアプリケーションは、ほとんど構成を行わずに、そのままReactで動作するように設定されます。

Railsは、最新のWebアプリケーションを構築するために必要なすべてのものを作成するのに役立つジェネレーターと呼ばれる多数のスクリプトを提供します。 これらのコマンドの完全なリストとその機能を確認するには、ターミナルウィンドウで次のコマンドを実行します。

  1. rails -h

これにより、オプションの包括的なリストが生成され、アプリケーションのパラメータを設定できるようになります。 リストされているコマンドの1つは new 新しいRailsアプリケーションを作成するコマンド。

次に、を使用して新しいRailsアプリケーションを作成します。 new 発生器。 ターミナルウィンドウで次のコマンドを実行します。

  1. rails new rails_react_recipe -d=postgresql -T --webpack=react --skip-coffee

上記のコマンドは、名前の付いたディレクトリに新しいRailsアプリケーションを作成します rails_react_recipe、必要なRubyとJavaScriptの依存関係をインストールし、Webpackを構成します。 これに関連付けられているフラグを見ていきましょう new ジェネレータコマンド:

コマンドの実行が完了したら、に移動します rails_react_recipe アプリのルートディレクトリであるディレクトリ:

  1. cd rails_react_recipe

次に、ディレクトリの内容を一覧表示します。

  1. ls

このルートディレクトリには、Railsアプリケーションの構造を構成する自動生成されたファイルとフォルダがいくつかあります。 package.json Reactアプリケーションの依存関係を含むファイル。

これで、新しいRailsアプリケーションが正常に作成されたので、次のステップでそれをデータベースに接続する準備が整いました。

ステップ2—データベースのセットアップ

新しいRailsアプリケーションを実行する前に、まずそれをデータベースに接続する必要があります。 このステップでは、新しく作成したRailsアプリケーションをPostgreSQLデータベースに接続して、必要なときにレシピデータを保存およびフェッチできるようにします。

The database.yml で見つかったファイル config/database.yml さまざまな開発環境のデータベース名などのデータベースの詳細が含まれています。 Railsは、アンダースコア((_)に続いて、アプリの名前に環境名を付けます。 環境データベース名はいつでも好きな名前に変更できます。

注:この時点で、変更できます config/database.yml Railsがデータベースの作成に使用するPostgreSQLの役割を設定します。 前提条件Rubyon RailsアプリケーションでPostgreSQLを使用する方法に従い、パスワードで保護されたロールを作成した場合は、ステップ4の手順に従うことができます。 macOSまたはUbuntu18.04

前述のように、RailsはWebアプリケーションの開発を容易にするための多くのコマンドを提供します。 これには、データベースを操作するためのコマンドが含まれます。 create, drop、 と reset. アプリケーションのデータベースを作成するには、ターミナルウィンドウで次のコマンドを実行します。

  1. rails db:create

このコマンドは、 developmenttest データベース、次の出力を生成します。

Output
Created database 'rails_react_recipe_development' Created database 'rails_react_recipe_test'

アプリケーションがデータベースに接続されたので、ターミナルウィンドウで次のコマンドを実行してアプリケーションを起動します。

  1. rails s --binding=127.0.0.1

The s また server コマンドはPumaを起動します。これは、デフォルトでRailsとともに配布されるWebサーバーであり、 --binding=127.0.0.1 サーバーを localhost.

このコマンドを実行すると、コマンドプロンプトが消え、次の出力が表示されます。

Output
=> Booting Puma => Rails 5.2.3 application starting in development => Run `rails server -h` for more startup options Puma starting in single mode... * Version 3.12.1 (ruby 2.6.3-p62), codename: Llamas in Pajamas * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 Use Ctrl-C to stop

アプリケーションを表示するには、ブラウザウィンドウを開き、次の場所に移動します。 http://localhost:3000. Railsのデフォルトのウェルカムページが表示されます。

これは、Railsアプリケーションが適切に設定されていることを意味します。

Webサーバーをいつでも停止するには、を押します。 CTRL+C サーバーが実行されているターミナルウィンドウで。 さあ、今これを実行してください。 プーマからさようならのメッセージが届きます。

Output
^C- Gracefully stopping, waiting for requests to finish === puma shutdown: 2019-07-31 14:21:24 -0400 === - Goodbye! Exiting

その後、プロンプトが再度表示されます。

これで、食品レシピアプリケーションのデータベースが正常に設定されました。 次のステップでは、Reactフロントエンドをまとめるのに必要なすべての追加のJavaScript依存関係をインストールします。

ステップ3—フロントエンドの依存関係をインストールする

このステップでは、フードレシピアプリケーションのフロントエンドに必要なJavaScriptの依存関係をインストールします。 それらが含まれます:

ターミナルウィンドウで次のコマンドを実行して、Yarnパッケージマネージャーでこれらのパッケージをインストールします。

  1. yarn add react-router-dom bootstrap jquery popper.js

このコマンドは、Yarnを使用して指定されたパッケージをインストールし、それらをに追加します。 package.json ファイル。 これを確認するには、 package.json プロジェクトのルートディレクトリにあるファイル:

  1. nano package.json

インストールされているパッケージが dependencies 鍵:

〜/ rails_react_recipe / package.json
{
  "name": "rails_react_recipe",
  "private": true,
  "dependencies": {
    "@babel/preset-react": "^7.0.0",
    "@rails/webpacker": "^4.0.7",
    "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
    "bootstrap": "^4.3.1",
    "jquery": "^3.4.1",
    "popper.js": "^1.15.0",
    "prop-types": "^15.7.2",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-router-dom": "^5.0.1"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.7.2"
  }
}

アプリケーションにいくつかのフロントエンド依存関係をインストールしました。 次に、フードレシピアプリケーションのホームページを設定します。

ステップ4—ホームページの設定

必要なすべての依存関係をインストールしたら、このステップでアプリケーションのホームページを作成します。 ホームページは、ユーザーが最初にアプリケーションにアクセスしたときのランディングページとして機能します。

Railsは、アプリケーションのModel-View-Controllerアーキテクチャパターンに従います。 MVCパターンでは、コントローラーの目的は、特定の要求を受信し、それらを適切なモデルまたはビューに渡すことです。 現在、ルートURLがブラウザにロードされると、アプリケーションはRailsのウェルカムページを表示します。 これを変更するには、コントローラーを作成してホームページを表示し、ルートに一致させます。

Railsは controller コントローラを作成するためのジェネレータ。 The controller ジェネレーターは、一致するアクションとともにコントローラー名を受け取ります。 詳細については、Railsの公式ドキュメントをご覧ください。

このチュートリアルでは、コントローラーを呼び出します Homepage. ターミナルウィンドウで次のコマンドを実行して、 index アクション。

  1. rails g controller Homepage index

注: Linuxでは、エラーが発生した場合 FATAL: Listen error: unable to monitor directories for changes.、これは、マシンが変更を監視できるファイル数のシステム制限によるものです。 次のコマンドを実行して修正します。

  1. echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

これにより、監視できるディレクトリの数が恒久的に増加します Listen524288. 同じコマンドを実行して置き換えることで、これを再度変更できます 524288 ご希望の番号で。

このコマンドを実行すると、次のファイルが生成されます。

Railsコマンドを実行して作成されたこれらの新しいページとは別に、Railsは次の場所にあるルートファイルも更新します。 config/routes.rb. それは追加します get ルートルートとして変更するホームページのルート。

Railsのルートルートは、ユーザーがアプリケーションのルートURLにアクセスしたときに表示されるものを指定します。 この場合、ユーザーにホームページを表示してもらいます。 にあるルートファイルを開きます config/routes.rb お気に入りのエディターで:

  1. nano config/routes.rb

このファイル内で、 get 'homepage/index'root 'homepage#index' ファイルが次のようになるようにします。

〜/ rails_react_recipe / config / routers.rb
Rails.application.routes.draw do
  root 'homepage#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

この変更により、Railsは、アプリケーションのルートへのリクエストを index のアクション Homepage コントローラは、次に、にあるものをすべてレンダリングします index.html.erb にあるファイル app/views/homepage/index.html.erb ブラウザに。

これが機能していることを確認するには、アプリケーションを起動します。

  1. rails s --binding=127.0.0.1

ブラウザでアプリケーションを開くと、アプリケーションの新しいランディングページが表示されます。

アプリケーションが機能していることを確認したら、を押します CTRL+C サーバーを停止します。

次に、 ~/rails_react_recipe/app/views/homepage/index.html.erb ファイルを作成し、ファイル内のコードを削除してから、ファイルを空として保存します。 これを行うことにより、あなたはの内容を確認します index.html.erb フロントエンドのReactレンダリングに干渉しないでください。

アプリケーションのホームページを設定したので、次のセクションに移動して、Reactを使用するようにアプリケーションのフロントエンドを構成します。

ステップ5—RailsフロントエンドとしてのReactの構成

このステップでは、テンプレートエンジンではなく、アプリケーションのフロントエンドでReactを使用するようにRailsを構成します。 これにより、Reactレンダリングを利用して、より視覚的に魅力的なホームページを作成できます。

Railsは、 Webpacker gem を使用して、すべてのJavaScriptコードをpacksにバンドルします。 これらは、packsディレクトリにあります。 app/javascript/packs. これらのパックは、Railsビューでリンクできます。 javascript_pack_tag ヘルパー、およびパックにインポートされたスタイルシートをリンクすることができます stylesheet_pack_tag ヘルパー。 React環境へのエントリポイントを作成するには、これらのパックの1つをアプリケーションレイアウトに追加します。

まず、名前を変更します ~/rails_react_recipe/app/javascript/packs/hello_react.jsx にファイルする ~/rails_react_recipe/app/javascript/packs/Index.jsx.

  1. mv ~/rails_react_recipe/app/javascript/packs/hello_react.jsx ~/rails_react_recipe/app/javascript/packs/Index.jsx

ファイルの名前を変更した後、 application.html.erb、アプリケーションレイアウトファイル:

  1. nano ~/rails_react_recipe/app/views/layouts/application.html.erb

アプリケーションレイアウトファイルのheadタグの最後に、次の強調表示されたコード行を追加します。

〜/ rails_react_recipe / app / views / layouts / application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>RailsReactRecipe</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <%= javascript_pack_tag 'Index' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

アプリケーションのヘッダーにJavaScriptパックを追加すると、すべてのJavaScriptコードが利用可能になり、 Index.jsx アプリを実行するたびにページ上のファイル。 JavaScriptパックに加えて、 meta viewport アプリケーションのページのサイズとスケーリングを制御するタグ。

ファイルを保存して終了します。

エントリファイルがページにロードされたので、ホームページのReactコンポーネントを作成します。 作成することから始めます components のディレクトリ app/javascript ディレクトリ:

  1. mkdir ~/rails_react_recipe/app/javascript/components

The components ディレクトリには、アプリケーション内の他のReactコンポーネントとともに、ホームページのコンポーネントが格納されます。 ホームページには、すべてのレシピを表示するためのテキストと召喚状のボタンが含まれます。

エディターで、 Home.jsx のファイル components ディレクトリ:

  1. nano ~/rails_react_recipe/app/javascript/components/Home.jsx

次のコードをファイルに追加します。

〜/ rails_react_recipe / app / javascript / components / Home.jsx
import React from "react";
import { Link } from "react-router-dom";

export default () => (
  <div className="vw-100 vh-100 primary-color d-flex align-items-center justify-content-center">
    <div className="jumbotron jumbotron-fluid bg-transparent">
      <div className="container secondary-color">
        <h1 className="display-4">Food Recipes</h1>
        <p className="lead">
          A curated list of recipes for the best homemade meal and delicacies.
        </p>
        <hr className="my-4" />
        <Link
          to="/recipes"
          className="btn btn-lg custom-button"
          role="button"
        >
          View Recipes
        </Link>
      </div>
    </div>
  </div>
);

このコードでは、Reactと Link ReactRouterのコンポーネント。 The Link コンポーネントは、あるページから別のページに移動するためのハイパーリンクを作成します。 次に、Bootstrapクラスでスタイル設定された、ホームページ用のマークアップ言語を含む機能コンポーネントを作成してエクスポートしました。

あなたと Home コンポーネントが配置されたら、ReactRouterを使用してルーティングを設定します。 作成する routes のディレクトリ app/javascript ディレクトリ:

  1. mkdir ~/rails_react_recipe/app/javascript/routes

The routes ディレクトリには、対応するコンポーネントを含むいくつかのルートが含まれます。 指定されたルートがロードされるたびに、対応するコンポーネントがブラウザにレンダリングされます。

の中に routes ディレクトリ、作成 Index.jsx ファイル:

  1. nano ~/rails_react_recipe/app/javascript/routes/Index.jsx

次のコードを追加します。

〜/ rails_react_recipe / app / javascript / routers / Index.jsx
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";

export default (
  <Router>
    <Switch>
      <Route path="/" exact component={Home} />
    </Switch>
  </Router>
);

これで Index.jsx ルートファイル、いくつかのモジュールをインポートしました: React Reactを使用できるようにするモジュール、および BrowserRouter, Route、 と Switch React Routerのモジュール。これらを組み合わせることで、あるルートから別のルートに移動できます。 最後に、 Home コンポーネント。リクエストがルートと一致するたびにレンダリングされます(/)ルート。 アプリケーションにさらにページを追加する場合は常に、このファイルでルートを宣言し、そのページにレンダリングするコンポーネントと一致させるだけです。

ファイルを保存して終了します。

これで、ReactRouterを使用してルーティングを正常に設定できました。 Reactが利用可能なルートを認識して使用するには、アプリケーションへのエントリポイントでルートが利用可能である必要があります。 これを実現するには、Reactがエントリファイルでレンダリングするコンポーネントでルートをレンダリングします。

作成する App.jsx のファイル app/javascript/components ディレクトリ:

  1. nano ~/rails_react_recipe/app/javascript/components/App.jsx

次のコードをに追加します App.jsx ファイル:

〜/ rails_react_recipe / app / javascript / components / App.jsx
import React from "react";
import Routes from "../routes/Index";

export default props => <>{Routes}</>;

の中に App.jsx ファイル、Reactと作成したルートファイルをインポートしました。 次に、フラグメント内のルートをレンダリングするコンポーネントをエクスポートしました。 このコンポーネントは、アプリケーションのエントリポイントでレンダリングされるため、アプリケーションがロードされるたびにルートが使用可能になります。

今、あなたはあなたを持っています App.jsx セットアップしたら、エントリファイルにレンダリングします。 エントリを開く Index.jsx ファイル:

  1. nano ~/rails_react_recipe/app/javascript/packs/Index.jsx

そこでのコードを次のコードに置き換えます。

〜/ rails_react_recipe / app / javascript / packs / Index.jsx
import React from "react";
import { render } from "react-dom";
import 'bootstrap/dist/css/bootstrap.min.css';
import $ from 'jquery';
import Popper from 'popper.js';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import App from "../components/App";

document.addEventListener("DOMContentLoaded", () => {
  render(
    <App />,
    document.body.appendChild(document.createElement("div"))
  );
});

このコードスニペットでは、React、ReactDOM、Bootstrap、jQuery、Popper.jsからのレンダリングメソッド、および App 成分。 ReactDOMのrenderメソッドを使用して、 App のコンポーネント div ページの本文に追加された要素。 アプリケーションがロードされるたびに、Reactはコンテンツをレンダリングします App 内部のコンポーネント div ページ上の要素。

ファイルを保存して終了します。

最後に、いくつかのCSSスタイルをホームページに追加します。

あなたの application.css あなたの中で ~/rails_react_recipe/app/assets/stylesheets ディレクトリ:

  1. nano ~/rails_react_recipe/app/assets/stylesheets/application.css

次に、内容を置き換えます application.css 次のコードでファイルします。

〜/ rails_react_recipe / app / Assets / stylesheets / application.css
.bg_primary-color {
  background-color: #FFFFFF;
}
.primary-color {
  background-color: #FFFFFF;
}
.bg_secondary-color {
  background-color: #293241;
}
.secondary-color {
  color: #293241;
}
.custom-button.btn {
  background-color: #293241;
  color: #FFF;
  border: none;
}
.custom-button.btn:hover {
  color: #FFF !important;
  border: none;
}
.hero {
  width: 100vw;
  height: 50vh;
}
.hero img {
  object-fit: cover;
  object-position: top;
  height: 100%;
  width: 100%;
}
.overlay {
  height: 100%;
  width: 100%;
  opacity: 0.4;
}

これにより、ヒーロー画像のフレームワーク、または後で追加するWebサイトのフロントページに大きなWebバナーが作成されます。 さらに、これにより、ユーザーがアプリケーションに入るときに使用するボタンのスタイルが設定されます。

CSSスタイルを配置したら、ファイルを保存して終了します。 次に、アプリケーションのWebサーバーを再起動してから、ブラウザーにアプリケーションを再ロードします。 新しいホームページが表示されます。

このステップでは、フロントエンドとしてReactを使用するようにアプリケーションを構成しました。 次のセクションでは、レシピの作成、読み取り、更新、および削除を可能にするモデルとコントローラーを作成します。

ステップ6—レシピコントローラーとモデルの作成

アプリケーションのReactフロントエンドを設定したので、このステップでは、レシピモデルとコントローラーを作成します。 レシピモデルは、ユーザーのレシピに関する情報を保持するデータベーステーブルを表し、コントローラーはレシピの作成、読み取り、更新、または削除の要求を受信して処理します。 ユーザーがレシピを要求すると、レシピコントローラーはこの要求を受信し、それをレシピモデルに渡します。レシピモデルは、要求されたデータをデータベースから取得します。 次に、モデルはレシピデータをコントローラーへの応答として返します。 最後に、この情報がブラウザに表示されます。

を使用してレシピモデルを作成することから始めます generate model Railsによって提供されるサブコマンドであり、モデルの名前とその列およびデータ型を指定します。 ターミナルウィンドウで次のコマンドを実行して、 Recipe モデル:

  1. rails generate model Recipe name:string ingredients:text instruction:text image:string

上記のコマンドは、Railsに作成するように指示します Recipe と一緒にモデル name タイプの列 stringingredientsinstruction タイプの列 text、 と image タイプの列 string. このチュートリアルでは、モデルに名前を付けています Recipe、慣例により、Railsのモデルは単数形の名前を使用し、対応するデータベーステーブルは複数形の名前を使用するためです。

の実行 generate model コマンドは2つのファイルを作成します:

次に、レシピモデルファイルを編集して、有効なデータのみがデータベースに保存されるようにします。 これは、モデルにデータベース検証を追加することで実現できます。 次の場所にあるレシピモデルを開きます app/models/recipe.rb:

  1. nano ~/rails_react_recipe/app/models/recipe.rb

次の強調表示されたコード行をファイルに追加します。

class Recipe < ApplicationRecord
  validates :name, presence: true
  validates :ingredients, presence: true
  validates :instruction, presence: true
end

このコードでは、の存在をチェックするモデル検証を追加しました name, ingredients、 と instruction 分野。 これらの3つのフィールドが存在しない場合、レシピは無効であり、データベースに保存されません。

ファイルを保存して終了します。

Railsが作成する recipes データベース内のテーブルでは、 migration を実行する必要があります。これは、Railsではプログラムでデータベースに変更を加える方法です。 セットアップしたデータベースで移行が機能することを確認するには、に変更を加える必要があります。 20190407161357_create_recipes.rb ファイル。

このファイルをエディターで開きます。

  1. nano ~/rails_react_recipe/db/migrate/20190407161357_create_recipes.rb

次の強調表示された行を追加して、ファイルが次のようになるようにします。

db / migrate / 20190407161357_create_recipes.rb
class CreateRecipes < ActiveRecord::Migration[5.2]
  def change
    create_table :recipes do |t|
      t.string :name, null: false
      t.text :ingredients, null: false
      t.text :instruction, null: false
      t.string :image, default: 'https://raw.githubusercontent.com/do-community/react_rails_recipe/master/app/assets/images/Sammy_Meal.jpg'
      t.timestamps
    end
  end
end

この移行ファイルには、Rubyクラスが含まれています。 change メソッド、およびと呼ばれるテーブルを作成するコマンド recipes 列とそのデータ型とともに。 また、更新しました 20190407161357_create_recipes.rb とともに NOT NULL の制約 name, ingredients、 と instruction 追加することによる列 null: false、データベースを変更する前に、これらの列に値があることを確認してください。 最後に、画像列にデフォルトの画像URLを追加しました。 別の画像を使用する場合は、これを別のURLにすることができます。

これらの変更を加えて、ファイルを保存して終了します。 これで、移行を実行して実際にテーブルを作成する準備が整いました。 ターミナルウィンドウで、次のコマンドを実行します。

  1. rails db:migrate

ここでは、データベース移行コマンドを使用しました。このコマンドは、移行ファイルの命令を実行します。 コマンドが正常に実行されると、次のような出力が表示されます。

Output
== 20190407161357 CreateRecipes: migrating ==================================== -- create_table(:recipes) -> 0.0140s == 20190407161357 CreateRecipes: migrated (0.0141s) ===========================

レシピモデルを配置したら、レシピコントローラーを作成し、レシピを作成、読み取り、削除するためのロジックを追加します。 ターミナルウィンドウで、次のコマンドを実行します。

  1. rails generate controller api/v1/Recipes index create show destroy -j=false -y=false --skip-template-engine --no-helper

このコマンドでは、 Recipes のコントローラー api/v1 ディレクトリと index, create, show、 と destroy アクション。 The index アクションは、すべてのレシピのフェッチを処理します。 create アクションは、新しいレシピの作成を担当します。 show アクションは単一のレシピをフェッチし、 destroy アクションは、レシピを削除するためのロジックを保持します。

また、コントローラーをより軽量にするために、次のようないくつかのフラグを渡しました。

コマンドを実行すると、ルートファイルも更新され、 Recipes コントローラ。 これらのルートを使用するには、 config/routes.rb ファイル。

テキストエディタでルートファイルを開きます。

  1. nano ~/rails_react_recipe/config/routes.rb

開いたら、次のコードのように更新し、強調表示された行を変更または追加します。

〜/ rails_react_recipe / config / routers.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      get 'recipes/index'
      post 'recipes/create'
      get '/show/:id', to: 'recipes#show'
      delete '/destroy/:id', to: 'recipes#destroy'
    end
  end
  root 'homepage#index'
  get '/*path' => 'homepage#index'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

このルートファイルでは、のHTTP動詞を変更しました createdestroy それができるようにルート postdelete データ。 のルートも変更しました showdestroy 追加することによるアクション :id ルートへのパラメータ。 :id 読み取りまたは削除するレシピの識別番号を保持します。

また、キャッチオールルートを追加しました get '/*path' これにより、既存のルートと一致しない他のリクエストが index のアクション homepage コントローラ。 このように、フロントエンドのルーティングは、レシピの作成、読み取り、または削除に関連しないリクエストを処理します。

ファイルを保存して終了します。

アプリケーションで使用可能なルートのリストを表示するには、ターミナルウィンドウで次のコマンドを実行します。

  1. rails routes

このコマンドを実行すると、プロジェクトのURIパターン、動詞、および一致するコントローラーまたはアクションのリストが表示されます。

次に、すべてのレシピを一度に取得するためのロジックを追加します。 RailsはActiveRecordライブラリを使用して、このようなデータベース関連のタスクを処理します。 ActiveRecordは、クラスをリレーショナルデータベーステーブルに接続し、それらを操作するための豊富なAPIを提供します。

すべてのレシピを取得するには、ActiveRecordを使用してレシピテーブルをクエリし、データベースに存在するすべてのレシピをフェッチします。

を開きます recipes_controller.rb 次のコマンドでファイルします。

  1. nano ~/rails_react_recipe/app/controllers/api/v1/recipes_controller.rb

次の強調表示されたコード行をレシピコントローラーに追加します。

〜/ rails_react_recipe / app / controllers / api / v1 / recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
  end

  def show
  end

  def destroy
  end
end

あなたの中で index アクション、を使用して all ActiveRecordが提供するメソッドを使用すると、データベース内のすべてのレシピを取得できます。 を使用して order メソッドでは、作成日から降順で並べ替えます。 このようにして、最初に最新のレシピを入手します。 最後に、レシピのリストをJSON応答として送信します。 render.

次に、新しいレシピを作成するためのロジックを追加します。 すべてのレシピをフェッチする場合と同様に、提供されたレシピの詳細を検証して保存するためにActiveRecordに依存します。 次の強調表示されたコード行でレシピコントローラーを更新します。

〜/ rails_react_recipe / app / controllers / api / v1 / recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
    recipe = Recipe.create!(recipe_params)
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

  def show
  end

  def destroy
  end

  private

  def recipe_params
    params.permit(:name, :image, :ingredients, :instruction)
  end
end

の中に create アクション、あなたはActiveRecordのを使用します create 新しいレシピを作成する方法。 The create メソッドには、モデルに提供されたすべてのコントローラーパラメーターを一度に割り当てる機能があります。 これにより、レコードの作成が容易になりますが、悪用される可能性もあります。 これは、強力なパラメーターとして知られるRailsが提供する機能を使用することで防ぐことができます。 このように、ホワイトリストに登録されていない限り、パラメータを割り当てることはできません。 あなたのコードでは、あなたは recipe_params のパラメータ create 方法。 The recipe_paramsprivate 間違ったコンテンツや悪意のあるコンテンツがデータベースに侵入するのを防ぐために、コントローラーパラメーターをホワイトリストに登録した方法。 この場合、あなたは許可しています name, image, ingredients、 と instruction を有効に使用するためのパラメータ create 方法。

これで、レシピコントローラーがレシピを読み取って作成できるようになりました。 残っているのは、単一のレシピを読み取って削除するためのロジックです。 次のコードでレシピコントローラーを更新します。

〜/ rails_react_recipe / app / controllers / api / v1 / recipes_controller.rb
class Api::V1::RecipesController < ApplicationController
  def index
    recipe = Recipe.all.order(created_at: :desc)
    render json: recipe
  end

  def create
    recipe = Recipe.create!(recipe_params)
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

  def show
    if recipe
      render json: recipe
    else
      render json: recipe.errors
    end
  end

  def destroy
    recipe&.destroy
    render json: { message: 'Recipe deleted!' }
  end

  private

  def recipe_params
    params.permit(:name, :image, :ingredients, :instruction)
  end

  def recipe
    @recipe ||= Recipe.find(params[:id])
  end
end

新しいコード行で、プライベートを作成しました recipe 方法。 The recipe メソッドはActiveRecordを使用します find そのレシピを見つける方法 id 一致します id で提供される params それをインスタンス変数に割り当てます @recipe. の中に show アクション、レシピがによって返されるかどうかを確認しました recipe メソッドを使用してJSON応答として送信するか、そうでない場合はエラーを送信しました。

の中に destroy アクション、Rubyの安全なナビゲーション演算子を使用して同様のことをしました &.、回避します nil メソッドを呼び出すときのエラー。 これにより、レシピが存在する場合にのみレシピを削除してから、応答としてメッセージを送信できます。

これで、これらの変更が完了しました。 recipes_controller.rb、ファイルを保存してテキストエディタを終了します。

このステップでは、レシピのモデルとコントローラーを作成しました。 バックエンドでレシピを操作するために必要なすべてのロジックを作成しました。 次のセクションでは、レシピを表示するためのコンポーネントを作成します。

ステップ7—レシピの表示

このセクションでは、レシピを表示するためのコンポーネントを作成します。 最初に、既存のすべてのレシピを表示できるページを作成し、次に別のページを作成して個々のレシピを表示します。

まず、すべてのレシピを表示するページを作成します。 ただし、データベースは現在空であるため、これを行う前に、使用するレシピが必要です。 Railsは、アプリケーションのシードデータを作成する機会を提供します。

シードファイルを開きます seeds.rb 編集するには:

  1. nano ~/rails_react_recipe/db/seeds.rb

このシードファイルの内容を次のコードに置き換えます。

〜/ rails_react_recipe / db / seeds.rb
9.times do |i|
  Recipe.create(
    name: "Recipe #{i + 1}",
    ingredients: '227g tub clotted cream, 25g butter, 1 tsp cornflour,100g parmesan, grated nutmeg, 250g fresh fettuccine or tagliatelle, snipped chives or chopped parsley to serve (optional)',
    instruction: 'In a medium saucepan, stir the clotted cream, butter, and cornflour over a low-ish heat and bring to a low simmer. Turn off the heat and keep warm.'
  )
end

このコードでは、ループを使用して、Railsに9つのレシピを作成するように指示しています。 name, ingredients、 と instruction. ファイルを保存して終了します。

このデータをデータベースにシードするには、ターミナルウィンドウで次のコマンドを実行します。

  1. rails db:seed

このコマンドを実行すると、データベースに9つのレシピが追加されます。 これで、それらをフェッチしてフロントエンドでレンダリングできます。

すべてのレシピを表示するコンポーネントは、 index でのアクション RecipesController すべてのレシピのリストを取得します。 これらのレシピは、ページのカードに表示されます。

作成する Recipes.jsx のファイル app/javascript/components ディレクトリ:

  1. nano ~/rails_react_recipe/app/javascript/components/Recipes.jsx

ファイルが開いたら、次の行を追加して、ReactモジュールとLinkモジュールをファイルにインポートします。

〜/ rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react";
import { Link } from "react-router-dom";

次に、 Recipes を拡張するクラス React.Component クラス。 次の強調表示されたコードを追加して、拡張するReactコンポーネントを作成します React.Component:

〜/ rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipes extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: []
    };
  }

}
export default Recipes;

コンストラクター内で、レシピの状態を保持する state オブジェクトを初期化しています。これは、初期化時に空の配列です([]).

次に、 componentDidMount Recipeクラスのメソッド。 componentDidMount メソッドは、コンポーネントがマウントされた直後に呼び出されるReactライフサイクルメソッドです。 このライフサイクルメソッドでは、すべてのレシピをフェッチするための呼び出しを行います。 これを行うには、次の行を追加します。

〜/ rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipes extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: []
    };
  }

  componentDidMount() {
      const url = "/api/v1/recipes/index";
      fetch(url)
        .then(response => {
          if (response.ok) {
            return response.json();
          }
          throw new Error("Network response was not ok.");
        })
        .then(response => this.setState({ recipes: response }))
        .catch(() => this.props.history.push("/"));
  }

}
export default Recipes;

あなたの中で componentDidMount メソッドでは、 FetchAPIを使用してすべてのレシピをフェッチするHTTP呼び出しを行いました。 応答が成功すると、アプリケーションはレシピの配列をレシピ状態に保存します。 エラーが発生した場合は、ユーザーをホームページにリダイレクトします。

最後に、 render のメソッド Recipe クラス。 render メソッドは、コンポーネントがレンダリングされるときに評価され、ブラウザーページに表示されるReact要素を保持します。 この場合、 render メソッドは、コンポーネントの状態からレシピのカードをレンダリングします。 次の強調表示された行をに追加します Recipes.jsx:

〜/ rails_react_recipe / app / javascript / components / Recipes.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipes extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      recipes: []
    };
  }

  componentDidMount() {
    const url = "/api/v1/recipes/index";
    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipes: response }))
      .catch(() => this.props.history.push("/"));
  }
  render() {
    const { recipes } = this.state;
    const allRecipes = recipes.map((recipe, index) => (
      <div key={index} className="col-md-6 col-lg-4">
        <div className="card mb-4">
          <img
            src={recipe.image}
            className="card-img-top"
            alt={`${recipe.name} image`}
          />
          <div className="card-body">
            <h5 className="card-title">{recipe.name}</h5>
            <Link to={`/recipe/${recipe.id}`} className="btn custom-button">
              View Recipe
            </Link>
          </div>
        </div>
      </div>
    ));
    const noRecipe = (
      <div className="vw-100 vh-50 d-flex align-items-center justify-content-center">
        <h4>
          No recipes yet. Why not <Link to="/new_recipe">create one</Link>
        </h4>
      </div>
    );

    return (
      <>
        <section className="jumbotron jumbotron-fluid text-center">
          <div className="container py-5">
            <h1 className="display-4">Recipes for every occasion</h1>
            <p className="lead text-muted">
              We’ve pulled together our most popular recipes, our latest
              additions, and our editor’s picks, so there’s sure to be something
              tempting for you to try.
            </p>
          </div>
        </section>
        <div className="py-5">
          <main className="container">
            <div className="text-right mb-3">
              <Link to="/recipe" className="btn custom-button">
                Create New Recipe
              </Link>
            </div>
            <div className="row">
              {recipes.length > 0 ? allRecipes : noRecipe}
            </div>
            <Link to="/" className="btn btn-link">
              Home
            </Link>
          </main>
        </div>
      </>
    );
  }
}
export default Recipes;

保存して終了 Recipes.jsx.

すべてのレシピを表示するコンポーネントを作成したので、次のステップはそのルートを作成することです。 にあるフロントエンドルートファイルを開きます app/javascript/routes/Index.jsx:

  1. nano app/javascript/routes/Index.jsx

次の強調表示された行をファイルに追加します。

〜/ rails_react_recipe / app / javascript / routers / Index.jsx
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";

export default (
  <Router>
    <Switch>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
    </Switch>
  </Router>
);

ファイルを保存して終了します。

この時点で、コードが正しく機能していることを確認することをお勧めします。 以前と同じように、次のコマンドを使用してサーバーを起動します。

  1. rails s --binding=127.0.0.1

先に進み、ブラウザでアプリを開きます。 ホームページのレシピの表示ボタンをクリックすると、シードレシピが表示されます。

使用する CTRL+C ターミナルウィンドウでサーバーを停止し、プロンプトを元に戻します。

アプリケーションに存在するすべてのレシピを表示できるようになったので、次に、個々のレシピを表示するための2番目のコンポーネントを作成します。 作成する Recipe.jsx のファイル app/javascript/components ディレクトリ:

  1. nano app/javascript/components/Recipe.jsx

と同じように Recipes コンポーネントの場合、次の行を追加してReactモジュールとLinkモジュールをインポートします。

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react";
import { Link } from "react-router-dom";

次に、 Recipe 拡張するクラス React.Component 強調表示されたコード行を追加してクラスを作成します。

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }
}

export default Recipe;

あなたのように Recipes コンポーネント、コンストラクターで、レシピの状態を保持する状態オブジェクトを初期化しました。 あなたはまた、 addHtmlEntities 方法 this そのため、コンポーネント内でアクセスできます。 The addHtmlEntities メソッドは、コンポーネント内の文字エンティティをHTMLエンティティに置き換えるために使用されます。

特定のレシピを見つけるために、アプリケーションには id レシピの。 これはあなたの Recipe コンポーネントは、 id param. これには、 props コンポーネントに渡されます。

次に、 componentDidMount アクセスする方法 id param から match の鍵 props 物体。 あなたが取得したら id、次に、レシピをフェッチするためのHTTPリクエストを作成します。 次の強調表示された行をファイルに追加します。

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;

    const url = `/api/v1/show/${id}`;

    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

}

export default Recipe;

の中に componentDidMount メソッド、オブジェクトの破壊を使用すると、 id param から props オブジェクト、次にFetch APIを使用して、HTTPリクエストを作成し、 id を使用してコンポーネントの状態に保存します setState 方法。 レシピが存在しない場合、アプリはユーザーをレシピページにリダイレクトします。

次に、 addHtmlEntities メソッド。文字列を受け取り、エスケープされたすべての開き角かっこと閉じ角かっこをHTMLエンティティに置き換えます。 これは、レシピ命令に保存されたエスケープ文字を変換するのに役立ちます。

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;

    const url = `/api/v1/show/${id}`;

    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

  addHtmlEntities(str) {
    return String(str)
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">");
  }
}

export default Recipe;

最後に、 render 状態からレシピを取得し、それをページにレンダリングするメソッド。 これを行うには、次の強調表示された行を追加します。

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;

    const url = `/api/v1/show/${id}`;

    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

  addHtmlEntities(str) {
    return String(str)
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">");
  }

  render() {
    const { recipe } = this.state;
    let ingredientList = "No ingredients available";

    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }
    const recipeInstruction = this.addHtmlEntities(recipe.instruction);

    return (
      <div className="">
        <div className="hero position-relative d-flex align-items-center justify-content-center">
          <img
            src={recipe.image}
            alt={`${recipe.name} image`}
            className="img-fluid position-absolute"
          />
          <div className="overlay bg-dark position-absolute" />
          <h1 className="display-4 position-relative text-white">
            {recipe.name}
          </h1>
        </div>
        <div className="container py-5">
          <div className="row">
            <div className="col-sm-12 col-lg-3">
              <ul className="list-group">
                <h5 className="mb-2">Ingredients</h5>
                {ingredientList}
              </ul>
            </div>
            <div className="col-sm-12 col-lg-7">
              <h5 className="mb-2">Preparation Instructions</h5>
              <div
                dangerouslySetInnerHTML={{
                  __html: `${recipeInstruction}`
                }}
              />
            </div>
            <div className="col-sm-12 col-lg-2">
              <button type="button" className="btn btn-danger">
                Delete Recipe
              </button>
            </div>
          </div>
          <Link to="/recipes" className="btn btn-link">
            Back to recipes
          </Link>
        </div>
      </div>
    );
  }

}

export default Recipe;

これで render この方法では、コンマで区切られた材料を配列に分割してその上にマッピングし、材料のリストを作成します。 材料がない場合、アプリは材料がありませんというメッセージを表示します。 また、レシピ画像をヒーロー画像として表示し、レシピ指示の横にレシピ削除ボタンを追加し、レシピページにリンクするボタンを追加します。

ファイルを保存して終了します。

表示するには Recipe ページ上のコンポーネントをルートファイルに追加します。 ルートファイルを開いて編集します。

  1. nano app/javascript/routes/Index.jsx

次に、次の強調表示された行をファイルに追加します。

〜/ rails_react_recipe / app / javascript / routers / Index.jsx
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";

export default (
  <Router>
    <Switch>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
      <Route path="/recipe/:id" exact component={Recipe} />
    </Switch>
  </Router>
);

このルートファイルでは、 Recipe コンポーネントとそのルートを追加しました。 そのルートには :id param に置き換えられます id あなたが見たいレシピの。

使用 rails s サーバーを再起動するコマンドを実行してから、 http://localhost:3000 ブラウザで。 レシピの表示ボタンをクリックして、レシピページに移動します。 レシピページで、レシピの表示ボタンをクリックしてレシピを表示します。 データベースからのデータが入力されたページが表示されます。

このセクションでは、データベースに9つのレシピを追加し、これらのレシピを個別におよびコレクションとして表示するためのコンポーネントを作成しました。 次のセクションでは、レシピを作成するためのコンポーネントを追加します。

ステップ8—レシピの作成

使用可能な食品レシピアプリケーションを作成するための次のステップは、新しいレシピを作成する機能です。 このステップでは、レシピを作成するためのコンポーネントを作成します。 このコンポーネントには、ユーザーから必要なレシピの詳細を収集するためのフォームが含まれ、 create でのアクション Recipe レシピデータを保存するコントローラー。

作成する NewRecipe.jsx のファイル app/javascript/components ディレクトリ:

  1. nano app/javascript/components/NewRecipe.jsx

新しいファイルで、これまでに他のコンポーネントで使用したReactモジュールとLinkモジュールをインポートします。

〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
import React from "react";
import { Link } from "react-router-dom";

次に、 NewRecipe 拡張するクラス React.Component クラス。 次の強調表示されたコードを追加して、拡張するReactコンポーネントを作成します react.Component:

〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
import React from "react";
import { Link } from "react-router-dom";

class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }
}

export default NewRecipe;

の中に NewRecipe コンポーネントのコンストラクター、状態オブジェクトを空で初期化しました name, ingredients、 と instruction 田畑。 これらは、有効なレシピを作成するために必要なフィールドです。 また、3つの方法があります。 onChange, onSubmit、 と stripHtmlEntities、あなたがバインドした this. これらのメソッドは、状態の更新、フォームの送信、および特殊文字の変換( <)エスケープ/エンコードされた値( &lt;)、 それぞれ。

次に、を作成します stripHtmlEntities 強調表示された行をに追加することにより、メソッド自体 NewRecipe 成分:

〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }

  stripHtmlEntities(str) {
    return String(str)
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  }

}

export default NewRecipe;

の中に stripHtmlEntities メソッド、あなたは置き換えています <> エスケープされた値を持つ文字。 このようにして、生のHTMLをデータベースに保存しません。

次に、 onChangeonSubmit へのメソッド NewRecipe フォームの編集と送信を処理するコンポーネント:

〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }

  stripHtmlEntities(str) {
    return String(str)
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  }

  onChange(event) {
    this.setState({ [event.target.name]: event.target.value });
  }

  onSubmit(event) {
    event.preventDefault();
    const url = "/api/v1/recipes/create";
    const { name, ingredients, instruction } = this.state;

    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
      return;

    const body = {
      name,
      ingredients,
      instruction: instruction.replace(/\n/g, "<br> <br>")
    };

    const token = document.querySelector('meta[name="csrf-token"]').content;
    fetch(url, {
      method: "POST",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    })
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.props.history.push(`/recipe/${response.id}`))
      .catch(error => console.log(error.message));
  }

}

export default NewRecipe;

の中に onChange この方法では、ES6 計算されたプロパティ名を使用して、すべてのユーザー入力の値を、州内の対応するキーに設定しました。 の中に onSubmit メソッドでは、必要な入力がどれも空でないことを確認しました。 次に、新しいレシピを作成するためにレシピコントローラーに必要なパラメーターを含むオブジェクトを作成します。 正規表現を使用して、命令内のすべての改行文字をブレークタグに置き換え、ユーザーが入力したテキスト形式を保持できるようにします。

クロスサイトリクエストフォージェリ(CSRF)攻撃から保護するために、RailsはCSRFセキュリティトークンをHTMLドキュメントに添付します。 このトークンは、GET リクエストが行われます。 とともに token 上記のコードで定数である場合、アプリケーションはサーバー上のトークンを検証し、セキュリティトークンが期待されるものと一致しない場合は例外をスローします。 の中に onSubmit メソッドでは、アプリケーションはRailsによってHTMLドキュメントに埋め込まれた CSRFトークンを取得し、JSON文字列を使用してHTTPリクエストを作成します。 レシピが正常に作成されると、アプリケーションはユーザーをレシピページにリダイレクトし、そこで新しく作成されたレシピを表示できます。

最後に、 render ユーザーが作成したいレシピの詳細を入力するためのフォームをユーザーがレンダリングするメソッド:

〜/ rails_react_recipe / app / javascript / components / NewRecipe.jsx
class NewRecipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: "",
      ingredients: "",
      instruction: ""
    };

    this.onChange = this.onChange.bind(this);
    this.onSubmit = this.onSubmit.bind(this);
    this.stripHtmlEntities = this.stripHtmlEntities.bind(this);
  }

  stripHtmlEntities(str) {
    return String(str)
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  }

  onChange(event) {
    this.setState({ [event.target.name]: event.target.value });
  }

  onSubmit(event) {
    event.preventDefault();
    const url = "/api/v1/recipes/create";
    const { name, ingredients, instruction } = this.state;

    if (name.length == 0 || ingredients.length == 0 || instruction.length == 0)
      return;

    const body = {
      name,
      ingredients,
      instruction: instruction.replace(/\n/g, "<br> <br>")
    };

    const token = document.querySelector('meta[name="csrf-token"]').content;
    fetch(url, {
      method: "POST",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(body)
    })
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.props.history.push(`/recipe/${response.id}`))
      .catch(error => console.log(error.message));
  }

  render() {
    return (
      <div className="container mt-5">
        <div className="row">
          <div className="col-sm-12 col-lg-6 offset-lg-3">
            <h1 className="font-weight-normal mb-5">
              Add a new recipe to our awesome recipe collection.
            </h1>
            <form onSubmit={this.onSubmit}>
              <div className="form-group">
                <label htmlFor="recipeName">Recipe name</label>
                <input
                  type="text"
                  name="name"
                  id="recipeName"
                  className="form-control"
                  required
                  onChange={this.onChange}
                />
              </div>
              <div className="form-group">
                <label htmlFor="recipeIngredients">Ingredients</label>
                <input
                  type="text"
                  name="ingredients"
                  id="recipeIngredients"
                  className="form-control"
                  required
                  onChange={this.onChange}
                />
                <small id="ingredientsHelp" className="form-text text-muted">
                  Separate each ingredient with a comma.
                </small>
              </div>
              <label htmlFor="instruction">Preparation Instructions</label>
              <textarea
                className="form-control"
                id="instruction"
                name="instruction"
                rows="5"
                required
                onChange={this.onChange}
              />
              <button type="submit" className="btn custom-button mt-3">
                Create Recipe
              </button>
              <Link to="/recipes" className="btn btn-link mt-3">
                Back to recipes
              </Link>
            </form>
          </div>
        </div>
      </div>
    );
  }

}

export default NewRecipe;

renderメソッドには、3つの入力フィールドを含むフォームがあります。 1つは recipeName, recipeIngredients、 と instruction. 各入力フィールドには onChange を呼び出すイベントハンドラー onChange 方法。 また、 onSubmit を呼び出す送信ボタンのイベントハンドラー onSubmit 次にフォームデータを送信するメソッド。

ファイルを保存して終了します。

ブラウザでこのコンポーネントにアクセスするには、ルートファイルをそのルートで更新します。

  1. nano app/javascript/routes/Index.jsx

ルートファイルを更新して、次の強調表示された行を含めます。

〜/ rails_react_recipe / app / javascript / routers / Index.jsx
import React from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Home from "../components/Home";
import Recipes from "../components/Recipes";
import Recipe from "../components/Recipe";
import NewRecipe from "../components/NewRecipe";

export default (
  <Router>
    <Switch>
      <Route path="/" exact component={Home} />
      <Route path="/recipes" exact component={Recipes} />
      <Route path="/recipe/:id" exact component={Recipe} />
      <Route path="/recipe" exact component={NewRecipe} />
    </Switch>
  </Router>
);

ルートを設定したら、ファイルを保存して終了します。 開発サーバーを再起動して、 http://localhost:3000 ブラウザで。 レシピページに移動し、新しいレシピの作成ボタンをクリックします。 データベースにレシピを追加するためのフォームを含むページがあります。

必要なレシピの詳細を入力し、レシピの作成ボタンをクリックします。 ページに新しく作成されたレシピが表示されます。

このステップでは、レシピを作成する機能を追加することで、食品レシピアプリケーションに命を吹き込みました。 次のステップでは、レシピを削除する機能を追加します。

ステップ9—レシピを削除する

このセクションでは、レシピを削除できるようにレシピコンポーネントを変更します。

レシピページの削除ボタンをクリックすると、アプリケーションはデータベースからレシピを削除するリクエストを送信します。 これを行うには、 Recipe.jsx ファイル:

  1. nano app/javascript/components/Recipe.jsx

のコンストラクターで Recipe コンポーネント、バインド thisdeleteRecipe 方法:

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };
    this.addHtmlEntities = this.addHtmlEntities.bind(this);
    this.deleteRecipe = this.deleteRecipe.bind(this);
  }
...

次に、 deleteRecipe レシピコンポーネントへのメソッド:

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
    this.deleteRecipe = this.deleteRecipe.bind(this);
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/show/${id}`;
    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

  addHtmlEntities(str) {
    return String(str)
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">");
  }

  deleteRecipe() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/destroy/${id}`;
    const token = document.querySelector('meta[name="csrf-token"]').content;

    fetch(url, {
      method: "DELETE",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json"
      }
    })
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(() => this.props.history.push("/recipes"))
      .catch(error => console.log(error.message));
  }

  render() {
    const { recipe } = this.state;
    let ingredientList = "No ingredients available";
... 

の中に deleteRecipe メソッド、あなたは id 削除するレシピのURLを作成し、CSRFトークンを取得します。 次に、あなたは DELETE にリクエスト Recipes レシピを削除するコントローラー。 レシピが正常に削除されると、アプリケーションはユーザーをレシピページにリダイレクトします。

でコードを実行するには deleteRecipe 削除ボタンがクリックされるたびにメソッドを実行し、クリックイベントハンドラーとしてボタンに渡します。 追加します onClick の削除ボタンへのイベント render 方法:

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
...
return (
  <div className="">
    <div className="hero position-relative d-flex align-items-center justify-content-center">
      <img
        src={recipe.image}
        alt={`${recipe.name} image`}
        className="img-fluid position-absolute"
      />
      <div className="overlay bg-dark position-absolute" />
      <h1 className="display-4 position-relative text-white">
        {recipe.name}
      </h1>
    </div>
    <div className="container py-5">
      <div className="row">
        <div className="col-sm-12 col-lg-3">
          <ul className="list-group">
            <h5 className="mb-2">Ingredients</h5>
            {ingredientList}
          </ul>
        </div>
        <div className="col-sm-12 col-lg-7">
          <h5 className="mb-2">Preparation Instructions</h5>
          <div
            dangerouslySetInnerHTML={{
              __html: `${recipeInstruction}`
            }}
          />
        </div>
        <div className="col-sm-12 col-lg-2">
          <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
            Delete Recipe
          </button>
        </div>
      </div>
      <Link to="/recipes" className="btn btn-link">
        Back to recipes
      </Link>
    </div>
  </div>
);
...

チュートリアルのこの時点で、 Recipe.jsx ファイルは次のようになります。

〜/ rails_react_recipe / app / javascript / components / Recipe.jsx
import React from "react";
import { Link } from "react-router-dom";

class Recipe extends React.Component {
  constructor(props) {
    super(props);
    this.state = { recipe: { ingredients: "" } };

    this.addHtmlEntities = this.addHtmlEntities.bind(this);
    this.deleteRecipe = this.deleteRecipe.bind(this);
  }

  addHtmlEntities(str) {
    return String(str)
      .replace(/&lt;/g, "<")
      .replace(/&gt;/g, ">");
  }

  componentDidMount() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/show/${id}`;
    fetch(url)
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(response => this.setState({ recipe: response }))
      .catch(() => this.props.history.push("/recipes"));
  }

  deleteRecipe() {
    const {
      match: {
        params: { id }
      }
    } = this.props;
    const url = `/api/v1/destroy/${id}`;
    const token = document.querySelector('meta[name="csrf-token"]').content;
    fetch(url, {
      method: "DELETE",
      headers: {
        "X-CSRF-Token": token,
        "Content-Type": "application/json"
      }
    })
      .then(response => {
        if (response.ok) {
          return response.json();
        }
        throw new Error("Network response was not ok.");
      })
      .then(() => this.props.history.push("/recipes"))
      .catch(error => console.log(error.message));
  }

  render() {
    const { recipe } = this.state;
    let ingredientList = "No ingredients available";
    if (recipe.ingredients.length > 0) {
      ingredientList = recipe.ingredients
        .split(",")
        .map((ingredient, index) => (
          <li key={index} className="list-group-item">
            {ingredient}
          </li>
        ));
    }

    const recipeInstruction = this.addHtmlEntities(recipe.instruction);

    return (
      <div className="">
        <div className="hero position-relative d-flex align-items-center justify-content-center">
          <img
            src={recipe.image}
            alt={`${recipe.name} image`}
            className="img-fluid position-absolute"
          />
          <div className="overlay bg-dark position-absolute" />
          <h1 className="display-4 position-relative text-white">
            {recipe.name}
          </h1>
        </div>
        <div className="container py-5">
          <div className="row">
            <div className="col-sm-12 col-lg-3">
              <ul className="list-group">
                <h5 className="mb-2">Ingredients</h5>
                {ingredientList}
              </ul>
            </div>
            <div className="col-sm-12 col-lg-7">
              <h5 className="mb-2">Preparation Instructions</h5>
              <div
                dangerouslySetInnerHTML={{
                  __html: `${recipeInstruction}`
                }}
              />
            </div>
            <div className="col-sm-12 col-lg-2">
              <button type="button" className="btn btn-danger" onClick={this.deleteRecipe}>
                Delete Recipe
              </button>
            </div>
          </div>
          <Link to="/recipes" className="btn btn-link">
            Back to recipes
          </Link>
        </div>
      </div>
    );
  }
}

export default Recipe;

ファイルを保存して終了します。

アプリケーションサーバーを再起動し、ホームページに移動します。 レシピの表示ボタンをクリックして既存のすべてのレシピを表示し、個々のレシピを表示し、ページのレシピの削除ボタンをクリックして記事を削除します。 レシピページにリダイレクトされ、削除されたレシピは存在しなくなります。

削除ボタンが機能することで、完全に機能するレシピアプリケーションが完成しました。

結論

このチュートリアルでは、データベースとしてPostgreSQLを使用し、スタイリングにBootstrapを使用して、RubyonRailsとReactフロントエンドを使用して食品レシピアプリケーションを作成しました。 より多くのRubyonRailsコンテンツを実行したい場合は、 SSHトンネルを使用した3層Railsアプリケーションでの通信の保護チュートリアルを参照するか、コーディング方法に進んでください。 RubyシリーズでRubyスキルをリフレッシュします。 Reactをさらに深く掘り下げるには、Reactを使用してDigitalOceanAPIからデータを表示する方法の記事を試してください。

モバイルバージョンを終了