環境
- Rails 6.1.3.2
- Tailwind 2.2.4
実装するもの
こんな感じのフォームです。
アイデアはQiitaから拝借しました。
アソシエーションと各カラムの値
アソシエーション
User -< SocialAccount >- SocialService
各カラム
# Table name: social_accounts
#
# id :bigint not null, primary key
# identifier :text(65535) not null
# created_at :datetime not null
# updated_at :datetime not null
# social_service_id :bigint not null
# user_id :bigint not null
# Table name: social_services
#
# id :bigint not null, primary key
# position :integer not null
# name :string not null
# base_url :text(65535) not null
# created_at :datetime not null
# updated_at :datetime not null
SocialServiceの値
SocialServiceは、GitHub
やTwitter
、ブログ
になります。事前に管理側で値を入れておくものなので、seedなどを利用する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
[1] pry(main)> SocialService.all
=> [#<SocialService:0x00007fd040925320
id: 1,
position: 1000,
name: "その他リンク",
base_url: "",
created_at: Wed, 23 Jun 2021 02:10:43.476867000 JST +09:00,
updated_at: Wed, 23 Jun 2021 02:11:18.593401000 JST +09:00>,
#<SocialService:0x00007fd0408d7530
id: 2,
position: 999,
name: "ブログ",
base_url: "",
created_at: Wed, 23 Jun 2021 02:10:43.479999000 JST +09:00,
updated_at: Wed, 23 Jun 2021 02:11:18.995271000 JST +09:00>,
#<SocialService:0x00007fd0408d6d38
id: 3,
position: 1,
name: "GitHub",
base_url: "https://github.com/",
created_at: Wed, 23 Jun 2021 02:10:43.483004000 JST +09:00,
updated_at: Wed, 23 Jun 2021 02:11:19.431160000 JST +09:00>,
#<SocialService:0x00007fd0408d69f0
id: 4,
position: 2,
name: "Twitter",
base_url: "https://twitter.com/",
created_at: Wed, 23 Jun 2021 02:10:43.486446000 JST +09:00,
updated_at: Wed, 23 Jun 2021 02:11:20.160150000 JST +09:00>]
|
そして、それぞれのアカウントのURLはbase_url
と各サービスのIDを合わせたものなので、以下のメソッドを定義する。
base_url
がないアカウントも、identifier
にURLを入れるようにする。
1
2
3
4
5
6
7
8
|
class SocialAccount < ApplicationRecord
belongs_to :user
belongs_to :social_service
def url
"#{social_service.base_url}#{identifier}"
end
end
|
アカウントの編集ページ
ルーティング
設定のページをまとめるためにnamespaceを切っている。
ここはある程度自由でいいので参考までに。
1
2
3
4
5
|
Rails.application.routes.draw do
namespace :settings do
resources :social_accounts, only: %i[index]
end
end
|
コントローラ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
module Settings
class SocialAccountsController < ApplicationController
def index
social_accounts_hash = current_user.social_accounts.index_by(&:social_service_id)
@social_accounts = all_social_accounts(current_user, social_accounts_hash)
end
private
def all_social_accounts(user, social_accounts_hash)
SocialService.all.map do |social_service|
# social_accountレコードがあればそれ、なければnewする
social_accounts_hash[social_service.id] || user.social_accounts.new(social_service_id: social_service.id)
end
end
end
end
|
find_or_initialize_by
を使えば以下のようにシンプルに書けるが、find_or_initialize_by
のたびにクエリを発行するのでパフォーマンスが悪くなる。
そのため、current_user.social_accounts.index_by(&:social_service_id)
で一度にレコードを取得して、そのハッシュを使って配列を作成している。
これは、【Rails】index_byとgroup_byを用いて取り回しのきくハッシュを作成するに詳しく書いた。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
module Settings
class SocialAccountsController < ApplicationController
def index
@social_accounts = all_social_accounts(current_user)
end
private
def all_social_accounts(user)
SocialService.asc.map do |social_service|
SocialAccount.find_or_initialize_by(social_service_id: social_service.id, user_id: user.id)
end
end
end
end
|
@social_accounts
の中身は以下のような、実際のレコードのインスタンスとinitializeされたインスタンスが混ざった配列になる。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
[1] pry(#<Settings::SocialAccountsController>)> @social_accounts
=> [#<SocialAccount:0x00007fa00abe2330
id: nil,
social_service_id: 1,
user_id: 22,
identifier: nil,
created_at: nil,
updated_at: nil>,
#<SocialAccount:0x00007fa00b53ca70
id: 1,
social_service_id: 2,
user_id: 22,
identifier: "https://blog.aiandrox.com",
created_at: Thu, 24 Jun 2021 08:46:55.906186000 JST +09:00,
updated_at: Thu, 24 Jun 2021 08:46:55.906186000 JST +09:00>,
#<SocialAccount:0x00007fa00b52d5c0
id: 3,
social_service_id: 3,
user_id: 22,
identifier: "aiandrox",
created_at: Thu, 24 Jun 2021 09:31:05.522428000 JST +09:00,
updated_at: Thu, 24 Jun 2021 09:43:30.552457000 JST +09:00>,
#<SocialAccount:0x00007fa00abe1d90
id: nil,
social_service_id: 4,
user_id: 22,
identifier: nil,
created_at: nil,
updated_at: nil>]
|
ビュー
フォームは、クラスを除いてシンプルにするとこういう構造になる。
1
2
3
4
5
6
7
8
9
10
11
|
= form_with model: current_user, url: settings_social_accounts_path, method: :put do |f|
- @social_accounts.each do |social_account|
= f.fields_for 'social_accounts_attributes[]', SocialAccount.new do |ff|
= ff.hidden_field :social_service_id, value: social_account.social_service.id
= ff.label :service_name, "#{social_account.social_service.name} URL"
br
- if social_account.social_service.base_url.present?
= social_account.social_service.base_url
= ff.text_field :identifier, value: social_account.identifier
br
= f.submit
|
① f.fields_for ‘social_accounts_attributes[]’, SocialAccount.new do |ff|
第一引数は、【Rails】ネストされたattributes_paramsをフォームからコントローラに渡すと同じような感じ。
第二引数はsocial_account
ではなく、SocialAccount.new
にしないといけない。
fields_forでは、新規レコードのときは
1
2
3
4
|
'social_accounts_attributes' => [{
'social_service_id' => '2',
'identifier' => 'https://blog.aiandrox.com'
}]
|
こうなるが、既存レコードのときは、IDがキーとして振られるようになる。
1
2
3
4
5
6
|
'social_accounts_attributes' => [{
'1' => {
'social_service_id' => '2',
'identifier' => 'https://blog.aiandrox.com'
},
}]
|
そのため、どちらの場合も同じ構造になるように、SocialAccount.new
にする。
参考:Action View フォームヘルパー - Railsガイド
これにTailwindのクラスを当てると、以下のようになる。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
= form_with model: current_user, url: settings_social_accounts_path, method: :put, class: 'w-full max-w-lg text-sm' do |f|
- @social_accounts.each do |social_account|
= f.fields_for 'social_accounts_attributes[]', SocialAccount.new do |ff|
= ff.hidden_field :social_service_id, value: social_account.social_service.id
.flex.flex-wrap.mb-6
= ff.label :service_name, "#{social_account.social_service.name} URL", class: 'block tracking-wide text-gray-700 text-xs font-bold mb-2'
div class="flex flex-row rounded overflow-hidden w-full border border-gray-300"
- if social_account.social_service.base_url.present?
div class='appearance-none block bg-gray-300 text-gray-800 py-3 px-4 leading-tight'
= social_account.social_service.base_url
= ff.text_field :identifier, value: social_account.identifier, class: 'appearance-none block w-full bg-gray-100 text-gray-700 py-3 px-4 leading-tight focus:outline-none focus:bg-white focus:border-gray-500'
.form-group
= f.submit class: 'inline-block cursor-pointer no-underline text-sm py-3 px-4 mt-4 leading-none rounded lg:mt-0 hover:border-transparent border border-gray-300'
|
これでフォームの見た目はできた。
Tailwindの参考:Simple rounded text form by perrogrun | Forms, Inputs
更新アクション
ルーティング
social_accounts_controller
にupsert_all
アクションを追加する。
このupsert_all
の定義の仕方については、【Rails】複数レコードを一括操作するルーティングに詳しく書いた。
1
2
3
4
5
6
7
8
9
|
Rails.application.routes.draw do
namespace :settings do
resources :social_accounts, only: %i[index] do
collection do
put '', action: :upsert_all
end
end
end
end
|
コントローラ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
module Settings
class SocialAccountsController < ApplicationController
# indexは省略
def upsert_all
social_accounts_params.each do |attribute|
SocialAccount.create_or_update_or_destroy(
user_id: current_user.id,
social_service_id: attribute[:social_service_id],
identifier: attribute[:identifier],
)
end
redirect_to settings_social_accounts_path, notice: '外部サービスの情報を更新しました!'
end
private
# all_social_accountsは省略
def social_accounts_params
params.require(:user)
.permit(social_accounts_attributes: [:social_service_id, :identifier])
.dig(:social_accounts_attributes)
end
end
end
|
SocialAccount
にクラスメソッドを追加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class SocialAccount < ApplicationRecord
# 省略
validates :identifier, presence: true
def self.create_or_update_or_destroy(user_id:, social_service_id:, identifier:)
social_account = find_or_initialize_by(
user_id: user_id,
social_service_id: social_service_id,
)
# 更新に失敗 = identifierが空なので、レコードを削除する
social_account.update(identifier: identifier) || social_account.destroy!
end
end
|
本当はupdateの成功可否でdestroyするのはよくないですが、とりあえずはこれでヨシとします。