Resources

Summary

# Gemfile
source 'https://rails-assets.org' do
  gem 'rails-assets-jquery'
  gem 'rails-assets-bootstrap', '~> 4.0.0.alpha.6'
  gem 'rails-assets-tether'
end

# application.js
//= require jquery
...
//= require tether
//= require bootstrap

# application.css
*= require bootstrap

# helpers/bootstrap/common_helper.rb
module Bootstrap::CommonHelper
  ArgumentError = Class.new(::ArgumentError)
  
  # Returns a new Hash with:
  # * keys converted to Symbols
  # * the +:class+ key has its value converted to an Array of String
  # @example
  # canonicalize_options("id" => "ID", "class" => "CLASS") # => {:id=>"ID", :class=>["CLASS"]} 
  # canonicalize_options(:class => 'one two') # => {:class=>["one", "two"]}
  # canonicalize_options("class" => [:one, 2]) # => {:class=>["one", "2"]} 
  # @param [Hash] hash typically an +options+ param to a method call
  # @raise [ArgumentError] if _hash_ is not a Hash
  # @return [Hash]
  def canonicalize_options(hash)
    raise ArgumentError.new("expected a Hash, got #{hash.inspect}") unless hash.is_a?(Hash)

    hash.symbolize_keys.tap do |h|
      h[:class] = arrayify_and_stringify_elements(h[:class])
    end
  end

  # Returns a new Array of String from _arg_.
  # @example
  # arrayify_and_stringify_elements(nil) #=> [] 
  # arrayify_and_stringify_elements('foo') #=> ["foo"]
  # arrayify_and_stringify_elements('foo bar') #=> ["foo", "bar"]
  # arrayify_and_stringify_elements([:foo, 'bar']) #=> ["foo", "bar"]
  # @param [String, Array] arg
  # @return [Array of String]
  def arrayify_and_stringify_elements(arg)
    return false if arg == false
    
    case
    when arg.blank? then []
    when arg.is_a?(Array) then arg
    else arg.to_s.strip.split(/\s/)
    end.map(&:to_s)
  end
  
  # Returns down-caret character used in various dropdown menus.
  # @param [Hash] options html options for 
  # @example
  # caret(id: 'my-id') #=> 
  # @return [String]
  def caret(options={})
    options= canonicalize_options(options)
    options = ensure_class(options, 'caret')
    content_tag(:span, nil, options)
  end
  
  # Returns new (canonicalized) Hash where :class value includes _klasses_.
  # 
  # @example
  # ensure_class({class: []}, 'foo') #=> {class: 'foo'}
  # ensure_class({class: ['bar'], id: 'my-id'}, ['foo', 'foo2']) #=> {:class=>["bar", "foo", "foo2"], :id=>"my-id"}
  # @param [Hash] hash
  # @param [String, Array] klasses one or more classes to add to the +:class+ key of _hash_
  # @return [Hash]
  def ensure_class(hash, klasses)
    canonicalize_options(hash)
    
    hash.dup.tap do |h|
      Array(klasses).map(&:to_s).each do |k|
        h[:class] << k unless h[:class].include?(k)
      end
    end
  end

  # Returns extra arguments that are Bootstrap modifiers. Basically 2nd argument
  # up to (not including) the last (Hash) argument.
  #
  # @example
  # extract_extras('text') #=> []
  # extract_extras('text', :small, :info, id: 'foo') #=> [:small, :info]
  # @return [Array]
  def extract_extras(*args)
    args.extract_options!
    args.shift
    args
  end
  
  def bootstrap_generator(*args, bs_class, element, &block)
    options = canonicalize_options(args.extract_options!)
    options = ensure_class(options, bs_class)
  
    content = block_given? ? capture(&block) : args.shift
  
    content_tag(element.to_sym, options) do
      content
    end
  end
end

# helpers/bootstrap/modal_helper.rb
module Bootstrap::ModalHelper
  ArgumentError = Class.new(StandardError)
  def modal_trigger(text, options={})
    options = canonicalize_options(options)
    href = options.delete(:href) or raise(ArgumentError, 'missing :href option')
    options.merge!(role: 'button', href: href, data: { toggle: 'modal'})
    options = ensure_class(options, 'btn')
    
    content_tag(:a, text, options)
  end

  def modal(options={})
    options = canonicalize_options(options)
    options.has_key?(:id) or raise(ArgumentError, "missing :id option")
    options = ensure_class(options, %w(modal fade))
    content_tag(:div, options) do
      content_tag(:div, class: 'modal-dialog', role: :document) do
        content_tag(:div, class: 'modal-content') do
          yield
        end
      end
    end
  end
  
  def modal_header(*args, &block)
    options = canonicalize_options(args.extract_options!)
    options = ensure_class(options, 'modal-header')
  
    content = block_given? ? capture(&block) : args.shift

    button_content = content_tag(:button, class: :close, data: { dismiss: :modal }, aria: { label: 'Close' }) do
      content_tag(:span, "×".html_safe, aria: { hidden: true }).html_safe + content_tag(:span, "Close", class: 'sr-only')
    end

    content_tag(:div, options) do
      content_tag(:h4, content, class: 'modal-title') + button_content.html_safe
    end.html_safe

  end

  def modal_title(*args, &block)
    bootstrap_generator(*args, 'modal-title', :h4, &block)
  end
  
  def modal_body(*args, &block)
    bootstrap_generator(*args, 'modal-body', :div, &block)
  end

  def modal_footer(*args, &block)
    options = canonicalize_options(args.extract_options!)
    options = ensure_class(options, 'modal-footer')
  
    content = block_given? ? capture(&block) : args.shift
    button_close_content = content_tag(:button, 'Close', type: :button, class: 'btn btn-secondary', data: { dismiss: :modal })
    content_tag(:div, options) do
      button_close_content + content
    end
  end
end

# helpers/bootstrap/card_helper.rb
module Bootstrap::CardHelper
  def card(options={})
    options = canonicalize_options(options)
    options = ensure_class(options, %w(card))
    content_tag(:div, options) do
      content_tag(:div, class: 'card-block') do
        yield
      end
    end
  end
  
  def card_header(*args, &block)
    bootstrap_generator(*args, 'card-header', :h5, &block)
  end

  def card_title(*args, &block)
    bootstrap_generator(*args, 'card-title', :h5, &block)
  end
  
  def card_subtitle(*args, &block)
    bootstrap_generator(*args, 'card-subtitle mb-2 text-muted', :h6, &block)
  end
  
  def card_body(*args, &block)
    bootstrap_generator(*args, 'card-text', :p, &block)
  end
  
  def card_list(*args, &block)
    bootstrap_generator(*args, 'list-group list-group-flush', :ul, &block)
  end  

  def card_list_item(*args, &block)
    bootstrap_generator(*args, 'list-group-item', :li, &block)
  end
end