patorashのブログ

方向性はまだない

gem buoysを使ってパンくずリストを作る

今ちょっと新しいRailsアプリを作っていて、そこでパンくずリストが欲しいなぁ〜と思ったので社内のチャットで「パンくずリストを作るためのgemのデファクトスタンダートってやっぱりgretelですか?」と聞いたところ、I18n対応できるbuoysというgemがあるのでそちらのほうがいいんじゃない?と言われたのでそちらを使ってみることにしました。

buoysはgretelにインスパイアされてるみたいなので、ほぼ同じに見えました。

buoyはググってみると、ブイ(海とかに浮いてる目印になるやつ)のようです。なるほど〜。

github.com

こっちはgretel。

github.com

セットアップ

インストール

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自体も非常に便利ですが、この関数によってパンくずリストの定義がすこぶる簡単になったのでよかったです👍