Rails5.1にReactを導入

react-railsとwebpackerの場合

インストール

Gemfileに以下を追加して、bundle install

gem 'react-rails' 
gem 'webpacker'

installコマンド実行

rails webpacker:install  
rails webpacker:install:react   

これでReactに必要なフォルダやファイルが生成されてReactが入る。 webpackerによりapp/javascript/packsにhello_react.jsxが生成されているのがわかる。
これはReactを使って、「Hello React」を表示させる簡単なものである。 そして、webpackerにより使えるようになったを追加していく。

Reactを使って「Hello React」を表示

コントローラー、ビュー、ルーティングを生成
rails g controller sample index

app/views/layouts/application.html.erbに以下のようにjavascript_pack_tagを追加

<!DOCTYPE html>
<html>
<head>
  <title>CalculatorReact2</title>
  <%= csrf_meta_tags %>
  <%= stylesheet_link_tag 'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <%= javascript_pack_tag 'hello_react' %>
</head>
<body>
  <%= yield %>
</body>
</html>

この状態でrailsサーバーを起動すると、「Hello React」が表示される。

react-railsのReactコンポーネントの作成

react-railsではReactコンポーネントは以下のコマンドで作成できる。

rails g react:component コンポーネント名

そうするとapp/javascript/component/ の配下にコンポーネント名.jsが生成される。

例えば、rails g react:component calculator だと

app/javascript/component/calculator.jsが生成される。

あとは好きなviewのそのコンポーネントを使いたいところで
<% react_component 'calculator' %>というようなview helperを記載しておくと、app/javascript/component/calculator.jsを読み込んでくれる。

app/views/layouts/application.html.erbには<%= javascript_pack_tag 'application' %>を記載しておけば読み込んでくれる。

react-rails のapplication.js

webpackerと比べて以下が追加されている。

var componentRequireContext = require.context("components", true)
var ReactRailsUJS = require("react_ujs")
ReactRailsUJS.useContext(componentRequireContext)

rails g react:componet で作成したコンポーネントファイルに以下を書かなくてもいい。

import ReactDOM from 'react-dom';
ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

 

webpackerのみ場合

react-railsがないとrailsサーバーを起動しても、Reactは動いてくれない。

railsサーバー起動後、別ターミナルで以下を叩きコンパイルする

./bin/webpack-dev-server

このままではreactを変更するたびにコンパイルしなければいけないので、

package.jsonに以下を追加し、

"scripts": {
"start": "rails s & bin/webpack-dev-server"
},

yarn startで同時に行ってくれる。

webpackerで読み込むコンポーネント

コンポーネントを入れるフォルダを作成

webpackerのみの場合、app/javascript/配下にはpacksフォルダしか生成されないので、ここで任意のフォルダを作成する。

app/javascript/calculator/

コンポーネントファイルを作成

app/javascript/calculator/calculator.js

import React, { Component } from 'react';

class Calculator extends Component {
 render() {
  return (
   <div>
    <h1>Calculator is working!!</h1>
   </div>
  )
 }
}
export default Calculator;

app/javascript/calculator/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Calculator from './calculator';

document.addEventListener('DOMContentLoaded', ()=> {
  const container = document.body.appendChild(document.createElement('div'));
  ReactDOM.render(<Calculator/>, container)
})

Viewで読み込む

app/javascript/packs/calculator.jsを作成し、その中に読み込むフォルダを記載

import 'calculator'

そうするとapp/javascript/calculator/ 配下のコンポーネントをimportする。

あとはapp/views/layouts/application.html.erbのHEADタグ内にreact-railsの時と同じようにjavascript_pack_tagを追加し、app/javascript/packs/calculator.jsを指定してあげるとviewで読み込んでくれる。

<!DOCTYPE html>
<html>
<head>
  <title>CalculatorReact2</title>
  <%= csrf_meta_tags %>
  <%= stylesheet_link_tag 'application', media: 'all' %>
  <%= javascript_include_tag 'application' %>
  <%= javascript_pack_tag 'calculator' %>
</head>
<body>
  <%= yield %>
</body>
</html>

複数のフォームがあり空のフォームはサブミットされないようにする

したいこと

二つのフォームがあり片方は何も書かれていない空の状態であれば、そのフォームをサブミットせずに記入されている片方のフォームだけ送信する方法。
この記事のように二つのフォームが同じモデルへ送信するのであれば、これは意味がないが、それぞれが別のモデルへ保存される時に便利。
今回はこの一つのフォームで複数のモデルへ保存するフォームに実装することを想定して作例する。 ただし、そこまで深入りしないが、今回はお互いのフォームはアソシエーションで紐づいていて、親子関係にあるため、親モデルのフォームが空であっても、親モデルのレコードは作成されるようにしている。

ポイント

  • サブミットするときの関数を設定する。
  • クリックすると、jqueryでフォームが空であるか調査し、空であればそのフォームにdisabled属性を加え、フォームを送信

View.html.erb

<%= form_with(model: review, local: true, id: "review_form") do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content, id: :review_content %>
  </div>
  <%= form.fields_for :phrases do |phrase_field| %>
    <div class="field">
      <%= phrase_field.text_area :content, id: :phrase_content %>
    </div>
  <% end %>
  <div class="actions">
    <button name="button" type="button" id="submit_btn">投稿</button>
  </div>
<% end %>

jquery

$(function(){
$('#review_form').on('submit', function(){
  if($("#phrase_content").val() == "") {
    $('#phrase_content').attr('disabled','true');
  };
});
});

参考

【jQuery】submit前に処理を行う方法 - Qiita

textareaの縦幅を入力行数によって自動変更させる方法

autosizeというライブラリを使います

autosize | RubyGems.org | your community gem host

デモはこちら

方法

  1. Gemfileに記入
    gem 'autosize', '~> 2.4'
    bundleinstallを実行
  2. app/assets/javascripts/application.jsに以下を追記 //= require autosize

本来ならこれでできるはずだけど、エラーが出てassets配下を探していたので、gem environmentを実行して、GEM PATHを頼りにgemがインストールされているところへ行き、app/assets/javascripts/へautosize.jsファイルをコピーした

jqueryコード

autosize($('textarea'));
たったこれだけで、縦幅が自動で変更されます。 javascriptのコードやいろんなオプションはこちらを参考に

一つのフォームで複数のモデルに保存する

したいこと

今回は映画のレビューを投稿するreview投稿フォーム画面に、 映画の好きなフレーズを投稿できるphrase投稿フォームも表示し、 一度送信ボタンへ押すだけでreviewはもちろん、そのreviewに紐づいたphraseも保存する。 なので、reviewモデルとそのreviewモデルに紐づいたphraseモデルが存在している。

重要なメソッド

  • field_forメソッド
  • accepts_nested_attributes_for

バージョン

rails 5.1.4

 カラム

reviewsテーブルにはcontentカラム
phrasesテーブルにもcontentカラムのみとする

アソシエーション定義

model/review.rb

  has_many :phrases, dependent: :destroy
  accepts_nested_attributes_for :phrases

model/phrase.rb

belongs_to :review, optional: true

Controller

def new
  review = Review.new
  2.times { @review.phrases.build }
end

def create
  @review = Review.new(review_params)
  @review.save
end

private
def review_params
  params.require(:review).permit(:content,  phrases_attributes: [:content])
end

View

<%= form_with(model: review, local: true) do |form| %>
  <div class="field">
    <%= form.label :content %>
    <%= form.text_area :content, id: :review_content %>
  </div>
  <%= form.fields_for :phrases do |phrase_field| %>
    <div class="field">
      <%= phrase_field.text_area :content, id: :phrase_content %>
    </div>
  <% end %>
  <%= form.submit %>
<% end %>

学んだこと

  • field_forメソッドで異なるモデルへ保存するためのフォームを作成できる。controller#newでtimesメソッドを使ってインタンスを作成しておけば、viewではeach文を使って保存しなくても、(field_forだけではなく)form.fields_forでコントローラーで作成したインスタンス分のフォームを表示してくれる。
  • accepts_nested_attributes_forをモデルに追加することでStrongParameterに記載しているネストされたパラメーター(phrases_attributes: [:content])を読み取り、それぞれのモデルのレコードを更新できる。
  • rails5からはbelongs_to :review, optional: trueにしないと外部キー(〇〇_id)がもしnilの場合、保存ができなくなっているため、optional: trueをつけておくとそれを無効化しておく。ただ、今回はあってもなくても、どちらでも大丈夫。

参考

deviseで作ったuserモデルにカラムを追加し、sign up時に登録されるようにする

gemを読む

GitHub - plataformatec/devise: Flexible authentication solution for Rails with Warden.

この部分

# In case you want to permit additional parameters (the lazy way™), you can do so using a simple before filter in your   
`ApplicationController:`

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
end

まず追加したいカラムを通常どうりmigrationファイルで作成

def change
    add_column :users, :name, :string
end

パラメーターで送れるようにビューのフォームに追加したい項目を追記

<div class="field">
    <%= f.label :name %><br />
    <%= f.text_field :name %>
</div>

次に上記のコードをApplicationControllerに追記する。変更するのは追加したいカラム名のみ

class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
  end
end

HerokuとS3を連携

S3の初期設定

この記事のバケットの作成のところ(Imagemagikの導入の手前)までの通りすればできました。

【Rails】S3へ『CarrierWave+fog』を使って画像アップロードする方法 | vdeep

Gemfile

こちらを追加し、bundle install

gem 'fog-aws', group: :production
gem 'carrierwave', '~> 1.0'

○_uploader/rbに以下を追記

if Rails.env.production?
    storage :fog
  else
    storage :file
  end

carrier_wave.rb

$ touch config/initializers/carrier_wave.rbでファイル作成
作成したファイルに以下を追記

require 'carrierwave/storage/abstract'
require 'carrierwave/storage/file'
require 'carrierwave/storage/fog'


CarrierWave.configure do |config|
  if Rails.env.production?
    config.storage = :fog
    config.fog_provider = 'fog/aws'
    config.fog_credentials = {
      # Amazon S3用の設定
      :provider              => 'AWS',
      :region                => ENV['S3_REGION'],
      :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key => ENV['S3_SECRET_KEY']
    }

    config.fog_directory     =  ENV['S3_BUCKET']
  else
    config.storage :file
    config.enable_processing = false if Rails.env.test?
  end
end

ここの下の画像の人の例を参考にしました。 f:id:shoheimoment:20171215112549p:plain

herokuの環境変数の設定

S3の初期設定で取得できているcredentials を前項のENVの[ ]内の変数を下記コマンドで設定していく。

heroku config:set S3_REGION=
heroku config:set S3_ACCESS_KEY=
heroku config:set S3_SECRET_KEY=
heroku config:set S3_BUCKET=

※credentialsがわからなくなったら、アクセスキーIDとREGIONは次から確認できる。 ルートアカウント認証情報を使用してサインイン→ヘッダーのユーザ名のdropdownからセキュリティ認証情報を選択→左のユーザー→ユーザーを選択→認証情報タブ secret keyはどう確認するかわからないからユーザーを作った時にcredentials.csvをダウンロードしておこう!

デプロイ

これでデプロイしてみるとできている。できなければ、DATABASEのリセットとかを試してみるといいかも。

file_filedの注意点

今回、以下のformで画像をupすることにして、空で送ってしまったときはバリデーションにひっかかるように設定しようと思った。

<%= form_with(model: @user_icon, local: true) do |form| %>
  <%= form.file_field :image %>
  <%= form.submit "更新する" %>
<% end %>

しかし、これで送信すると、以下のようにparameterにrequire(:user_icon)のkeyがそもそもないため、コントローラーのstrong_paramsでエラーになってしまう。
user_icons_controller.rb

def create
    @user_icon = UserIcon.new(user_icon_params)
    if @user_icon.save
      redirect_to user_path(@user_icon.user_id), notice: 'アイコンを設定しました'
    else
      render 'users/show'
    end
  end
#省略
private
  def user_icon_params
    params.require(:user_icon).permit(:user_id, :image)
  end

エラー詳細

[2] pry(#<UserIconsController>)> params
=> <ActionController::Parameters {"utf8"=>"✓", "authenticity_token"=>"62WAR0vsxA+HqJabeXbmOOOCF7TfE+x9WLp0yXFOTes2+dFb8AHh0hq36kOsR7eH4zmbk8GvSjdqdAgvKwjnjA==", "commit"=>"更新する", "controller"=>"user_icons", "action"=>"create"} permitted: false>

とりあえずは、下記のようにform.file_fieldタグの他に、今回アソシエーションで紐づいて必要だったuser_idをhiddenタグをparameterに仕込むことで解決できた。

<%= form_with(model: @user_icon, local: true) do |form| %>
  <%= form.hidden_field :user_id, value: "#{current_user.id}" %>
  <%= form.file_field :image %>
  <%= form.submit "更新する" %>
<% end %>

ただ、今までnameやemailを全て空欄で送信してもバリデーションにひっかけることができたのに、なぜ今回はkey(この例では:user_icon)が設定されないのか不思議である。