Skip to content

Instantly share code, notes, and snippets.

@tlux
Last active December 20, 2015 18:39
Show Gist options
  • Select an option

  • Save tlux/6177072 to your computer and use it in GitHub Desktop.

Select an option

Save tlux/6177072 to your computer and use it in GitHub Desktop.
Breadcrumbs
class ApplicationController < ActionController::Base
include BreadcrumbedController
before_action :set_controller_context
private
def set_controller_context
RequestStore.store[:controller_context] = self
end
end
class Breadcrumb
attr_reader :breadcrumbs, :caption, :destination, :path, :url
private :breadcrumbs
def initialize(breadcrumbs, caption, destination)
raise ArgumentError, 'no breadcrumb collection specified' if breadcrumbs.nil?
raise ArgumentError, 'caption is blank' if caption.blank?
raise ArgumentError, 'no destination specified' if destination.nil?
@breadcrumbs = breadcrumbs
@caption = caption.to_s
@destination = destination
if @destination.is_a? String
@path = @url = @destination
else
@path = breadcrumbs.controller_context.polymorphic_path(destination)
@url = breadcrumbs.controller_context.polymorphic_url(destination)
end
end
def index
breadcrumbs.index(self)
end
def first?
index.zero?
end
def last?
index == breadcrumbs.length - 1
end
def inspect
"#<#{self.class.name} caption: #{caption.inspect}, destination: #{destination.inspect}>"
end
def to_s
caption
end
end
class BreadcrumbCollection < Array
attr_reader :controller_context
def initialize(controller_context)
super()
@controller_context = controller_context
end
def append(caption, destination, options = {})
add_with_method(:push, caption, destination, options)
end
alias_method :add, :append
def inspect
substr = " " + map { |item| "\"#{item.caption}\"=>\"#{item.path}\"" }.join(', ')
"#<#{self.class.name}#{substr}>"
end
def prepend(caption, destination, options = {})
add_with_method(:unshift, caption, destination, options)
end
private
def add_with_method(method, caption, destination, options)
options.assert_valid_keys(:only, :except)
breadcrumb_addable = options[:only] && controller_context.action_name.in?(Array(options[:only]).map(&:to_s))
breadcrumb_addable ||= options[:except] && !controller_context.action_name.in?(Array(options[:except]).map(&:to_s))
breadcrumb_addable ||= options[:only].nil? && options[:except].nil?
send(method, Breadcrumb.new(self, caption, destination)) if breadcrumb_addable
end
end
module BreadcrumbedController
extend ActiveSupport::Concern
RECORD_CAPTION_METHODS = %i(name title to_s).freeze
included do
helper_method :breadcrumbs
end
module ClassMethods
protected
def breadcrumb(caption, destination, options = {})
before_action(options) do |controller|
controller.breadcrumbs.append(caption, destination)
end
end
def breadcrumbs(options = {}, &block)
raise ArgumentError, 'no block given' unless block_given?
before_action(options) do |controller|
controller.instance_exec(breadcrumbs, controller, &block)
end
end
# TODO Refactor!!!
def breadcrumbs_for_resource(*args)
options = args.extract_options!
route_key = options[:route]
record_caption_methods = Array(options.fetch(:record_caption) { RECORD_CAPTION_METHODS })
breadcrumbs(options.slice(:only, :except, :if, :unless)) do |b, controller|
parent_resource = args.first.present? && try_call(options[:parent]) != false
controller_namespace = options.fetch(:namespace) do
controller.class.name.deconstantize.split('::').map(&:underscore)
end
controller_namespace = try_call(controller_namespace)
if controller_namespace == false
controller_namespace = []
else
controller_namespace = Array(controller_namespace).flatten.map(&:to_sym)
end
# Determine collection and element names
if options[:class_name]
element_name = args.first
resource_class = options[:class_name].constantize
else
resource_class = case args.first
when NilClass then controller.controller_name.classify.constantize
when Class then args.first
when Symbol, String then args.first.to_s.classify.constantize
else raise ArgumentError, 'resource element and collection name could not be determined by reflection'
end
end
element_name ||= resource_class.model_name.element.to_sym
collection_name ||= resource_class.model_name.collection.to_sym
@parent_breadcrumb_records ||= []
@breadcrumb_routes ||= {}
if parent_resource
if record = load_record(element_name, options)
if !singular_string?(args.first.to_s) and try_call(options[:include_collection]) != false
b.append(resource_class.model_name.human(count: 2), controller_namespace + [collection_name])
end
record_caption = load_caption(record)
if try_call(options[:include_element]) != false
b.append(record_caption, controller_namespace + @parent_breadcrumb_records + [record])
end
@parent_breadcrumb_records << record
@breadcrumb_routes[record] = route_key.to_s.try(:singularize).presence || record.class.model_name.singular
end
else
# Resource is primary resource in controller
if try_call(options[:include_collection]) != false
b.append(resource_class.model_name.human(count: 2), controller_namespace + @parent_breadcrumb_records + [collection_name])
end
if controller.action_name.in? %w(show edit update)
if record = load_record(element_name, options)
@breadcrumb_routes[record] = route_key.to_s.try(:singularize).presence || record.class.model_name.singular
record_caption = load_caption(record)
if try_call(options[:include_element]) != false
if route_key
#raise @parent_breadcrumb_records.inspect
route = send("#{controller_namespace.join('_')}_#{@breadcrumb_routes.values.join('_')}_path", *@parent_breadcrumb_records, record)
else
route = controller_namespace + @parent_breadcrumb_records + [record]
end
b.append(record_caption, route)
end
end
end
if controller.action_name.in? %w(new create)
if route_key
route = send("new_#{controller_namespace.join('_')}_#{@breadcrumb_routes.values.join('_')}_path", *@parent_breadcrumb_records)
else
route = [:new] + controller_namespace + @parent_breadcrumb_records + [element_name]
end
b.append(I18n.translate('breadcrumbs.new', model: resource_class.model_name.human), route)
end
if controller.action_name.in? %w(edit update)
# [:edit, controller_namespace, @parent_breadcrumb_records, record].compact.flatten(1)
if route_key
route = send("edit_#{controller_namespace.join('_')}_#{@breadcrumb_routes.values.join('_')}_path", *@parent_breadcrumb_records, record)
else
route = [:edit] + controller_namespace + @parent_breadcrumb_records + [record]
end
b.append(I18n.translate('breadcrumbs.edit', model: resource_class.model_name.human), route)
end
end
end
end
end
protected
def breadcrumbs
@breadcrumbs ||= BreadcrumbCollection.new(self)
end
private
def load_caption(record)
RECORD_CAPTION_METHODS.collect { |method| record.try(method) }.reject(&:blank?).compact.first
end
def load_record(element_name, options)
record = instance_variable_get("@#{element_name}")
raise "@#{element_name} has not been set in #{self.class.name.deconstantize.underscore}/#{controller_name}##{action_name}" if options[:allow_nil] != true and record.nil?
if options[:class_name]
record.becomes(options[:class_name].to_s.constantize)
else
record
end
end
def singular_string?(str)
str.pluralize != str and str.singularize == str
end
def try_call(proc_or_symbol_or_object, *args)
return nil if proc_or_symbol_or_object.nil?
if proc_or_symbol_or_object.is_a? Symbol
method(proc_or_symbol_or_object).call
elsif proc_or_symbol_or_object.respond_to? :call
self.instance_exec(*args, &proc_or_symbol_or_object)
else
proc_or_symbol_or_object
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment