20200929のRailsに関する記事は15件です。

もう迷わない!RSpec導入の流れ

RSpecとは

 Railsにおいて、単体テストコードを記述するときに用いるGemのこと。

テストコードを記述する直前までの大まかな流れ

  1. Gemfileに追記
  2. Gemをインストール
  3. rspecをインストール
  4. テストコードをターミナル上で可視化できるようにする
  5. FactoryBotとFakerのGemをGemfileに追記
  6. Gemのインストール
  7. (テスト用の画像を用意)

1. Gemfileに追記

gem 'rspec-rails'と記述する。
必ず、group :development, :test do ~ endのグループ内に

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
end

2. Gemをインストール

ターミナルでカレントディレクトリがテストコードを使用するアプリケーションのディレクトリであることを確認の上

bundle install

3. rspecをインストール

rails g rspec:install

これによって、「specディレクトリ」や「.rspecファイル」が生成される

4. テストコードをターミナル上で可視化できるようにする

.rspec
--format documentation

を追記する。

5. FactoryBotとFakerのGemをGemfileに追記

必要に応じてGemを導入。今回はチャットアプリを想定しているため、導入。

Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
end

先ほどと同じgroupの中に記述。

6. Gemのインストール

bundle install

7. (テスト用の画像を用意)

public/imagesにファイル名「test_image.png」で配置。

ポイント

  • rspecは「bundle install」だけでなく、「rails g rspec:install」をして初めて使える。
  • 記述するのは、group :development, :test do ~ endのグループ内。

最後に

bundle installはRailsに取り込んだだけ!
rails g rspec:installで解凍するイメージ!

細かいテストコードはまた別の機会に。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

rails routesからパスを作る

環境

  • AWS cloud9 EC2
  • ruby 2.6.3
  • Rails 5.2.4.3

アクションのパスを作る方法

ルーティングの確認

 rails routesを実行する

Terminal
vocstartsoft:~/environment/***** $ rails routes

         Prefix Verb   URI Pattern                                                                              
          books GET    /books(.:format)                 books#index
                POST   /books(.:format)                 books#create
       new_book GET    /books/new(.:format)             books#new
      edit_book GET    /books/:id/edit(.:format)        books#edit
           book GET    /books/:id(.:format)             books#show
                PATCH  /books/:id(.:format)             books#update
                PUT    /books/:id(.:format)             books#update
                DELETE /books/:id(.:format)             books#destroy

アクションのパスを作成

1. 一番右の列を見る

飛びたいアクションを確認する
(例:book#edit

2.一番左の列を見る(Prefix)

1で確認した行のPrefixの列を確認する
※もし空白の場合は、直近の上のものを使用する。
(例:edit_book

3._pathをつける

2で確認したものに _path をつける。
(例:edit_book_path

4.URI Patternの列を見る

URI Patternの列を見て :id があるか確認する。

  • idがなかったら
    3で作成したものがパスになる
  • idがあったら
    3で作成したものに引数をしてして:idを指定する
    (例:edit_book_path(@book.id))

5.HTTPメソッドを確認する

link_toなどではデフォルトでGETが行われるようになっている。
=>そのため、GET以外の場合は指定する必要がある。

  • 指定方法
    pathの後に、, method: :httpメソッドを付け加える
    (例:book_path(@book.id), method: :delete)

何故:idが必要なのか

表示したViewページが:idによって違う内容にする必要があるかどうかを考える

  • :idがないbooks#index books#newなどは、一覧の表示や新規作成のため、特定の:idを必要としない
  • :idがあるbooks#edit books#showなどは、特定の:idについての表示をする必要があるため、表示させたい:idを引数として指定する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】Webpackerにselect2導入メモ

jQuery導入

select2を使う為に、jQueryが必要です。

jQuery導入方法

select2導入

terminal
yarn add select2
app/javascript/packs/application.js
import 'select2'
import 'select2/dist/css/select2.css'

document.addEventListener('turbolinks:load', () => {
  $('.js-select').select2({
    placeholder: 'Select an option',
    allowClear: true
  })
})

カスタマイズスタイル

sass
$select2-height: 38px

.select2-container
  .select2-selection--single
    height: $select2-height
    line-height: $select2-height
    .select2-selection__arrow, .select2-selection__rendered
      height: $select2-height
      line-height: $select2-height

  .select2-search--dropdown .select2-search__field
    font-size: 16px
    padding: 4px

以上でRuby on Railsアプリケーションにselect2が導入できました。
select タグに js-select クラスを付けたら動くはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヘルパーで部分テンプレート(パーシャル)を返す

ヘルパーで部分テンプレート(パーシャル)を返す方法

if文を使って条件ごとに異なるパーシャルを返したい場合があると思います。

しかし"html.erb"ファイルの中でif文を書くとどうしてもコードが長くなってしまいますよね。
例えばこんな風に、、、

sample.html.erb
<% if A == A %>
 <%= render 'follow_button' %>
<% else %>
 <%= render 'unfollow_button' %>
<% end %>

こんな時はヘルパーメソッドに処理をまとめるとスッキリします。⬇︎

users_helper.rb
module UsersHelper

  def follow_unfollow_button(user)
    if A == A 
     render 'follow_button' 
    else 
     render 'unfollow_button' 
    end 
  end

end

ビューでhelperメソッドを使うと、、

sample.html.erb
<%= follow_unfollow_button(@user) %>

1行でスッキリしました。

まとめ

slimやhamlを使用していればif文も多少は短くなるのですが、いずれにせよビューの中にロジックが書かれているとやはりコードの可読性が下がってしまいます。
できるだけif文などのロジックやhelperやdecoratorにまとめた方が良いと思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】便利なヘルパーメソッド

はじめに

ヘルパーメソッドについて、パーフェクトRuby on Rails 【増補改訂版】を読んで個人的に学んだ内容をまとめます。

ヘルパーメソッドとは

ビューテンプレートで開発の手助けとなる便利なメソッド。HTML生成を行うものやURL生成するものなどがある。

パスとURLヘルパーメソッド

ルーティングを表示する際のPrefixの値+_pathでドメインやポートなどを除いた/から始まるパスを、Prefixの値+_urlとするとドメインなども含んだ完全なURLを返す。
パスに「:id」といった変動する値がある場合、引数にハッシュ形式で値を指定する。
url_forやlink_to、redirect_toでパスやURLを指定する際に使う。

以下のルーティングの場合、次のようなパスやURLが返る。

ルーティング

    users GET    /users(.:format)             users#index
     user GET    /users/:id(.:format)         users#show
edit_user GET    /users/:id/edit(.:format)    users#edit

返るパスやURL

  • users_path → /users
  • user_path(id: 1) → /users/1
  • edit_user_url(id: 1) → http://www.example.com/users/1/edit

url_for

Webアプリケーションのパスを構築するためのヘルパーメソッド。文字列を直接書かずにルーティング上の定義を活用できる。
単にURLを取得したいときに使用する。

url_for(users_path)
#=> /users

link_to

link_toはaタグを生成するヘルパーメソッド。
_path_urlでルーティング上の定義を活用して、画面にリンクを設置したいときに使用する。

link_to('編集', edit_user_path(id: 1))
#=> <a href="/users/1/edit">編集</a>

time_ago_in_words

ある時刻と現在時間のあいだにどの程度空きがあるかを、人間にとってわかりやすく表示するヘルパーメソッド。
最終ログイン日時などを表示するサービスで活用できそう。

time_ago_in_words(Time.current)
#=> less than a minute
time_ago_in_words(Time.current + 3.days)
#=> 3 days

number_with_delimiter

長い数字をカンマ区切りするヘルパーメソッド。オプションで区切り文字を変更することもできる。

number_with_delimiter 1234567890
#=> 1,234,567,890
number_with_delimiter 1234567890, delimiter: '@'
#=> 1@234@567@890

その他にも様々なヘルパーメソッドがRailsには用意されているので、自分で実装する前にRailsガイドを参考にすると良さそう。

独自ヘルパーメソッド

Rails組み込みのヘルパーメソッドでは用途に合わない場合は自分でヘルパーメソッドを実装する。
ヘルパーメソッドを定義するためのモジュールは「app/helpers」というディレクトリの配下に定義する。
アプリケーション全体で使うものであれば「app/helpers/application_helper.rb」に、コントローラ毎に使うようなものであれば「app/helpers/users_helper.rb」のようにコントローラに対応するhelper用のファイルを用意する。

例)全角の英文字を半角に変換するヘルパーメソッド

app/helper/application_helper.rb
module ApplicationHelper
  def to_hankaku(str)
    str.tr('A-Za-z', 'A-Za-z')
  end
end
to_hankaku('Ruby')
#=> Ruby

参考

パーフェクトRuby on Rails 【増補改訂版】

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ajaxを使ってタグの追加と削除、(成功・エラー)メッセージの表示を行う。

環境

Ruby 2.5.7
Rails 5.2.4

ライブラリ

jQuery 1.12.4

前提

今回はTagテーブルを使って解説していきます。

まずはajaxを使わなくてもタグの追加と削除ができることを事前にご確認ください。

エラーメッセージの日本語化やアソシエーションについては触れませんので必要な方はご自身でお願い致します。

turbolinksは無効にしています。(有効時の動作は確認していません。)

手順

タイトルにもある通り、次の手順でajaxを利用してタグの追加・削除・エラーメッセージの表示を実装していきます。

1.tag.rbにバリデーションの設定
2.部分テンプレートの用意(views/layouts/_error_messages.html.erb, views/layouts/_flash_messages.html.erb, views/tags/_tag.html.erb)
3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)
4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)
5.tags_controller.rb(:index, :create, :destroy)の用意

イメージとしては、送信ボタンを押すとコントローラーのcreateアクションが実行
createアクション内でcreate.js.erbを呼び出すメソッドが実行
create.js.erb内に書かれているJavaScript(jQuery)が発火
DOM操作により、エラーメッセージの要素とタグ一覧の要素が上書き(jQueryの.html()メソッド)
削除時のdestroyアクションも同様です。

完成形の流れとしては
1.index画面でフォームに値を入力する。
1-1.存在しないタグなら新規保存(サクセスメッセージ)
1-2.存在するタグなら保存失敗(バリデーションエラーメッセージ)
1-3.空白なら保存失敗(バリデーションエラーメッセージ)

2.個別のタグを削除する
1-1.サクセスメッセージを表示

全てajaxのため、ページの全体の更新はありません。

1.tag.rbにバリデーションの設定

tag.rb
class Tag < ApplicationRecord

  ...

  # 空白の禁止と一意性(重複NG)を付与します。
  validates :name, presence: true, uniqueness: true

end

2.部分テンプレートの用意(layouts/_error_messages.html.erb, layouts/_flash_messages.html.erb, _tag.html.erb)

ここで作る3つの部分テンプレートを、アクションに応じてDOM操作で上書きしていく形です。
jQueryで書くと1行1行DOM操作をすることになって冗長になってしまうので、部分テンプレートを利用して後述するjQueryが<%= render ... %>1行で済むようになっています。

バリデーションエラーメッセージ用の部分テンプレート
すでにある方は流用してもらって大丈夫です。

layouts/_error_messages.html.erb
<% if model.errors.any? %>
  <div id="validation-errors">
    <p><%= model.errors.count %>件のエラーがあります。</p>
    <ul class="error-messages">
      <% model.errors.full_messages.each do |message| %>
        <li class="error-message"><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

jQueryのDOM操作によって上書きされた時にバリデーションエラーが発生していれば、エラーメッセージが表示されます。
ローカル変数のmodelは後述するindex.html.erbで@tag(Tag.new)に置き換えます。

次にサクセスメッセージ(フラッシュメッセージ)用の部分テンプレート
こちらもすでにある方は流用してください。

layouts/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <p class="alert alert-<%= key %>">
    <%= value %>
  </p>
<% end %>

eachで配列から取得することになっていますが、これは複数のフラッシュメッセージを取得するというよりも、どのようなキー(flash[キー])でもこの一文で再表示ができるようになっています。
今回使うキーは[:success], [:warning]の2種類です。(ちなみにこの2種類はbootstrapでデザインも用意されているため、今回選択しました。)
この二つを使う場合は部分テンプレートを下記のように書き換えることもできます。

layouts/_flash_messages.html.erb
<% case flash.keys[0] %>
  <% when "success" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:success] %>
  </p>
  <% when "warning" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:warning] %>
  </p>
<% end %>

flashに格納されるキーは配列のため、keys[0]とする必要があります。
case文でkeyパターンに応じて表示するフラッシュメッセージを変化させます。
p要素のclassでkeys[0]を使用している理由は、キーによって(今回の場合はsuccessかwarning)cssのデザインを変更するためです。

しかしこれでは、キーの種類(キーの名前は必要に応じて任意でつけることができます。)が増えた時にその都度行を増やしていく必要があるため、冗長になってしまいます。

そこで今回はeach文を使用することで、その手間をなくした形です。
また部分テンプレート化することで、バリデーションエラーメッセージと同様に、他のviewでも使い回すことができます。

次にタグ一覧を表示する部分テンプレート

tags/_tag.html.erb
<div class="tags-index__tags--tag">
  <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
  <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
</div>

こちらも部分テンプレートを使わなければ後述するjQueryで4行分書く必要がり、jQueryでのDOM操作は極力シンプルに管理したいので部分テンプレート化しています。
ローカル変数のtagはindex.html.erbでインスタンス変数@tags(Tag.all)を指定することで、部分テンプレート内では個別のローカル変数tagとして自動で置き換わります。(index.html.erbのところで詳しく解説します。)
(<%= tag.menu_tag_ids.count %>)はタグ名の横にそのタグが使われているメニューの数を表示しています。(メニューテーブル(割愛) 1:多 メニュータグテーブル(中間テーブル)(割愛) 多:1 タグテーブル(今回))
<%= link_to 'X', tag_path(tag), method: :delete, remote: true %>を押すとdestroyアクションが発動します。remote: trueとすることで、ajax通信であることを明示します。

"remote: true"の仕組み
通常のリクエスト(リンク)ではHTMLを取得しますが、remote: true(html変換後はdata-remote="true"属性)が付与されたリクエストの場合はJSファイルのみを取得します。
Railsの場合はlayouts/application.html.erbの<head>タグ内にjsを読みこむタグがあるので、通常はHTMLを取得するとjsも自動で再読み込みがされます。なお、turbolinksを使っている場合はこの限りではありません。

3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)

タグの一覧画面とその中に新規入力のフォームも作成していきます。

views/tags/index.html.erb
<div class="contents tags-index">
  <div class="tags-index--messages"><!--メッセージの表示エリア--></div>
  <h2>タグ一覧</h2>
  <div class="tags-index__list" id="tags-index--tag-list">
    <%= render @tags %>
  </div>
  <div class="tags-index__form">
    <%= form_with model: @tag, url: tags_path(@tag) do |f| %>
      <%= f.text_field :name %>
      <%= f.submit %>
    <% end %>
  </div>
</div>

タグ一覧の下に新規追加フォームがくっついてる状態です。

新規登録フォームはform_withを利用します。
form_withはデフォルトがremote: true属性を持っているので省略可能です。(form_forを使う場合はremote: true属性を明示する必要があります。)
form_withでajaxを使わない場合はlocal: true属性を明示する必要があります。
フォームヘルパーはremote: trueの場合はJSのみを、local: trueの場合はHTMLをアクションに伝えます。
また、フォームヘルパーはデフォルトのHTTPメソッドがPOSTでもあるのでmethod: :post省略可能です。

<%= render @tags %>のインスタンス変数 @tagsは後述するコントローラー側でTag.allを取得しています。
見慣れない部分テンプレートの書き方ですが、この略記法の部分テンプレートは変数名に対応する_変数名.html.erbが呼び出され、その部分テンプレート内ではローカル変数(今回でいう部分テンプレート内のtag)として使えるようになります。
今回の@tagsように配列で取得した場合は、部分テンプレート内ではその配列の数だけ、繰り返し表示がされます。
これは書き換えると以下のようになります。

views/tags/index.html.erb
<%= render partial: 'tag', locals: {tags: @tags} %>
views/tags/_tag.html.erb
<% tags.each do |tag| %>
  <div class="tags-index__tags--tag">
    <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
    <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
  </div>
<% end %>

今回の記述方法はこれらを略記した形です。
Railsガイド - レイアウトとレンダリング

4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)

コントローラーから呼び出すjs(.erb)を作成します。
ここにjsを記述することで、コントローラーからcreate, destroyがそれぞれ呼び出された時に発火するjQueryでDOM操作が行われます。
ここで使われている.html()はそこに要素が何もなければ追加となり、すでに別の要素があれば書き換えとなります。

createアクション時のjs(.erb)

views/tags/create.js.erb
<% if @tag.errors.any? %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/error_messages', locals: {model: @tag}) %></p>")
<% else %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
  $("#tags-index--tag-list").html("<%= j(render @tags) %>")
  $("#tag_name").val('')
<% end %>

<% if @tag.errors.any? %>はsubimitボタンが押された時に@tagにエラーが発生しているかどうかをチェックします。

"オブジェクト.errors.any?"と"オブジェクト.invalid?"の違い
今回の場合、コントローラー側で@tag.saveが試された後にcreate.js.erbを呼んでいるため@tagにはすでにerrorが発生しているかどうかがわかる状態でjs.erbに渡って来ています。
他方、invalid?メソッドは保存前のオブジェクトに対して手動でバリデーションチェックを行うものです。
通常はオブジェクトが保存されようとした時(.saveを試みた時)に自動でバリデーションチェックが実行され、今回は.saveの成否によってアクションを分けるため、改めて手動でinvalid?をするよりは、.saveの時点ですでにエラーが発生しているかどうか(.errors.any?)を確認した方がDRYに則っていると言えます。

エラーが発生している場合(空白やタグ名の重複)
・バリデーションエラーメッセージ(error_messages.html.erb)を$(".tags-index--messages")に追加または書き換え

エラーが発生していない場合(正常に保存ができた)は
・フラッシュメッセージ(flash_message.html.erb)を$(".tags-index--messages")に追加または書き換え
・タグ一覧($("#tags-index--tag-list"))を書き換え
・フォームに入力されたタグ名をクリア($("#tag_name").val(''))

が、実行されます。

destroyアクション時のjs(.erb)

views/tags/destroy.js.erb
$(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
$("#tags-index--tag-list").html("<%= j(render @tags) %>")

create時と同じです。
・タグが削除されたらフラッシュメッセージの追加または書き換え
・タグ一覧の書き換え

が、実行されます。

"partial: "の必要性renderを使ってオプションの追加(今回でいうlocals: {tag: @tag})が必要な場合はファイル名(パス)の前にpartial:を明示する必要があります。
Railsガイド - レイアウトとレンダリング

5.tags_controller.rb(:index, :create, :destroy)の用意

コントローラーに今回必要なアクションを記載していきます

tags_controller.rb
class TagsController < ApplicationController

  def index
    @tags = Tag.all
    @tag = Tag.new
  end

  def create
    @tags = Tag.all
    @tag = Tag.new(tag_params)

    respond_to do |format|
      if @tag.save
        format.js { flash.now[:success] = "保存しました。" }
      else
        format.js
      end
    end
  end

  def destroy
    @tags = Tag.all
    @tag = Tag.new
    Tag.find(params[:id]).destroy
    flash.now[:warning] = "削除しました。"
  end

  private
  def tag_params
    params.require(:tag).permit(:name)
  end

end

今回はcreateのform_with、destroyのlink_to、それぞれでremote: true(form_withの場合はデフォルトなので省略)を明示しているため、どちらもJSのみをリクエストしていることになります。

tags_controller.rb
def create

  ...

  respond_to do |format|
    if @tag.save
      format.js { flash.now[:success] = "保存しました。" }
    else
      format.js
    end
  end
end

respond_to do |format|はリクエストの形式によって処理を分けるブロックパラメータです。
今回、タグの追加・削除についてはHTMLで要求が来る(local: true)ことは想定していませんのでformat.js {処理}のみを記載しています。
HTMLでの要求が来た時に処理を分けたい場合はformat.html {処理}を書けば、リクエストの方式に応じで処理を分けることができます。
if @tag.saveが成功した場合はflash[:success]キーに"保存しました。"というメッセージを載せてcreate.js.erbに渡します。
失敗した場合はバリデーションエラーメッセージのみを表示するため、フラッシュメッセージは使いませんので、format.jsの後ろに処理は書かず、表示のみを行います。
前述しましたが、この時にバリデーションエラーが発生している@tagをcreate.js.erbに渡して、そこでエラー処理(バリデーションエラーメッセージの表示)をしています。

destroyアクションではflash[:warning]キーに"削除しました。"というメッセージを載せてdestroy.js.erbに渡します。

"format.js"を書く場合と書かない場合
通常、コントローラーにはviewファイルに対応する同名のアクション名を作ります。(def index end ならindex.html.erb)
アクション名の中身で特にrenderredirect_toなどのに指定がなければアクションの最後にrender :アクション名が省略されて(暗示的に)実行されています。
(def index endなど、中身を書かなくてもviewがレンダリングされるのはこのためです。)
今回、createアクションでは、条件によってフラッシュメッセージを渡したいので明示的にformat.jsを記述し、その中で処理を分けていましたが、destroyアクションでは今のところ処理を分ける必要はないので、respond_toformat.jsrender :destroyは記述していません。
render :destroyに関してはすでに暗示的に記述されているため、書く必要がないということです。最初に書きましたが、暗示的にrender :アクション名が発動するのはアクションの中で他にrender :別のアクション名redirect_toがない場合に限るので、他方createアクションのように明示的にformat.jsを記述し実行された場合、アクションの最後に暗示的なrender :createが実行されることはありません。(DoubleRenderingErrorにならない理由です。)

Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

まとめ

質問や解釈の違い、記述方法に違和感ありましたら、コメント等でご指摘いただけると幸いです。

最後まで読んでいただきありがとうございました。

参考サイト

Railsガイド - レイアウトとレンダリング
Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby on Rails】ajaxを使ってタグの追加と削除、(成功・エラー)メッセージの表示を行う。

環境

Ruby 2.5.7
Rails 5.2.4

ライブラリ

jQuery 1.12.4

前提

今回はTagテーブルを使って解説していきます。

まずはajaxを使わなくてもタグの追加と削除ができることを事前にご確認ください。

エラーメッセージの日本語化やアソシエーションについては触れませんので必要な方はご自身でお願い致します。

turbolinksは無効にしています。(有効時の動作は確認していません。)

手順

タイトルにもある通り、次の手順でajaxを利用してタグの追加・削除・エラーメッセージの表示を実装していきます。

1.tag.rbにバリデーションの設定
2.部分テンプレートの用意(views/layouts/_error_messages.html.erb, views/layouts/_flash_messages.html.erb, views/tags/_tag.html.erb)
3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)
4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)
5.tags_controller.rb(:index, :create, :destroy)の用意

イメージとしては、送信ボタンを押すとコントローラーのcreateアクションが実行
createアクション内でcreate.js.erbを呼び出すメソッドが実行
create.js.erb内に書かれているJavaScript(jQuery)が発火
DOM操作により、エラーメッセージの要素とタグ一覧の要素が上書き(jQueryの.html()メソッド)
削除時のdestroyアクションも同様です。

完成形の流れとしては
1.index画面でフォームに値を入力する。
1-1.存在しないタグなら新規保存(サクセスメッセージ)
1-2.存在するタグなら保存失敗(バリデーションエラーメッセージ)
1-3.空白なら保存失敗(バリデーションエラーメッセージ)

2.個別のタグを削除する
1-1.サクセスメッセージを表示

全てajaxのため、ページの全体の更新はありません。

1.tag.rbにバリデーションの設定

tag.rb
class Tag < ApplicationRecord

  ...

  # 空白の禁止と一意性(重複NG)を付与します。
  validates :name, presence: true, uniqueness: true

end

2.部分テンプレートの用意(layouts/_error_messages.html.erb, layouts/_flash_messages.html.erb, _tag.html.erb)

ここで作る3つの部分テンプレートを、アクションに応じてDOM操作で上書きしていく形です。
jQueryで書くと1行1行DOM操作をすることになって冗長になってしまうので、部分テンプレートを利用して後述するjQueryが<%= render ... %>1行で済むようになっています。

バリデーションエラーメッセージ用の部分テンプレート
すでにある方は流用してもらって大丈夫です。

layouts/_error_messages.html.erb
<% if model.errors.any? %>
  <div id="validation-errors">
    <p><%= model.errors.count %>件のエラーがあります。</p>
    <ul class="error-messages">
      <% model.errors.full_messages.each do |message| %>
        <li class="error-message"><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

jQueryのDOM操作によって上書きされた時にバリデーションエラーが発生していれば、エラーメッセージが表示されます。
ローカル変数のmodelは後述するindex.html.erbで@tag(Tag.new)に置き換えます。

次にサクセスメッセージ(フラッシュメッセージ)用の部分テンプレート
こちらもすでにある方は流用してください。

layouts/_flash_messages.html.erb
<% flash.each do |key, value| %>
  <p class="alert alert-<%= key %>">
    <%= value %>
  </p>
<% end %>

eachで配列から取得することになっていますが、これは複数のフラッシュメッセージを取得するというよりも、どのようなキー(flash[キー])でもこの一文で再表示ができるようになっています。
今回使うキーは[:success], [:warning]の2種類です。(ちなみにこの2種類はbootstrapでデザインも用意されているため、今回選択しました。)
この二つを使う場合は部分テンプレートを下記のように書き換えることもできます。

layouts/_flash_messages.html.erb
<% case flash.keys[0] %>
  <% when "success" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:success] %>
  </p>
  <% when "warning" %>
  <p class="alert alert-<%= flash.keys[0] %>">
    <%= flash[:warning] %>
  </p>
<% end %>

flashに格納されるキーは配列のため、keys[0]とする必要があります。
case文でkeyパターンに応じて表示するフラッシュメッセージを変化させます。
p要素のclassでkeys[0]を使用している理由は、キーによって(今回の場合はsuccessかwarning)cssのデザインを変更するためです。

しかしこれでは、キーの種類(キーの名前は必要に応じて任意でつけることができます。)が増えた時にその都度行を増やしていく必要があるため、冗長になってしまいます。

そこで今回はeach文を使用することで、その手間をなくした形です。
また部分テンプレート化することで、バリデーションエラーメッセージと同様に、他のviewでも使い回すことができます。

次にタグ一覧を表示する部分テンプレート

tags/_tag.html.erb
<div class="tags-index__tags--tag">
  <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
  <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
</div>

こちらも部分テンプレートを使わなければ後述するjQueryで4行分書く必要がり、jQueryでのDOM操作は極力シンプルに管理したいので部分テンプレート化しています。
ローカル変数のtagはindex.html.erbでインスタンス変数@tags(Tag.all)を指定することで、部分テンプレート内では個別のローカル変数tagとして自動で置き換わります。(index.html.erbのところで詳しく解説します。)
(<%= tag.menu_tag_ids.count %>)はタグ名の横にそのタグが使われているメニューの数を表示しています。(メニューテーブル(割愛) 1:多 メニュータグテーブル(中間テーブル)(割愛) 多:1 タグテーブル(今回))
<%= link_to 'X', tag_path(tag), method: :delete, remote: true %>を押すとdestroyアクションが発動します。remote: trueとすることで、ajax通信であることを明示します。

"remote: true"の仕組み
通常のリクエスト(リンク)ではHTMLを取得しますが、remote: true(html変換後はdata-remote="true"属性)が付与されたリクエストの場合はJSファイルのみを取得します。
Railsの場合はlayouts/application.html.erbの<head>タグ内にjsを読みこむタグがあるので、通常はHTMLを取得するとjsも自動で再読み込みがされます。なお、turbolinksを使っている場合はこの限りではありません。

3.タグの一覧画面、兼新規作成フォームを作成(views/tags/index.html.erb)

タグの一覧画面とその中に新規入力のフォームも作成していきます。

views/tags/index.html.erb
<div class="contents tags-index">
  <div class="tags-index--messages"><!--メッセージの表示エリア--></div>
  <h2>タグ一覧</h2>
  <div class="tags-index__list" id="tags-index--tag-list">
    <%= render @tags %>
  </div>
  <div class="tags-index__form">
    <%= form_with model: @tag, url: tags_path(@tag) do |f| %>
      <%= f.text_field :name %>
      <%= f.submit %>
    <% end %>
  </div>
</div>

タグ一覧の下に新規追加フォームがくっついてる状態です。

新規登録フォームはform_withを利用します。
form_withはデフォルトがremote: true属性を持っているので省略可能です。(form_forを使う場合はremote: true属性を明示する必要があります。)
form_withでajaxを使わない場合はlocal: true属性を明示する必要があります。
フォームヘルパーはremote: trueの場合はJSのみを、local: trueの場合はHTMLをアクションに伝えます。
また、フォームヘルパーはデフォルトのHTTPメソッドがPOSTでもあるのでmethod: :post省略可能です。

<%= render @tags %>のインスタンス変数 @tagsは後述するコントローラー側でTag.allを取得しています。
見慣れない部分テンプレートの書き方ですが、この略記法の部分テンプレートは変数名に対応する_変数名.html.erbが呼び出され、その部分テンプレート内ではローカル変数(今回でいう部分テンプレート内のtag)として使えるようになります。
今回の@tagsように配列で取得した場合は、部分テンプレート内ではその配列の数だけ、繰り返し表示がされます。
これは書き換えると以下のようになります。

views/tags/index.html.erb
<%= render partial: 'tag', locals: {tags: @tags} %>
views/tags/_tag.html.erb
<% tags.each do |tag| %>
  <div class="tags-index__tags--tag">
    <%= tag.name %>(<%= tag.menu_tag_ids.count %>)
    <%= link_to 'X', tag_path(tag), method: :delete, remote: true %>
  </div>
<% end %>

今回の記述方法はこれらを略記した形です。
Railsガイド - レイアウトとレンダリング

4.アクションごとのjs.erbを用意(views/tags/create.js.erb, views/tags/destroy.js.erb)

コントローラーから呼び出すjs(.erb)を作成します。
ここにjsを記述することで、コントローラーからcreate, destroyがそれぞれ呼び出された時に発火するjQueryでDOM操作が行われます。
ここで使われている.html()はそこに要素が何もなければ追加となり、すでに別の要素があれば書き換えとなります。

createアクション時のjs(.erb)

views/tags/create.js.erb
<% if @tag.errors.any? %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/error_messages', locals: {model: @tag}) %></p>")
<% else %>
  $(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
  $("#tags-index--tag-list").html("<%= j(render @tags) %>")
  $("#tag_name").val('')
<% end %>

<% if @tag.errors.any? %>はsubimitボタンが押された時に@tagにエラーが発生しているかどうかをチェックします。

"オブジェクト.errors.any?"と"オブジェクト.invalid?"の違い
今回の場合、コントローラー側で@tag.saveが試された後にcreate.js.erbを呼んでいるため@tagにはすでにerrorが発生しているかどうかがわかる状態でjs.erbに渡って来ています。
他方、invalid?メソッドは保存前のオブジェクトに対して手動でバリデーションチェックを行うものです。
通常はオブジェクトが保存されようとした時(.saveを試みた時)に自動でバリデーションチェックが実行され、今回は.saveの成否によってアクションを分けるため、改めて手動でinvalid?をするよりは、.saveの時点ですでにエラーが発生しているかどうか(.errors.any?)を確認した方がDRYに則っていると言えます。

エラーが発生している場合(空白やタグ名の重複)
・バリデーションエラーメッセージ(error_messages.html.erb)を$(".tags-index--messages")に追加または書き換え

エラーが発生していない場合(正常に保存ができた)は
・フラッシュメッセージ(flash_message.html.erb)を$(".tags-index--messages")に追加または書き換え
・タグ一覧($("#tags-index--tag-list"))を書き換え
・フォームに入力されたタグ名をクリア($("#tag_name").val(''))

が、実行されます。

destroyアクション時のjs(.erb)

views/tags/destroy.js.erb
$(".tags-index--messages").html("<p><%= j(render partial: 'layouts/flash_messages', locals: {model: @tag}) %></p>")
$("#tags-index--tag-list").html("<%= j(render @tags) %>")

create時と同じです。
・タグが削除されたらフラッシュメッセージの追加または書き換え
・タグ一覧の書き換え

が、実行されます。

"partial: "の必要性renderを使ってオプションの追加(今回でいうlocals: {tag: @tag})が必要な場合はファイル名(パス)の前にpartial:を明示する必要があります。
Railsガイド - レイアウトとレンダリング

5.tags_controller.rb(:index, :create, :destroy)の用意

コントローラーに今回必要なアクションを記載していきます

tags_controller.rb
class TagsController < ApplicationController

  def index
    @tags = Tag.all
    @tag = Tag.new
  end

  def create
    @tags = Tag.all
    @tag = Tag.new(tag_params)

    respond_to do |format|
      if @tag.save
        format.js { flash.now[:success] = "保存しました。" }
      else
        format.js
      end
    end
  end

  def destroy
    @tags = Tag.all
    @tag = Tag.new
    Tag.find(params[:id]).destroy
    flash.now[:warning] = "削除しました。"
  end

  private
  def tag_params
    params.require(:tag).permit(:name)
  end

end

今回はcreateのform_with、destroyのlink_to、それぞれでremote: true(form_withの場合はデフォルトなので省略)を明示しているため、どちらもJSのみをリクエストしていることになります。

tags_controller.rb
def create

  ...

  respond_to do |format|
    if @tag.save
      format.js { flash.now[:success] = "保存しました。" }
    else
      format.js
    end
  end
end

respond_to do |format|はリクエストの形式によって処理を分けるブロックパラメータです。
今回、タグの追加・削除についてはHTMLで要求が来る(local: true)ことは想定していませんのでformat.js {処理}のみを記載しています。
HTMLでの要求が来た時に処理を分けたい場合はformat.html {処理}を書けば、リクエストの方式に応じで処理を分けることができます。
if @tag.saveが成功した場合はflash[:success]キーに"保存しました。"というメッセージを載せてcreate.js.erbに渡します。
失敗した場合はバリデーションエラーメッセージのみを表示するため、フラッシュメッセージは使いませんので、format.jsの後ろに処理は書かず、表示のみを行います。
前述しましたが、この時にバリデーションエラーが発生している@tagをcreate.js.erbに渡して、そこでエラー処理(バリデーションエラーメッセージの表示)をしています。

destroyアクションではflash[:warning]キーに"削除しました。"というメッセージを載せてdestroy.js.erbに渡します。

"format.js"を書く場合と書かない場合
通常、コントローラーにはviewファイルに対応する同名のアクション名を作ります。(def index end ならindex.html.erb)
アクション名の中身で特にrenderredirect_toなどのに指定がなければアクションの最後にrender :アクション名が省略されて(暗示的に)実行されています。
(def index endなど、中身を書かなくてもviewがレンダリングされるのはこのためです。)
今回、createアクションでは、条件によってフラッシュメッセージを渡したいので明示的にformat.jsを記述し、その中で処理を分けていましたが、destroyアクションでは今のところ処理を分ける必要はないので、respond_toformat.jsrender :destroyは記述していません。
render :destroyに関してはすでに暗示的に記述されているため、書く必要がないということです。最初に書きましたが、暗示的にrender :アクション名が発動するのはアクションの中で他にrender :別のアクション名redirect_toがない場合に限るので、他方createアクションのように明示的にformat.jsを記述し実行された場合、アクションの最後に暗示的なrender :createが実行されることはありません。(DoubleRenderingErrorにならない理由です。)

Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

まとめ

質問や解釈の違い、記述方法に違和感ありましたら、コメント等でご指摘いただけると幸いです。

最後まで読んでいただきありがとうございました。

参考サイト

Railsガイド - レイアウトとレンダリング
Railsガイド - Action Controller の概要
Pikawaka - resoond_to
Qiita - 【Rails】flashメッセージを使用して簡易メッセージを表示させる詳しい方法と解説

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Ubuntuでrails6環境構築時、bundle installで引っ掛かる

環境構築を終えて、bundle installするときに、色々引っ掛かる

OSはWSL1 Ubuntu(18.04)
こちらを参考に進めていきました。

(Ubuntu)Ruby on rails 6.0 環境構築

nokogiriが通らない

An error occurred while installing nokogiri (1.10.10), and Bundler
cannot continue.
Make sure that `gem install nokogiri -v '1.10.10' --source
'https://rubygems.org/'` succeeds before bundling.

In Gemfile:
  rails was resolved to 6.0.3.3, which depends on
    actioncable was resolved to 6.0.3.3, which depends on
      actionpack was resolved to 6.0.3.3, which depends on
        actionview was resolved to 6.0.3.3, which depends on
          rails-dom-testing was resolved to 2.0.3, which depends on
            nokogiri

bundle configを変更するといけるらしいので

bundle installをしている場合

gem install nokogiri -- --use-system-libraries

bundle install --path vendor/bundleをしている場合
アプリの.bundleの中のconfigに

.bundle
BUNDLE_BUILD__NOKOGIRI: "--use-system-libraries"

を追記する。

bootsnapが通らない

An error occurred while installing bootsnap (1.4.8), and Bundler
cannot continue.
Make sure that `gem install bootsnap -v '1.4.8' --source
'https://rubygems.org/'` succeeds before bundling.

どうやらバージョンを下げるといいようで

Gemfile
gem 'bootsnap', '<= 1.1.0', require: false

これでやるといけた。

msgpackが通らない

An error occurred while installing msgpack (1.3.3), and Bundler
cannot continue.
Make sure that `gem install msgpack -v '1.3.3' --source
'https://rubygems.org/'` succeeds before bundling.

どうやら

Permission denied @ rb_file_s_rename.....

ということなのでアプリのvenfor/bundle内のgemsの権限をいじる

sudo chown -R username /home/username/testrails/railsapp/vendor/bundle/ruby/2.7.0/gems

パスは環境によって変わります。

Pumaが入らない

bundle/config
BUNDLE_BUILD__PUMA: "--with-cflags=-Wno-error=implicit-function-declaration"

を.bundle/configに書き込むか

$ bundle config build.puma --with-cflags="-Wno-error=implicit-function-declaration"

どちらかでいけるはず。

ほかに何か出てきたとき

権限関係の原因が多いと思うのでまず確認するといいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rack::Timeout でパスごとにタイムアウトを指定する

rack-timeout を使用しておりパスごとにタイムアウトの指定を変更するため以前(rails 5.2.3、 rack-timeout 0.5.1 まで)は以下のように指定していました(多少簡略化してあります)。

Gemfile
gem 'rack-timeout', require: 'rack/timeout/base'
config/initializers/rack_timeout.rb
module Rack
  class DynamicTimeout < Rack::Timeout
    def call(env)
      # URL が /admin から始まる場合は 30 秒、それ以外は 10 秒となるように
      @service_timeout = env['REQUEST_URI'].start_with?('/admin') ? 30 : 10

      super(env)
    end
  end
end

timeout_params = {
  service_timeout: 10,
  wait_timeout: 5.minutes.to_i,
  wait_overtime: 5.minutes.to_i,
  service_past_wait: false,
}

Rails.application.config.middleware.insert_before Rack::Runtime, Rack::DynamicTimeout, timeout_params

ところが Rails 6、あるいは rack-timeout 0.6.0 からタイムアウトが動的に指定した service_timeout の値よりも短くなっていました。
以下のようにログに出力されている値(30秒)よりも実際は短かった(10秒)です。

severity:ERROR  message:source=rack-timeout id=0284947e-c24c-4024-a9ba-c542b3eb58f9 timeout=30000ms service=10000ms state=timed_out

色々ログを仕込んだりして原因を調べたところ、どうやら Rack::DynamicTimeoutcall とは別に Rack::Timeout 自体の call が呼ばれているようでした。

都合2回 call メソッドが実行されて最初は Rack::DynamicTimeout で次が Rack::Timeout になっており、最終的にタイムアウトの値は Rack::Timeout で指定された値が使われるという状態です。
どこで呼ばれているか、それを変更できるかというところまで調べていくとちょっと時間がかかりそうだったので、今回は Rack::Timeout を上書きするパッチで対応することにしました。

Gemfile
gem 'rack-timeout'
config/initializers/rack_timeout.rb
module Rack
  module DynamicTimeout
    def call(env)
      # URL が /admin から始まる場合は 30 秒、それ以外は 10 秒となるように
      @service_timeout = env['REQUEST_URI'].start_with?('/admin') ? 30 : 10

      super(env)
    end
  end
end

Rack::Timeout.prepend Rack::DynamicTimeout

Module#prepend でモジュールを差し込んで Rack::Timeoutcall が呼ばれても対応できるようにしました。
ひとまずこれで。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルーティングのネストによって起きていること

動作環境
Ruby 2.6.5
Rails 6.0.3.2

ルーティングのネストによって何が起きているのかがわかりづらかったので、自分の確認を兼ねて投稿してみました。

ルーティングのネストをした場合

routes.rb
Rails.application.routes.draw do
  resources :hoges do
    resources :fugas, only: [:index]
  end
end

上記のコードでは、hogesに対してfugasがネストされており、ターミナル上でrails routesを実行すると、以下のようになります。

Prefix ➡︎ hoge_fugas
Verb ➡︎ GET
URI Pattern ➡︎ /hoges/:hoge_id/fugas(.:format)
Controller#Action ➡︎ fugas#index

もちろん、上記以外も表示されますが今回はネストの箇所のみを扱います。
これだけだといまいちわかりづらいので、ルーティングのネストをしていない場合と比較していきます。

ルーティングのネストをしない場合

routes.rb
Rails.application.routes.draw do
  resources :hoges
  resources :fugas, only: [:index]
end

上記のコードでは先ほどのコードと違い、ネストされていません。では先ほどと同様にターミナル上でrails routesを実行して、同じ箇所を確認してみましょう。

Prefix ➡︎ fugas
Verb ➡︎ GET
URI Pattern ➡︎ /fugas(.:format)
Controller#Action ➡︎ fugas#index

先ほどと比べてPrefixとURI Patternが異なっていることがわかります。
PrefixはURI Patternを簡単に書いたものなので、URI Patternのみに注目していきます。

ルーティングのネストをした場合としない場合の違い

ネストした場合
URI Pattern ➡︎ /hoges/:hoge_id/fugas(.:format)
ネストしない場合
URI Pattern ➡︎ /fugas(.:format)

つまり、ルーティングのネストとはURI Patternを変更するために行っているものと言えると思います。しかし、この言い方だとわかりづらいと思うので、もう少し詳しく説明します。

ネストすることによってURI Patternが変更したためにfugasコントローラーのindexアクションを作動するには、hoge_idを指定する必要が出てきます。
そのため、paramsの中にhoge_idを入れることができるようになります。
つまり、先ほどはルーティングのネストをURI Patternを変更するためのものと言いましたが、paramsの中にネストする側(今回はhoge)のid情報を入れるためのものと言い換えることができます。

paramsにネストする側(今回はhoge)のid情報を入れることにより、ネストされた側(今回はfuga)のコントローラー内でネストする側の情報を扱うことができるということになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsコマンドが正常に操作しない時疑うべきはspring

動機

Railsを用いてテストコードを作成中に以下のコマンドを打つと正常に動作しなくなったので備忘録としてどのように解決したかを記述

$ rails g rspec:request article


Running via Spring preloader in process 15349
Deprecation warning: Expected boolean default value for '--orm'; got :active_record (string).
This will be rejected in the future unless you explicitly pass the options `check_default_type: false` or call `allow_incompatible_default_type!` in your code
You can silence deprecations warning by setting the environment variable THOR_SILENCE_DEPRECATION.
Could not find generator 'rspec:request'. Maybe you meant "rspec:decorator"?
Run `rails generate --help` for more options.

springとは?

ザックリですが
「開発をよりスピードアップさせるため開発しているアプリケーションをバックグラウンドで保持し、「テスト・rakeタスク・マイグレーション」を毎回起動しなくてもいいようにしてくれるRailsアプリケーションのプリローダー」

といいやつに見えて、ここのプロセスが残っていたりするのが問題の可能性があります。

解決手順

まずはspringのstatusを確認

$ spring status


Spring is running:

 3729 spring server | blogapp | started 17 hours ago

やはり止まっていました。

次にspringの停止を行います。

$ spring stop
Spring stopped.

再度状態確認

$ spring status
Spring is not running.

再度Railsコマンド実行

$ rails g rspec:request article
Running via Spring preloader in process 15565
      create  spec/requests/articles_spec.rb

成功しました!

再度コマンド操作を行うとspringも実行されますのでご心配なく

身に覚えの無いエラーが起きた際はspringを疑ってみましょう

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Rails]カスタムフォントの設定

はじめに

アプリ開発でフリーフォントを使用してデザインを変えたのでその使用方法をまとめました。

目次

1 フリーフォントの準備
2 フォントの読み込み

1 フリーフォントの準備

まず、フリーフォントサイトから自分が使用したいものをダウンロードします。
その後、ダウンロードしたファイル(拡張子ttfまたはotf)をapp/assets/fonts下に配置します。

注意事項

フォントはそれぞれ利用条件が異なります。商用可能であっても利用できる範囲に制限があったり、使用時に報告が必要なものもありますので、ご利用の際には必ず配布ページをご確認ください。

2 フォントの読み込み

CSSファイルに以下を記述します。

@font-face {
  font-family: abcdefg;
  src: url('/assets/abcdefg.ttf') format("truetype");
  font-weight: normal;
  font-style: normal;
}

フォントを適用したい箇所にfont-familyを指定します。

p {
  font-family; “abcdefg”;
}

これでフォントを適用することができました。

3 本番環境への適応

このままでは本番環境でフォントが適用されなかったため、以下を変更しました。

/config/production.rb
# 変更前
config.assets.compile = false
# 変更後
config.assets.compile = true

これにより本番環境で読み込まれていなかった画像ファイルも表示することができました。

参考リンク

https://qiita.com/dir_sh0606/items/ce605b70f1cb333f9d59
https://qiita.com/kinpin/items/bd7c9a7d7a739e297742

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[初学者向け]削除機能の実装方法

投稿内容

今回は削除機能の実装方法について投稿します。
以下のようなボタンを押した際にdestroyアクションを発動させます。
image.png

コントローラーのアクション

items_controller.rb
class ItemsController < ApplicationController

#省略

  def destroy
    if @item.destroy
      redirect_to root_path
    else
      render :show
    end
  end
end

解説) destroyアクションが呼ばれたらインスタンス変数@itemを削除します。無事削除できた場合はroot_pathへ遷移。削除できなかった場合はshowアクションへ遷移するようハンドリングします。

Rubyの記述

show.html.haml
.item-show-page__destroy-btn
  = link_to "商品を削除する", item_path(@item), method: :delete, class: "item-show-page__destroy-link"

解説) link_to@itemを指定し、メソッドはdeleteを指定します。

これでボタンをクリックすると削除できると思います。

最後に

今回の削除機能は至ってシンプルなものなので、初学者の方でもとっつきやすいかと思われます。是非参考にしてください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Ruby】メソッドジャンプをしたい!

はじめに

環境macOS Catalina10.15.5

Rubyで他の人が書いたソースコードを読むときにメソッドの定義場所がすぐわかるようにメソッドジャンプがしたいなと思い、いろいろ試してたどり着いた方法を書き記します

私はテキストエディタはAtomを主に使っていたのですが、RubyのコードをAtomでメソッドジャンプするのは、いろいろ試しましたがうまくいきませんでした、なので

結論 VSCodeを使うのが簡単

という事になりまりました

導入手順

1.こちらでVSCodeをダウンロード

https://azure.microsoft.com/ja-jp/products/visual-studio-code/

2.日本語化

  • メニューバーからviewを選択
  • command palette を選択
  • configure display languageを選択
  • install additional languageを選択
  • Japanese Language Pack for Visual Studio Codeのパッケージを探す
  • もう一度configure display languageを選択
  • jaを選択
  • VSCode再起動

3.パッケージのRubyをインストール

拡張機能の検索ボックスで検索できます

4.ジャンプの機能を有効化

インストールしたRubyの拡張機能の設定を開いて
Ruby:Intellisenseのところをfalseからrubylocateに変更

5.メソッドジャンプのやりかた

⌘を押しながらジャンプしたいメソッドをクリックすると定義場所にとべます!

ここまで15分程度だったのでとても簡単でした!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更ながら graphql-code-generator の便利さを痛感する その2

はじめに

今日も今日とて、フロントReact + バックRailsの...

ということで、今回は今更ながら graphql-code-generator の便利さを痛感するの続きとして、登録、削除等のMutationの部分をやっていきます。

※構成は前回と同じです。

とりあえずApolloの公式に沿ってやってみる

まずはTODOを追加する仕組みを作っていきます。

追加用のテキストフィールドを用意しました。
ここに値を入力して、エンターで追加処理が実行される仕組みとします。

スクリーンショット 2020-09-22 12.44.46.png

src/App.tsx
+import { gql, useMutation } from "@apollo/client";

...

+const ADD_TODO = gql`
+  mutation addTodo($name: String!) {
+    addTodo(input: { name: $name }) {
+      todo {
+        id
+        name
+      }
+    }
+  }
+`;

 const App = () => {
   const { loading, data } = useTodosQuery();
+  const [addTodo] = useMutation(ADD_TODO);

...
         </p>
+        <input
+          type="text"
+          onKeyPress={(e) => {
+            if (e.key === "Enter") {
+              addTodo({ variables: { name: e.currentTarget.value } });
+            }
+          }}
+        />
         {loading ? (
...

文字を入力して、エンターで追加処理が実行できる仕組みができました。

エンター押下後、画面をリロードすると、TODOが追加されていることがわかります。

スクリーンショット 2020-09-22 14.18.50.png

リロードしないと動きがわからないので、ついでに、追加処理に加え、入力欄のクリア、一覧情報の再取得処理をやっておきます。

src/App.tsx
...
-  const { loading, data } = useTodosQuery();
+  const { loading, data, refetch } = useTodosQuery();
-  const [addTodo] = useMutation(ADD_TODO);
+  const [addTodo] = useMutation(ADD_TODO, {
+    update() {
+      refetch();
+    },
+  });

...

           onKeyPress={(e) => {
             if (e.key === "Enter") {
               addTodo({ variables: { name: e.currentTarget.value } });
+              e.currentTarget.value = "";
             }
           }}

特に型関連でエラーは起きませんでした。

もう少し便利になるよう作り込んでみる

何も入力せずに、登録処理を実行し、エラーが出るような仕組みを入れます。

Todoにバリデーションエラーのフィールドを追加して、以下のような値がレスポンスに含まれるようにしました。

"errors":[{"field":"name","error":"blank"}]

また、処理の結果が正常に行われたかのresultフィールド(Boolean)を追加しました。

resulttrueなら一覧をリロード、falseならエラー情報をalertで表示する仕組みを作ってみます。

src/App.tsx
 const ADD_TODO = gql`
   mutation addTodo($name: String!) {
     addTodo(input: { name: $name }) {
       todo {
         id
         name
+        errors {
+          field
+          error
+        }
      }
+      result
    }
  }
`;
src/App.tsx
   const [addTodo] = useMutation(ADD_TODO, {
-    update() {
-      refetch();
-    },
-  });
+    update(
+      _cache,
+      {
+        data: {
+          addTodo: {
+            todo: { errors },
+            result,
+          },
+        },
+      }
+    ) {
+      if (result) {
+        refetch();
+      } else {
+        errors.forEach(({ field, error }) => {
+          alert(`${field} ${error}`);
+        });
+      }
+    },
+  });

errorsの型が宣言されていないので、エラーが発生しました。

スクリーンショット 2020-09-22 16.10.28.png

型宣言をしてあげます。

type ValidationErrorType = {
  field: string;
  error: string;
};
-errors.forEach(({ field, error }) => {
+errors.forEach(({ field, error }: ValidationErrorType) => {
  alert(`${field} ${error}`);
});

エラーは解消され、バリデーションエラーメッセージがalertで確認できるようになりました。

スクリーンショット 2020-09-22 16.15.46.png

前回同様、graphql-code-generatorを使って、綺麗にしていこうと思います。

graphql-code-generatorを使う

まずは設定から

queriesとは別にmutations用のディレクトリを作成して、今回作成したクエリは、そちらに格納します。

codegen.yml
-documents: ./graphql/queries/*.graphql
+documents:
+  - ./graphql/mutations/*.graphql
+  - ./graphql/queries/*.graphql
graphql/mutations/add_todo.graphql
mutation addTodo($name: String!) {
  addTodo(input: { name: $name }) {
    todo {
      id
      name
      errors {
        field
        error
      }
    }
    result
  }
}

これでyarn generateを実行すると、src/types.d.tsにTODOを追加する用のuseAddTodoMutationが用意されました。

useAddTodoMutationを使って追加処理を書き直してみます。

-import { useTodosQuery } from "./types.d";
+import { useTodosQuery, useAddTodoMutation } from "./types.d";

+const [addTodo] = useMutation(ADD_TODO, {
+const [addTodo] = useAddTodoMutation({

ん?エラーが起きました。

スクリーンショット 2020-09-22 21.01.17.png

どうやらdatanullundefinedの可能性があるそうです。

それらを考慮して、少し書き直します。

const [addTodo] = useAddTodoMutation({
  update(_cache, { data }) {
    const result = data?.addTodo?.result || false;
    const errors = data?.addTodo?.todo.errors || [];

    if (result) {
      refetch();
    } else {
      errors.forEach((e) => {
        if (e) alert(`${e.field} ${e.error}`);
      });
    }
  },
});

少し手直しが必要でしたが、クエリと、型宣言の用意が不要になったので、ソース的にはスッキリしました :thumbsup:

同様に削除処理を実装してみる

雑ですが、各TODOの隣に、削除ボタンを用意して、削除処理を実行できるようにします。

スクリーンショット 2020-09-27 23.50.47.png

graphql/mutations/del_todo.graphql
mutation delTodo($id: ID!) {
  delTodo(input: { id: $id }) {
    todo {
      id
    }
  }
}

削除対象のTODOを特定するために、IDを取得する必要があります。

graphql/queries/todos.graphqlでは、TODOのnameだけ取得しているので、idもとるように修正します。

graphql/queries/todos.graphql
 query todos {
   todos {
+    id
     name
   }
 }

yarn generateを実行するとuseDelTodoMutation関数がsrc/types.d.tsに生まれました。

このuseDelTodoMutationを使って、削除ボタンクリック時に削除処理が実行できるようにします。

src/App.tsx
-import { useTodosQuery, useAddTodoMutation } from "./types.d";
+import { useTodosQuery, useAddTodoMutation, useDelTodoMutation } from "./types.d";


+const [delTodo] = useDelTodoMutation({
+  update() {
+    refetch();
+  }
+});


-{data && data.todos.map(({ name }, i) => <li key={i}>{name}</li>)}
+{data && data.todos.map(({ id, name }, i) => <li key={i}>{name}<button onClick={() => delTodo({ variables: { id } })}>削除</button></li>)}

お手軽に削除処理が実装できました :tada:

add-del.gif

最後に

一つの画面、コンポーネントで、一覧取得、追加処理、削除処理を実装したので、少しボリュームの大きめなファイルとなってしまったかもしれません。

別のファイルに型定義やクエリ情報を定義して、importでも良いかもしれないですが、人力で且つ、複数人となると、管理が次第に煩雑になったりする可能性があると思います。

これが、ある意味graphql-code-generatorのルールに則って開発をしていると、その問題が解消されるのではないかと思いました。

今回は、かなり小規模な例として、実装したので、導入にあたって障害となる箇所をあまり感じられませんでした。
LGTM なツールと思います :thumbsup:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む