今ちょっと新しいRailsアプリを作っていて、そこでパンくずリストが欲しいなぁ〜と思ったので社内のチャットで「パンくずリストを作るためのgemのデファクトスタンダートってやっぱりgretelですか?」と聞いたところ、I18n対応できるbuoysというgemがあるのでそちらのほうがいいんじゃない?と言われたのでそちらを使ってみることにしました。
buoysはgretelにインスパイアされてるみたいなので、ほぼ同じに見えました。
buoyはググってみると、ブイ(海とかに浮いてる目印になるやつ)のようです。なるほど〜。
こっちはgretel。
セットアップ
インストール
Gemfileに追加します。
gem 'buoys'
そして、bundle install
を実行します。
ファイル生成
generateコマンドがあるのでそれを使います。--template slim
とすることで、slimに対応したテンプレートが作られます。hamlもあるとか。
$ bin/rails g buoys:install --template slim
create config/locale/buoys.en.yml
create config/buoys/breadcrumbs.rb
create app/views/breadcrumbs/_buoys.html.slim
設定する
app/views/layout/appication.html.slimに追加
出力するパンくずリストをrenderしときます。
= render partial: 'breadcrumbs/buoys'
Bootstrap4に対応する
デフォルトだとBootstrap4に対応していないので、classを追加するなどしておきます。
li
タグに.breadcrumb-item
を追加しただけだったような気がする…。
- if buoys.any? ol.breadcrumb itemscope=true itemtype='http://schema.org/BreadcrumbList' - buoys.each.with_index(1) do |link, i| li.breadcrumb-item itemprop='itemListElement' itemscope=true itemtype='http://schema.org/ListItem' - # if `link.current?` is true, link.options includes {class: 'current'}. - if link.current? span itemprop='name' = link.text meta itemprop='position' content=i - else = link_to link.url, link.options.merge(itemprop: :item) do span itemprop='name' =link.text meta itemprop='position' content=i
パンくずリストを定義する
config/buoys/breadcrumbs.rb
を修正します。
直接文字をハードコードする場合は、以下のようにします。
buoy :stories do link 'Stories', stories_path end
I18n対応する場合は、symbolを渡します。
buoy :stories do link :stories, stories_path # same as `link I18n.t('stories', scope: 'buoys.breadcrumbs', default: 'stories'), story_path` end
モデルのインスタンスを渡しておくこともできます。また、pre_buoy
を指定することで、上位のパンくずを指定できます。
buoy :story do |story| link story.title, story_path(story) pre_buoy :stories end
Localeファイルを定義する
config/locale/buoys.ja.yml
を作ります。
ja: buoys: breadcrumbs: stories: 物語一覧
パンくずリストを表示させる
app/views/stories/index.html.slim
に、以下を設定します。
ruby: buoy :stories
これで、http://localhost:3000/stories
にアクセスすると、パンくずリストに物語一覧
と表示されます。
次に、app/views/stories/show.html.slim
に、以下を設定します。
ruby: buoy :story, @story # @story.idは1, @story.titleは指輪物語とする
これで、http://localhost:3000/stories/1
にアクセスすると、パンくずリストに物語一覧 / 指輪物語
と表示されます。
新規作成、編集にもパンくずリストをつける
さらに対応していこうと思います。
config/locale/buoys.ja.yml
を編集します。
ja: buoys: breadcrumbs: stories: 物語一覧 new: 新規作成 edit: 編集
config/buoys/breadcrumbs.rb
を修正します。
buoy :stories do link :stories, stories_path end buoy :story do |story| link story.title, story_path(story) pre_buoy :stories end buoy :new_story do link :new, new_story_path pre_buoy :stories end buoy :edit_story do |story| link :edit, edit_story_path(story) pre_buoy :story, story end
app/views/stories/new.html.slim
に、以下を設定します。
ruby: buoy :new_story
そして、app/views/stories/edit.html.slim
に、以下を設定します。
ruby: buoy :edit_story, @story
これで、新規作成のときは物語一覧 / 新規作成
と表示され、編集のときは、物語一覧 / 指輪物語 / 編集
と表示されるようになりました。やったね👍
パンくずリストの定義を自動生成する
そうはいってもモデルの数だけパンくずリストを定義していくの面倒過ぎます😩 そこで、メタプログラミングすることにしました。
config/buoys/breadcrumbs.rb
に関数define_buoy
を定義しました。ガンガンeval
使ってます!(evalは自己責任で…)
一応、nested_resourcesやnamespaceやデフォルトのbuoyにも対応しています。
ただし、nested_resourcesには1階層のみ。複数階層も対応できるとは思いますが、考慮しないといけないことが増えすぎるのでここまでにしてます。
def define_buoy(single_name, parent: nil, title_method:, namespace: nil, default_buoy: nil) single_name = "#{parent}_#{single_name}" unless parent.nil? single_path = "#{single_name}_path" single_path = "#{namespace}_#{single_path}" unless namespace.nil? plural_name = single_name.pluralize plural_path = "#{plural_name}_path" plural_path = "#{namespace}_#{plural_path}" unless namespace.nil? if parent.nil? buoy plural_name.to_sym do link plural_name.to_sym, eval(plural_path) pre_buoy default_buoy unless default_buoy.nil? end buoy single_name.to_sym do |record| link eval("record.#{title_method}"), eval("#{single_path}(record)") pre_buoy plural_name.to_sym end buoy "new_#{single_name}".to_sym do link :new, eval("new_#{single_path}") pre_buoy plural_name.to_sym end buoy "edit_#{single_name}".to_sym do |record| link :edit, eval("edit_#{single_path}(record)") pre_buoy single_name.to_sym, record end else buoy plural_name.to_sym do |parent_record| link plural_name.to_sym, eval("#{plural_path}(parent_record)") pre_buoy parent.to_sym, parent_record end buoy single_name.to_sym do |parent_record, record| link eval("record.#{title_method}"), eval("#{single_path}(parent_record, record)") pre_buoy plural_name.to_sym, parent_record end buoy "new_#{single_name}".to_sym do |parent_record| link :new, eval("new_#{single_path}(parent_record)") pre_buoy plural_name.to_sym, parent_record end buoy "edit_#{single_name}".to_sym do |parent_record, record| link :edit, eval("edit_#{single_path}(parent_record, record)") pre_buoy single_name.to_sym, parent_record, record end end end
この関数を使ってみましょう。
define_buoy 'story', title_method: :title
上記の関数は、以下の定義と同じです。
buoy :stories do link :stories, stories_path end buoy :story do |story| link story.title, story_path(story) pre_buoy :stories end buoy :new_story do link :new, new_story_path pre_buoy :stories end buoy :edit_story do |story| link :edit, edit_story_path(story) pre_buoy :story, story end
素晴らしい🎉🎉🎉
ネストしたリソースの場合
例えば、StoryにTagがネストしているとします。(/stories/1/tags
, /stories/1/tags/1
のように…)
こうします。
define_buoy 'tag',parent: 'story', title_method: :name
上記の関数は、以下の定義と同じです。
buoy :story_tags do |story| link :story_tags, story_tags_path(story) pre_buoy :story, story end buoy :story_tag do |story, tag| link tag.name, story_tag_path(story, tag) pre_buoy :story_tags, story end buoy :new_story_tag do |story| link :new, new_story_tag_path(story) pre_buoy :story_tags, story end buoy :edit_story_tag do |story, tag| link :edit, edit_story_tag_path(story, tag) pre_buoy :story_tag, story, tag end
そして、これに対応するLocaleの設定は、こう。
config/locale/buoys.ja.yml
を編集します。
ja: buoys: breadcrumbs: stories: 物語一覧 story_tags: タグ一覧 new: 新規作成 edit: 編集
これで、
- index…
物語一覧 / 指輪物語 / タグ一覧
- show…
物語一覧 / 指輪物語 / タグ一覧 / ファンタジー
- new…
物語一覧 / 指輪物語 / タグ一覧 / 新規作成
- edit…
物語一覧 / 指輪物語 / タグ一覧 / ファンタジー / 編集
のようにパンくずリストが作られます。
めちゃくちゃ捗る🚀🚀🚀
まとめ
buoy自体も非常に便利ですが、この関数によってパンくずリストの定義がすこぶる簡単になったのでよかったです👍