Bootstrap Framework and Ruby on Rails

Episode #81 by Teacher's Avatar David Kimura

Summary

Using Bootstrap in your application has become much more simple. Also learn to create some helper methods to make working with Bootstrap much easier.
rails view css 9:18

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