Migration
Each of these gems names its view helpers like the others, so an app switches in one go: there is no compatibility layer, and the old gem leaves the Gemfile when the migration lands.
From gretel
The shape stays the same, a crumb that names its parent, and view call sites barely change: breadcrumb :order, @order and breadcrumb @order work as they did. What moves is config/breadcrumbs.rb, split into one class per namespace, and a codemod writes that class.
An app switches in one go: there is no compatibility layer, and the two gems do not run side by side in production. gretel stays in the Gemfile of the migration branch only, for the comparison test below.
1. Convert the breadcrumbs file
bundle exec gretelx-convert config/breadcrumbs.rb --namespace Admin \
--output app/breadcrumbs/admin/breadcrumbs.rb --views app/views/admin
It writes Admin::Breadcrumbs and prints, with their line, what it left out or converted with a loss. Several files (config/breadcrumbs/*.rb) make one class, the later definition of a crumb winning as in gretel.
| gretel | GretelX |
|---|---|
crumb :key do |order| ... end | def key(order) ... end, a parameter tested for nil getting = nil |
link label, url | crumb label, url; link label or link label, nil give a level without a URL |
a second link | links: [link(label, url)] on the same level |
parent :other, order | parent: other(order) |
parent record | parent: crumb_for(record) |
parent :a and parent :b in an if/else | parent: condition ? a : b |
link and parent inside a final if, elsif, unless | one crumb call per branch |
t(".x"), current_user: view helpers | helpers.t(".x"), helpers.current_user; route helpers stay bare |
%i[a b].each { |name| crumb name do ... end }, {a: "A"}.each | one method per element |
It lists, without converting them: a loop over anything but a literal array or hash, link or parent anywhere else than at the top of the crumb or in its final conditional, link options (title:, class:), crumbs whose name is a Ruby keyword or an existing method (display, test), and any statement that is not a crumb. Comments are kept above the method they precede.
The codemod is built on Prism 1.2 or later: Ruby 3.4 ships it, and on Ruby 3.3 add gem "prism" to the development group.
2. Compare the trails
While gretel is still bundled, a test draws each trail with both gems and lists the differences in labels and URLs. gretel shows a crumb’s extra links as levels of their own: the comparison does the same with links.
require "gretelx/oracle"
class BreadcrumbsMigrationTest < ActionView::TestCase
test "GretelX draws the trails gretel drew" do
differences = GretelX::Oracle.compare(view, Admin::Breadcrumbs, [
[:orders], [:order, orders(:alice)], [orders(:alice)], [:returns], [:returns, customers(:alice)]
])
assert_empty differences, differences.join("\n")
end
end
3. Review the call sites
--views lists the breadcrumb calls that a derived trail may now cover: :generic_form (or :new, :edit) in new and edit views, breadcrumb @order in a show view, breadcrumb :orders in an index view. Nothing is deleted: a call goes once its view shows the same trail without it. Area prefixes in the keys (:admin_order) can go too, since the namespace does that job.
4. Remove gretel
Drop gretel and config/breadcrumbs.rb, and render breadcrumb_trail in the layout where breadcrumbs was; the guide has a loop. gretel’s rendering options (style: :bootstrap5, separator, link_current, semantic markup) become choices in that markup, and its structured_data becomes breadcrumb_json_ld.
Then add the breadcrumbs test: it catches the parents, route helpers and view calls the conversion got wrong, and the crumbs nothing reaches any more.
From loaf
loaf appends crumbs with breadcrumb: at controller class level (with only: and except:), inside actions, or in views. Its breadcrumb_trail yields each crumb, with name, url and current?, to a block, and decides current? by comparing the crumb’s URL with the request (the match: option).
GretelX uses the same two helper names, so loaf must go first. Its breadcrumb names one crumb instead of appending one, its breadcrumb_trail returns an array instead of taking a block, and the current level is simply the last.
loaf
class Admin::CategoriesController < Admin::BaseController
breadcrumb "Home", :admin_root_path
breadcrumb "Categories", :admin_categories_path
def show
@category = Category.find(params[:id])
@category.ancestors.each do |ancestor|
breadcrumb ancestor.name, admin_category_path(ancestor)
end
breadcrumb @category.name, admin_category_path(@category)
end
end
<% breadcrumb_trail do |crumb| %>
<li><%= link_to crumb.name, crumb.url %></li>
<% end %>
GretelX
module Admin
class Breadcrumbs < GretelX::Trail
def root
crumb "Home", admin_root_path
end
def categories
crumb "Categories", admin_categories_path
end
def category(category, ancestors = category.ancestors.to_a)
*grandparents, parent_category = ancestors
crumb category.name, admin_category_path(category),
parent: parent_category ? category(parent_category, grandparents) : categories
end
end
end
<% breadcrumb_trail.each do |level| %>
<li><%= link_to_unless level.current?, level.label, level.url %></li>
<% end %>
show finds the category crumb on its own: the view names nothing
Steps
- Replace
gem "loaf"withgem "gretelx". - Move the controllers’ class-level crumbs into each namespace’s trail class: "Home" becomes
root, a section becomes a crumb named after the controller. - Delete the crumbs appended in
index,show,newandedit, and turn the others into crumbs that name their parent. - In views, a run of
breadcrumb "Name", pathcalls becomes one crumb method, named withbreadcrumb :name. - In the layout,
breadcrumb_trail do |crumb|becomesbreadcrumb_trail.each do |level|, andcrumb.namebecomeslevel.label.