環境
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するのはよくないですが、とりあえずはこれでヨシとします。