All Files ( 29.57% covered at 3.42 hits/line )
107 files in total.
5418 relevant lines,
1602 lines covered and
3816 lines missed.
(
29.57%
)
# frozen_string_literal: true
#--
# Copyright (c) 2004-2020 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#++
- 9
require "active_support"
- 9
require "active_support/rails"
- 9
require "action_view/version"
- 9
module ActionView
- 9
extend ActiveSupport::Autoload
- 9
ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
- 9
eager_autoload do
- 9
autoload :Base
- 9
autoload :Context
- 9
autoload :Digestor
- 9
autoload :Helpers
- 9
autoload :LookupContext
- 9
autoload :Layouts
- 9
autoload :PathSet
- 9
autoload :RecordIdentifier
- 9
autoload :Rendering
- 9
autoload :RoutingUrlFor
- 9
autoload :Template
- 9
autoload :UnboundTemplate
- 9
autoload :ViewPaths
- 9
autoload_under "renderer" do
- 9
autoload :Renderer
- 9
autoload :AbstractRenderer
- 9
autoload :PartialRenderer
- 9
autoload :CollectionRenderer
- 9
autoload :ObjectRenderer
- 9
autoload :TemplateRenderer
- 9
autoload :StreamingTemplateRenderer
end
- 9
autoload_at "action_view/template/resolver" do
- 9
autoload :Resolver
- 9
autoload :PathResolver
- 9
autoload :FileSystemResolver
- 9
autoload :OptimizedFileSystemResolver
- 9
autoload :FallbackFileSystemResolver
end
- 9
autoload_at "action_view/buffers" do
- 9
autoload :OutputBuffer
- 9
autoload :StreamingBuffer
end
- 9
autoload_at "action_view/flows" do
- 9
autoload :OutputFlow
- 9
autoload :StreamingFlow
end
- 9
autoload_at "action_view/template/error" do
- 9
autoload :MissingTemplate
- 9
autoload :ActionViewError
- 9
autoload :EncodingError
- 9
autoload :TemplateError
- 9
autoload :SyntaxErrorInTemplate
- 9
autoload :WrongEncodingError
end
end
- 9
autoload :CacheExpiry
- 9
autoload :TestCase
- 9
def self.eager_load!
super
ActionView::Helpers.eager_load!
ActionView::Template.eager_load!
end
end
- 9
require "active_support/core_ext/string/output_safety"
- 9
ActiveSupport.on_load(:i18n) do
- 9
I18n.load_path << File.expand_path("action_view/locale/en.yml", __dir__)
end
# frozen_string_literal: true
- 3
require "active_support/core_ext/module/attr_internal"
- 3
require "active_support/core_ext/module/attribute_accessors"
- 3
require "active_support/ordered_options"
- 3
require "action_view/log_subscriber"
- 3
require "action_view/helpers"
- 3
require "action_view/context"
- 3
require "action_view/template"
- 3
require "action_view/lookup_context"
- 3
module ActionView #:nodoc:
# = Action View Base
#
# Action View templates can be written in several ways.
# If the template file has a <tt>.erb</tt> extension, then it uses the erubi[https://rubygems.org/gems/erubi]
# template system which can embed Ruby into an HTML document.
# If the template file has a <tt>.builder</tt> extension, then Jim Weirich's Builder::XmlMarkup library is used.
#
# == ERB
#
# You trigger ERB by using embeddings such as <tt><% %></tt>, <tt><% -%></tt>, and <tt><%= %></tt>. The <tt><%= %></tt> tag set is used when you want output. Consider the
# following loop for names:
#
# <b>Names of all the people</b>
# <% @people.each do |person| %>
# Name: <%= person.name %><br/>
# <% end %>
#
# The loop is set up in regular embedding tags <tt><% %></tt>, and the name is written using the output embedding tag <tt><%= %></tt>. Note that this
# is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong:
#
# <%# WRONG %>
# Hi, Mr. <% puts "Frodo" %>
#
# If you absolutely must write from within a function use +concat+.
#
# When on a line that only contains whitespaces except for the tag, <tt><% %></tt> suppresses leading and trailing whitespace,
# including the trailing newline. <tt><% %></tt> and <tt><%- -%></tt> are the same.
# Note however that <tt><%= %></tt> and <tt><%= -%></tt> are different: only the latter removes trailing whitespaces.
#
# === Using sub templates
#
# Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
# classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
#
# <%= render "shared/header" %>
# Something really specific and terrific
# <%= render "shared/footer" %>
#
# As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
# result of the rendering. The output embedding writes it to the current template.
#
# But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
# variables defined using the regular embedding tags. Like this:
#
# <% @page_title = "A Wonderful Hello" %>
# <%= render "shared/header" %>
#
# Now the header can pick up on the <tt>@page_title</tt> variable and use it for outputting a title tag:
#
# <title><%= @page_title %></title>
#
# === Passing local variables to sub templates
#
# You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
#
# <%= render "shared/header", { headline: "Welcome", person: person } %>
#
# These can now be accessed in <tt>shared/header</tt> with:
#
# Headline: <%= headline %>
# First name: <%= person.first_name %>
#
# The local variables passed to sub templates can be accessed as a hash using the <tt>local_assigns</tt> hash. This lets you access the
# variables as:
#
# Headline: <%= local_assigns[:headline] %>
#
# This is useful in cases where you aren't sure if the local variable has been assigned. Alternatively, you could also use
# <tt>defined? headline</tt> to first check if the variable has been assigned before using it.
#
# === Template caching
#
# By default, Rails will compile each template to a method in order to render it. When you alter a template,
# Rails will check the file's modification time and recompile it in development mode.
#
# == Builder
#
# Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object
# named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension.
#
# Here are some basic examples:
#
# xml.em("emphasized") # => <em>emphasized</em>
# xml.em { xml.b("emph & bold") } # => <em><b>emph & bold</b></em>
# xml.a("A Link", "href" => "http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
# xml.target("name" => "compile", "option" => "fast") # => <target option="fast" name="compile"\>
# # NOTE: order of attributes is not specified.
#
# Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
#
# xml.div do
# xml.h1(@person.name)
# xml.p(@person.bio)
# end
#
# would produce something like:
#
# <div>
# <h1>David Heinemeier Hansson</h1>
# <p>A product of Danish Design during the Winter of '79...</p>
# </div>
#
# Here is a full-length RSS example actually used on Basecamp:
#
# xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
# xml.channel do
# xml.title(@feed_title)
# xml.link(@url)
# xml.description "Basecamp: Recent items"
# xml.language "en-us"
# xml.ttl "40"
#
# @recent_items.each do |item|
# xml.item do
# xml.title(item_title(item))
# xml.description(item_description(item)) if item_description(item)
# xml.pubDate(item_pubDate(item))
# xml.guid(@person.firm.account.url + @recent_items.url(item))
# xml.link(@person.firm.account.url + @recent_items.url(item))
#
# xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
# end
# end
# end
# end
#
# For more information on Builder please consult the {source
# code}[https://github.com/jimweirich/builder].
- 3
class Base
- 3
include Helpers, ::ERB::Util, Context
# Specify the proc used to decorate input tags that refer to attributes with errors.
- 3
cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
# How to complete the streaming when an exception occurs.
# This is our best guess: first try to close the attribute, then the tag.
- 3
cattr_accessor :streaming_completion_on_exception, default: %("><script>window.location = "/500.html"</script></html>)
# Specify whether rendering within namespaced controllers should prefix
# the partial paths for ActiveModel objects with the namespace.
# (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb)
- 3
cattr_accessor :prefix_partial_path_with_controller_namespace, default: true
# Specify default_formats that can be rendered.
- 3
cattr_accessor :default_formats
# Specify whether an error should be raised for missing translations
- 3
cattr_accessor :raise_on_missing_translations, default: false
# Specify whether submit_tag should automatically disable on click
- 3
cattr_accessor :automatically_disable_submit_tag, default: true
# Annotate rendered view with file names
- 3
cattr_accessor :annotate_rendered_view_with_filenames, default: false
- 3
class_attribute :_routes
- 3
class_attribute :logger
- 3
class << self
- 3
delegate :erb_trim_mode=, to: "ActionView::Template::Handlers::ERB"
- 3
def cache_template_loading
ActionView::Resolver.caching?
end
- 3
def cache_template_loading=(value)
ActionView::Resolver.caching = value
end
- 3
def xss_safe? #:nodoc:
true
end
- 3
def with_empty_template_cache # :nodoc:
subclass = Class.new(self) {
# We can't implement these as self.class because subclasses will
# share the same template cache as superclasses, so "changed?" won't work
# correctly.
define_method(:compiled_method_container) { subclass }
define_singleton_method(:compiled_method_container) { subclass }
def self.name
superclass.name
end
def inspect
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
end
}
end
- 3
def changed?(other) # :nodoc:
compiled_method_container != other.compiled_method_container
end
end
- 3
attr_reader :view_renderer, :lookup_context
- 3
attr_internal :config, :assigns
- 3
delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, to: :lookup_context
- 3
def assign(new_assigns) # :nodoc:
@_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
# :stopdoc:
- 3
def self.build_lookup_context(context)
case context
when ActionView::Renderer
context.lookup_context
when Array
ActionView::LookupContext.new(context)
when ActionView::PathSet
ActionView::LookupContext.new(context)
when nil
ActionView::LookupContext.new([])
else
raise NotImplementedError, context.class.name
end
end
- 3
def self.empty
with_view_paths([])
end
- 3
def self.with_view_paths(view_paths, assigns = {}, controller = nil)
with_context ActionView::LookupContext.new(view_paths), assigns, controller
end
- 3
def self.with_context(context, assigns = {}, controller = nil)
new context, assigns, controller
end
- 3
NULL = Object.new
# :startdoc:
- 3
def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc:
@_config = ActiveSupport::InheritableOptions.new
unless formats == NULL
ActiveSupport::Deprecation.warn <<~eowarn.squish
Passing formats to ActionView::Base.new is deprecated
eowarn
end
case lookup_context
when ActionView::LookupContext
@lookup_context = lookup_context
else
ActiveSupport::Deprecation.warn <<~eowarn.squish
ActionView::Base instances should be constructed with a lookup context,
assignments, and a controller.
eowarn
@lookup_context = self.class.build_lookup_context(lookup_context)
end
@view_renderer = ActionView::Renderer.new @lookup_context
@current_template = nil
@cache_hit = {}
assign(assigns)
assign_controller(controller)
_prepare_context
end
- 3
def _run(method, template, locals, buffer, add_to_stack: true, &block)
_old_output_buffer, _old_template = @output_buffer, @current_template
@current_template = template if add_to_stack
@output_buffer = buffer
send(method, locals, buffer, &block)
ensure
@output_buffer, @current_template = _old_output_buffer, _old_template
end
- 3
def compiled_method_container
if self.class == ActionView::Base
ActiveSupport::Deprecation.warn <<~eowarn.squish
ActionView::Base instances must implement `compiled_method_container`
or use the class method `with_empty_template_cache` for constructing
an ActionView::Base instance that has an empty cache.
eowarn
end
self.class
end
- 3
def in_rendering_context(options)
old_view_renderer = @view_renderer
old_lookup_context = @lookup_context
if !lookup_context.html_fallback_for_js && options[:formats]
formats = Array(options[:formats])
if formats == [:js]
formats << :html
end
@lookup_context = lookup_context.with_prepended_formats(formats)
@view_renderer = ActionView::Renderer.new @lookup_context
end
yield @view_renderer
ensure
@view_renderer = old_view_renderer
@lookup_context = old_lookup_context
end
- 3
ActiveSupport.run_load_hooks(:action_view, self)
end
end
# frozen_string_literal: true
require "active_support/core_ext/string/output_safety"
module ActionView
# Used as a buffer for views
#
# The main difference between this and ActiveSupport::SafeBuffer
# is for the methods `<<` and `safe_expr_append=` the inputs are
# checked for nil before they are assigned and `to_s` is called on
# the input. For example:
#
# obuf = ActionView::OutputBuffer.new "hello"
# obuf << 5
# puts obuf # => "hello5"
#
# sbuf = ActiveSupport::SafeBuffer.new "hello"
# sbuf << 5
# puts sbuf # => "hello\u0005"
#
class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc:
def initialize(*)
super
encode!
end
def <<(value)
return self if value.nil?
super(value.to_s)
end
alias :append= :<<
def safe_expr_append=(val)
return self if val.nil?
safe_concat val.to_s
end
alias :safe_append= :safe_concat
end
class StreamingBuffer #:nodoc:
def initialize(block)
@block = block
end
def <<(value)
value = value.to_s
value = ERB::Util.h(value) unless value.html_safe?
@block.call(value)
end
alias :concat :<<
alias :append= :<<
def safe_concat(value)
@block.call(value.to_s)
end
alias :safe_append= :safe_concat
def html_safe?
true
end
def html_safe
self
end
end
end
# frozen_string_literal: true
module ActionView
class CacheExpiry
class Executor
def initialize(watcher:)
@cache_expiry = CacheExpiry.new(watcher: watcher)
end
def before(target)
@cache_expiry.clear_cache_if_necessary
end
end
def initialize(watcher:)
@watched_dirs = nil
@watcher_class = watcher
@watcher = nil
@mutex = Mutex.new
end
def clear_cache_if_necessary
@mutex.synchronize do
watched_dirs = dirs_to_watch
return if watched_dirs.empty?
if watched_dirs != @watched_dirs
@watched_dirs = watched_dirs
@watcher = @watcher_class.new([], watched_dirs) do
clear_cache
end
@watcher.execute
else
@watcher.execute_if_updated
end
end
end
def clear_cache
ActionView::LookupContext::DetailsKey.clear
end
private
def dirs_to_watch
all_view_paths.grep(FileSystemResolver).map!(&:path).tap(&:uniq!).sort!
end
def all_view_paths
ActionView::ViewPaths.all_view_paths.flat_map(&:paths)
end
end
end
# frozen_string_literal: true
- 6
module ActionView
# = Action View Context
#
# Action View contexts are supplied to Action Controller to render a template.
# The default Action View context is ActionView::Base.
#
# In order to work with Action Controller, a Context must just include this
# module. The initialization of the variables used by the context
# (@output_buffer, @view_flow, and @virtual_path) is responsibility of the
# object that includes this module (although you can call _prepare_context
# defined below).
- 6
module Context
- 6
attr_accessor :output_buffer, :view_flow
# Prepares the context by setting the appropriate instance variables.
- 6
def _prepare_context
@view_flow = OutputFlow.new
@output_buffer = nil
end
# Encapsulates the interaction with the view flow so it
# returns the correct buffer on +yield+. This is usually
# overwritten by helpers to add more behavior.
- 6
def _layout_for(name = nil)
name ||= :layout
view_flow.get(name).html_safe
end
end
end
# frozen_string_literal: true
- 3
require "concurrent/map"
- 3
require "action_view/path_set"
- 3
module ActionView
- 3
class DependencyTracker # :nodoc:
- 3
@trackers = Concurrent::Map.new
- 3
def self.find_dependencies(name, template, view_paths = nil)
tracker = @trackers[template.handler]
return [] unless tracker
tracker.call(name, template, view_paths)
end
- 3
def self.register_tracker(extension, tracker)
- 3
handler = Template.handler_for_extension(extension)
- 3
if tracker.respond_to?(:supports_view_paths?)
- 3
@trackers[handler] = tracker
else
@trackers[handler] = lambda { |name, template, _|
tracker.call(name, template)
}
end
end
- 3
def self.remove_tracker(handler)
@trackers.delete(handler)
end
- 3
class ERBTracker # :nodoc:
- 3
EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
# A valid ruby identifier - suitable for class, method and specially variable names
- 3
IDENTIFIER = /
[[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
[[:word:]]* # followed by optional letters, numbers or underscores
/x
# Any kind of variable name. e.g. @instance, @@class, $global or local.
# Possibly following a method call chain
- 3
VARIABLE_OR_METHOD_CHAIN = /
(?:\$|@{1,2})? # optional global, instance or class variable indicator
(?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
(?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
/x
# A simple string literal. e.g. "School's out!"
- 3
STRING = /
(?<quote>['"]) # an opening quote
(?<static>.*?) # with anything inside, captured as STATIC
\k<quote> # and a matching closing quote
/x
# Part of any hash containing the :partial key
- 3
PARTIAL_HASH_KEY = /
(?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
\s* # followed by optional spaces
/x
# Part of any hash containing the :layout key
- 3
LAYOUT_HASH_KEY = /
(?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
\s* # followed by optional spaces
/x
# Matches:
# partial: "comments/comment", collection: @all_comments => "comments/comment"
# (object: @single_comment, partial: "comments/comment") => "comments/comment"
#
# "comments/comments"
# 'comments/comments'
# ('comments/comments')
#
# (@topic) => "topics/topic"
# topics => "topics/topic"
# (message.topics) => "topics/topic"
- 3
RENDER_ARGUMENTS = /\A
(?:\s*\(?\s*) # optional opening paren surrounded by spaces
(?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
- 3
LAYOUT_DEPENDENCY = /\A
(?:\s*\(?\s*) # optional opening paren surrounded by spaces
(?:.*?#{LAYOUT_HASH_KEY}) # check if the line has layout key declaration
(?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
/xm
- 3
def self.supports_view_paths? # :nodoc:
true
end
- 3
def self.call(name, template, view_paths = nil)
new(name, template, view_paths).dependencies
end
- 3
def initialize(name, template, view_paths = nil)
- 48
@name, @template, @view_paths = name, template, view_paths
end
- 3
def dependencies
- 48
render_dependencies + explicit_dependencies
end
- 3
attr_reader :name, :template
- 3
private :name, :template
- 3
private
- 3
def source
- 96
template.source
end
- 3
def directory
- 12
name.split("/")[0..-2].join("/")
end
- 3
def render_dependencies
- 48
render_dependencies = []
- 48
render_calls = source.split(/\brender\b/).drop(1)
- 48
render_calls.each do |arguments|
- 69
add_dependencies(render_dependencies, arguments, LAYOUT_DEPENDENCY)
- 69
add_dependencies(render_dependencies, arguments, RENDER_ARGUMENTS)
end
- 48
render_dependencies.uniq
end
- 3
def add_dependencies(render_dependencies, arguments, pattern)
- 138
arguments.scan(pattern) do
- 75
match = Regexp.last_match
- 75
add_dynamic_dependency(render_dependencies, match[:dynamic])
- 75
add_static_dependency(render_dependencies, match[:static], match[:quote])
end
end
- 3
def add_dynamic_dependency(dependencies, dependency)
- 75
if dependency
- 27
dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
end
end
- 3
def add_static_dependency(dependencies, dependency, quote_type)
- 75
if quote_type == '"'
# Ignore if there is interpolation
- 15
return if dependency.include?('#{')
end
- 72
if dependency
- 45
if dependency.include?("/")
- 33
dependencies << dependency
else
- 12
dependencies << "#{directory}/#{dependency}"
end
end
end
- 3
def resolve_directories(wildcard_dependencies)
- 48
return [] unless @view_paths
wildcard_dependencies.flat_map { |query, templates|
@view_paths.find_all_with_query(query).map do |template|
"#{File.dirname(query)}/#{File.basename(template).split('.').first}"
end
}.sort
end
- 3
def explicit_dependencies
- 48
dependencies = source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
- 48
wildcards, explicits = dependencies.partition { |dependency| dependency.end_with?("*") }
- 48
(explicits + resolve_directories(wildcards)).uniq
end
end
- 3
register_tracker :erb, ERBTracker
end
end
# frozen_string_literal: true
- 3
require "action_view/dependency_tracker"
- 3
module ActionView
- 3
class Digestor
- 3
@@digest_mutex = Mutex.new
- 3
class << self
# Supported options:
#
# * <tt>name</tt> - Template name
# * <tt>format</tt> - Template format
# * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
# * <tt>dependencies</tt> - An array of dependent views
- 3
def digest(name:, format: nil, finder:, dependencies: nil)
if dependencies.nil? || dependencies.empty?
cache_key = "#{name}.#{format}"
else
cache_key = [ name, format, dependencies ].flatten.compact.join(".")
end
# this is a correctly done double-checked locking idiom
# (Concurrent::Map's lookups have volatile semantics)
finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
finder.digest_cache.fetch(cache_key) do # re-check under lock
partial = name.include?("/_")
root = tree(name, finder, partial)
dependencies.each do |injected_dep|
root.children << Injected.new(injected_dep, nil, nil)
end if dependencies
finder.digest_cache[cache_key] = root.digest(finder)
end
end
end
- 3
def logger
ActionView::Base.logger || NullLogger
end
# Create a dependency tree for template named +name+.
- 3
def tree(name, finder, partial = false, seen = {})
logical_name = name.gsub(%r|/_|, "/")
interpolated = name.include?("#")
if !interpolated && (template = find_template(finder, logical_name, [], partial, []))
if node = seen[template.identifier] # handle cycles in the tree
node
else
node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
node.children << tree(dep_file, finder, true, seen)
end
node
end
else
unless interpolated # Dynamic template partial names can never be tracked
logger.error " Couldn't find template for digesting: #{name}"
end
seen[name] ||= Missing.new(name, logical_name, nil)
end
end
- 3
private
- 3
def find_template(finder, name, prefixes, partial, keys)
finder.disable_cache do
finder.find_all(name, prefixes, partial, keys).first
end
end
end
- 3
class Node
- 3
attr_reader :name, :logical_name, :template, :children
- 3
def self.create(name, logical_name, template, partial)
klass = partial ? Partial : Node
klass.new(name, logical_name, template, [])
end
- 3
def initialize(name, logical_name, template, children = [])
@name = name
@logical_name = logical_name
@template = template
@children = children
end
- 3
def digest(finder, stack = [])
ActiveSupport::Digest.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
end
- 3
def dependency_digest(finder, stack)
children.map do |node|
if stack.include?(node)
false
else
finder.digest_cache[node.name] ||= begin
stack.push node
node.digest(finder, stack).tap { stack.pop }
end
end
end.join("-")
end
- 3
def to_dep_map
children.any? ? { name => children.map(&:to_dep_map) } : name
end
end
- 3
class Partial < Node; end
- 3
class Missing < Node
- 3
def digest(finder, _ = []) "" end
end
- 3
class Injected < Node
- 3
def digest(finder, _ = []) name end
end
- 3
class NullLogger
- 3
def self.debug(_); end
- 3
def self.error(_); end
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/string/output_safety"
module ActionView
class OutputFlow #:nodoc:
attr_reader :content
def initialize
@content = Hash.new { |h, k| h[k] = ActiveSupport::SafeBuffer.new }
end
# Called by _layout_for to read stored values.
def get(key)
@content[key]
end
# Called by each renderer object to set the layout contents.
def set(key, value)
@content[key] = ActiveSupport::SafeBuffer.new(value)
end
# Called by content_for
def append(key, value)
@content[key] << value
end
alias_method :append!, :append
end
class StreamingFlow < OutputFlow #:nodoc:
def initialize(view, fiber)
@view = view
@parent = nil
@child = view.output_buffer
@content = view.view_flow.content
@fiber = fiber
@root = Fiber.current.object_id
end
# Try to get stored content. If the content
# is not available and we're inside the layout fiber,
# then it will begin waiting for the given key and yield.
def get(key)
return super if @content.key?(key)
if inside_fiber?
view = @view
begin
@waiting_for = key
view.output_buffer, @parent = @child, view.output_buffer
Fiber.yield
ensure
@waiting_for = nil
view.output_buffer, @child = @parent, view.output_buffer
end
end
super
end
# Appends the contents for the given key. This is called
# by providing and resuming back to the fiber,
# if that's the key it's waiting for.
def append!(key, value)
super
@fiber.resume if @waiting_for == key
end
private
def inside_fiber?
Fiber.current.object_id != @root
end
end
end
# frozen_string_literal: true
- 9
module ActionView
# Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt>
- 9
def self.gem_version
Gem::Version.new VERSION::STRING
end
- 9
module VERSION
- 9
MAJOR = 6
- 9
MINOR = 1
- 9
TINY = 0
- 9
PRE = "alpha"
- 9
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end
# frozen_string_literal: true
- 9
require "active_support/benchmarkable"
- 9
module ActionView #:nodoc:
- 9
module Helpers #:nodoc:
- 9
extend ActiveSupport::Autoload
- 9
autoload :ActiveModelHelper
- 9
autoload :AssetTagHelper
- 9
autoload :AssetUrlHelper
- 9
autoload :AtomFeedHelper
- 9
autoload :CacheHelper
- 9
autoload :CaptureHelper
- 9
autoload :ControllerHelper
- 9
autoload :CspHelper
- 9
autoload :CsrfHelper
- 9
autoload :DateHelper
- 9
autoload :DebugHelper
- 9
autoload :FormHelper
- 9
autoload :FormOptionsHelper
- 9
autoload :FormTagHelper
- 9
autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
- 9
autoload :NumberHelper
- 9
autoload :OutputSafetyHelper
- 9
autoload :RenderingHelper
- 9
autoload :SanitizeHelper
- 9
autoload :TagHelper
- 9
autoload :TextHelper
- 9
autoload :TranslationHelper
- 9
autoload :UrlHelper
- 9
autoload :Tags
- 9
def self.eager_load!
super
Tags.eager_load!
end
- 9
extend ActiveSupport::Concern
- 9
include ActiveSupport::Benchmarkable
- 9
include ActiveModelHelper
- 9
include AssetTagHelper
- 9
include AssetUrlHelper
- 9
include AtomFeedHelper
- 9
include CacheHelper
- 9
include CaptureHelper
- 9
include ControllerHelper
- 9
include CspHelper
- 9
include CsrfHelper
- 9
include DateHelper
- 9
include DebugHelper
- 9
include FormHelper
- 9
include FormOptionsHelper
- 9
include FormTagHelper
- 9
include JavaScriptHelper
- 9
include NumberHelper
- 9
include OutputSafetyHelper
- 9
include RenderingHelper
- 9
include SanitizeHelper
- 9
include TagHelper
- 9
include TextHelper
- 9
include TranslationHelper
- 9
include UrlHelper
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/module/attribute_accessors"
- 9
require "active_support/core_ext/enumerable"
- 9
module ActionView
# = Active Model Helpers
- 9
module Helpers #:nodoc:
- 9
module ActiveModelHelper
end
- 9
module ActiveModelInstanceTag
- 9
def object
@active_model_object ||= begin
object = super
object.respond_to?(:to_model) ? object.to_model : object
end
end
- 9
def content_tag(type, options, *)
select_markup_helper?(type) ? super : error_wrapping(super)
end
- 9
def tag(type, options, *)
tag_generate_errors?(options) ? error_wrapping(super) : super
end
- 9
def error_wrapping(html_tag)
if object_has_errors?
Base.field_error_proc.call(html_tag, self)
else
html_tag
end
end
- 9
def error_message
object.errors[@method_name]
end
- 9
private
- 9
def object_has_errors?
object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
end
- 9
def select_markup_helper?(type)
["optgroup", "option"].include?(type)
end
- 9
def tag_generate_errors?(options)
options["type"] != "hidden"
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/array/extract_options"
- 9
require "active_support/core_ext/hash/keys"
- 9
require "active_support/core_ext/object/inclusion"
- 9
require "action_view/helpers/asset_url_helper"
- 9
require "action_view/helpers/tag_helper"
- 9
module ActionView
# = Action View Asset Tag Helpers
- 9
module Helpers #:nodoc:
# This module provides methods for generating HTML that links views to assets such
# as images, JavaScripts, stylesheets, and feeds. These methods do not verify
# the assets exist before linking to them:
#
# image_tag("rails.png")
# # => <img src="/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
- 9
module AssetTagHelper
- 9
extend ActiveSupport::Concern
- 9
include AssetUrlHelper
- 9
include TagHelper
# Returns an HTML script tag for each of the +sources+ provided.
#
# Sources may be paths to JavaScript files. Relative paths are assumed to be relative
# to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document
# root. Relative paths are idiomatic, use absolute paths only when needed.
#
# When passing paths, the ".js" extension is optional. If you do not want ".js"
# appended to the path <tt>extname: false</tt> can be set on the options.
#
# You can modify the HTML attributes of the script tag by passing a hash as the
# last argument.
#
# When the Asset Pipeline is enabled, you can pass the name of your manifest as
# source, and include other JavaScript or CoffeeScript files inside the manifest.
#
# If the server supports Early Hints header links for these assets will be
# automatically pushed.
#
# ==== Options
#
# When the last parameter is a hash you can add HTML attributes using that
# parameter. The following options are supported:
#
# * <tt>:extname</tt> - Append an extension to the generated URL unless the extension
# already exists. This only applies for relative URLs.
# * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only
# applies when a relative URL and +host+ options are provided.
# * <tt>:host</tt> - When a relative URL is provided the host is added to the
# that path.
# * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline
# when it is set to true.
# * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if
# you have Content Security Policy enabled.
#
# ==== Examples
#
# javascript_include_tag "xmlhr"
# # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
#
# javascript_include_tag "xmlhr", host: "localhost", protocol: "https"
# # => <script src="https://localhost/assets/xmlhr.debug-1284139606.js"></script>
#
# javascript_include_tag "template.jst", extname: false
# # => <script src="/assets/template.debug-1284139606.jst"></script>
#
# javascript_include_tag "xmlhr.js"
# # => <script src="/assets/xmlhr.debug-1284139606.js"></script>
#
# javascript_include_tag "common.javascript", "/elsewhere/cools"
# # => <script src="/assets/common.javascript.debug-1284139606.js"></script>
# # <script src="/elsewhere/cools.debug-1284139606.js"></script>
#
# javascript_include_tag "http://www.example.com/xmlhr"
# # => <script src="http://www.example.com/xmlhr"></script>
#
# javascript_include_tag "http://www.example.com/xmlhr.js"
# # => <script src="http://www.example.com/xmlhr.js"></script>
#
# javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true
# # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script>
- 9
def javascript_include_tag(*sources)
options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys
preload_links = []
sources_tags = sources.uniq.map { |source|
href = path_to_javascript(source, path_options)
preload_links << "<#{href}>; rel=preload; as=script"
tag_options = {
"src" => href
}.merge!(options)
if tag_options["nonce"] == true
tag_options["nonce"] = content_security_policy_nonce
end
content_tag("script", "", tag_options)
}.join("\n").html_safe
send_preload_links_header(preload_links)
sources_tags
end
# Returns a stylesheet link tag for the sources specified as arguments. If
# you don't specify an extension, <tt>.css</tt> will be appended automatically.
# You can modify the link attributes by passing a hash as the last argument.
# For historical reasons, the 'media' attribute will always be present and defaults
# to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
# apply to all media types.
#
# If the server supports Early Hints header links for these assets will be
# automatically pushed.
#
# stylesheet_link_tag "style"
# # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
#
# stylesheet_link_tag "style.css"
# # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
#
# stylesheet_link_tag "http://www.example.com/style.css"
# # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />
#
# stylesheet_link_tag "style", media: "all"
# # => <link href="/assets/style.css" media="all" rel="stylesheet" />
#
# stylesheet_link_tag "style", media: "print"
# # => <link href="/assets/style.css" media="print" rel="stylesheet" />
#
# stylesheet_link_tag "random.styles", "/css/stylish"
# # => <link href="/assets/random.styles" media="screen" rel="stylesheet" />
# # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
- 9
def stylesheet_link_tag(*sources)
options = sources.extract_options!.stringify_keys
path_options = options.extract!("protocol", "host", "skip_pipeline").symbolize_keys
preload_links = []
sources_tags = sources.uniq.map { |source|
href = path_to_stylesheet(source, path_options)
preload_links << "<#{href}>; rel=preload; as=style"
tag_options = {
"rel" => "stylesheet",
"media" => "screen",
"href" => href
}.merge!(options)
tag(:link, tag_options)
}.join("\n").html_safe
send_preload_links_header(preload_links)
sources_tags
end
# Returns a link tag that browsers and feed readers can use to auto-detect
# an RSS, Atom, or JSON feed. The +type+ can be <tt>:rss</tt> (default),
# <tt>:atom</tt>, or <tt>:json</tt>. Control the link options in url_for format
# using the +url_options+. You can modify the LINK tag itself in +tag_options+.
#
# ==== Options
#
# * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
# * <tt>:type</tt> - Override the auto-generated mime type
# * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
#
# ==== Examples
#
# auto_discovery_link_tag
# # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
# auto_discovery_link_tag(:atom)
# # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
# auto_discovery_link_tag(:json)
# # => <link rel="alternate" type="application/json" title="JSON" href="http://www.currenthost.com/controller/action" />
# auto_discovery_link_tag(:rss, {action: "feed"})
# # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
# auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
# # => <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" />
# auto_discovery_link_tag(:rss, {controller: "news", action: "feed"})
# # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
# auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
# # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed.rss" />
- 9
def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
if !(type == :rss || type == :atom || type == :json) && tag_options[:type].blank?
raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss, :atom, or :json.")
end
tag(
"link",
"rel" => tag_options[:rel] || "alternate",
"type" => tag_options[:type] || Template::Types[type].to_s,
"title" => tag_options[:title] || type.to_s.upcase,
"href" => url_options.is_a?(Hash) ? url_for(url_options.merge(only_path: false)) : url_options
)
end
# Returns a link tag for a favicon managed by the asset pipeline.
#
# If a page has no link like the one generated by this helper, browsers
# ask for <tt>/favicon.ico</tt> automatically, and cache the file if the
# request succeeds. If the favicon changes it is hard to get it updated.
#
# To have better control applications may let the asset pipeline manage
# their favicon storing the file under <tt>app/assets/images</tt>, and
# using this helper to generate its corresponding link tag.
#
# The helper gets the name of the favicon file as first argument, which
# defaults to "favicon.ico", and also supports +:rel+ and +:type+ options
# to override their defaults, "shortcut icon" and "image/x-icon"
# respectively:
#
# favicon_link_tag
# # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" />
#
# favicon_link_tag 'myicon.ico'
# # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
#
# Mobile Safari looks for a different link tag, pointing to an image that
# will be used if you add the page to the home screen of an iOS device.
# The following call would generate such a tag:
#
# favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
# # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
- 9
def favicon_link_tag(source = "favicon.ico", options = {})
tag("link", {
rel: "shortcut icon",
type: "image/x-icon",
href: path_to_image(source, skip_pipeline: options.delete(:skip_pipeline))
}.merge!(options.symbolize_keys))
end
# Returns a link tag that browsers can use to preload the +source+.
# The +source+ can be the path of a resource managed by asset pipeline,
# a full path, or an URI.
#
# ==== Options
#
# * <tt>:type</tt> - Override the auto-generated mime type, defaults to the mime type for +source+ extension.
# * <tt>:as</tt> - Override the auto-generated value for as attribute, calculated using +source+ extension and mime type.
# * <tt>:crossorigin</tt> - Specify the crossorigin attribute, required to load cross-origin resources.
# * <tt>:nopush</tt> - Specify if the use of server push is not desired for the resource. Defaults to +false+.
#
# ==== Examples
#
# preload_link_tag("custom_theme.css")
# # => <link rel="preload" href="/assets/custom_theme.css" as="style" type="text/css" />
#
# preload_link_tag("/videos/video.webm")
# # => <link rel="preload" href="/videos/video.mp4" as="video" type="video/webm" />
#
# preload_link_tag(post_path(format: :json), as: "fetch")
# # => <link rel="preload" href="/posts.json" as="fetch" type="application/json" />
#
# preload_link_tag("worker.js", as: "worker")
# # => <link rel="preload" href="/assets/worker.js" as="worker" type="text/javascript" />
#
# preload_link_tag("//example.com/font.woff2")
# # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="anonymous"/>
#
# preload_link_tag("//example.com/font.woff2", crossorigin: "use-credentials")
# # => <link rel="preload" href="//example.com/font.woff2" as="font" type="font/woff2" crossorigin="use-credentials" />
#
# preload_link_tag("/media/audio.ogg", nopush: true)
# # => <link rel="preload" href="/media/audio.ogg" as="audio" type="audio/ogg" />
#
- 9
def preload_link_tag(source, options = {})
href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
extname = File.extname(source).downcase.delete(".")
mime_type = options.delete(:type) || Template::Types[extname]&.to_s
as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
crossorigin = options.delete(:crossorigin)
crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
nopush = options.delete(:nopush) || false
link_tag = tag.link(**{
rel: "preload",
href: href,
as: as_type,
type: mime_type,
crossorigin: crossorigin
}.merge!(options.symbolize_keys))
preload_link = "<#{href}>; rel=preload; as=#{as_type}"
preload_link += "; type=#{mime_type}" if mime_type
preload_link += "; crossorigin=#{crossorigin}" if crossorigin
preload_link += "; nopush" if nopush
send_preload_links_header([preload_link])
link_tag
end
# Returns an HTML image tag for the +source+. The +source+ can be a full
# path, a file, or an Active Storage attachment.
#
# ==== Options
#
# You can add HTML attributes using the +options+. The +options+ supports
# additional keys for convenience and conformance:
#
# * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
# width="30" and height="45", and "50" becomes width="50" and height="50".
# <tt>:size</tt> will be ignored if the value is not in the correct format.
# * <tt>:srcset</tt> - If supplied as a hash or array of <tt>[source, descriptor]</tt>
# pairs, each image path will be expanded before the list is formatted as a string.
#
# ==== Examples
#
# Assets (images that are part of your app):
#
# image_tag("icon")
# # => <img src="/assets/icon" />
# image_tag("icon.png")
# # => <img src="/assets/icon.png" />
# image_tag("icon.png", size: "16x10", alt: "Edit Entry")
# # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
# image_tag("/icons/icon.gif", size: "16")
# # => <img src="/icons/icon.gif" width="16" height="16" />
# image_tag("/icons/icon.gif", height: '32', width: '32')
# # => <img height="32" src="/icons/icon.gif" width="32" />
# image_tag("/icons/icon.gif", class: "menu_icon")
# # => <img class="menu_icon" src="/icons/icon.gif" />
# image_tag("/icons/icon.gif", data: { title: 'Rails Application' })
# # => <img data-title="Rails Application" src="/icons/icon.gif" />
# image_tag("icon.png", srcset: { "icon_2x.png" => "2x", "icon_4x.png" => "4x" })
# # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x">
# image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
# # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
#
# Active Storage blobs (images that are uploaded by the users of your app):
#
# image_tag(user.avatar)
# # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
# image_tag(user.avatar.variant(resize_to_limit: [100, 100]))
# # => <img src="/rails/active_storage/representations/.../tiger.jpg" />
# image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100')
# # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" />
- 9
def image_tag(source, options = {})
options = options.symbolize_keys
check_for_image_tag_errors(options)
skip_pipeline = options.delete(:skip_pipeline)
options[:src] = resolve_image_source(source, skip_pipeline)
if options[:srcset] && !options[:srcset].is_a?(String)
options[:srcset] = options[:srcset].map do |src_path, size|
src_path = path_to_image(src_path, skip_pipeline: skip_pipeline)
"#{src_path} #{size}"
end.join(", ")
end
options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
tag("img", options)
end
# Returns an HTML video tag for the +sources+. If +sources+ is a string,
# a single video tag will be returned. If +sources+ is an array, a video
# tag with nested source tags for each source will be returned. The
# +sources+ can be full paths or files that exist in your public videos
# directory.
#
# ==== Options
#
# When the last parameter is a hash you can add HTML attributes using that
# parameter. The following options are supported:
#
# * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
# before the video loads. The path is calculated like the +src+ of +image_tag+.
# * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
# width="30" and height="45", and "50" becomes width="50" and height="50".
# <tt>:size</tt> will be ignored if the value is not in the correct format.
# * <tt>:poster_skip_pipeline</tt> will bypass the asset pipeline when using
# the <tt>:poster</tt> option instead using an asset in the public folder.
#
# ==== Examples
#
# video_tag("trailer")
# # => <video src="/videos/trailer"></video>
# video_tag("trailer.ogg")
# # => <video src="/videos/trailer.ogg"></video>
# video_tag("trailer.ogg", controls: true, preload: 'none')
# # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video>
# video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
# # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
# video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true)
# # => <video src="/videos/trailer.m4v" width="16" height="10" poster="screenshot.png"></video>
# video_tag("/trailers/hd.avi", size: "16x16")
# # => <video src="/trailers/hd.avi" width="16" height="16"></video>
# video_tag("/trailers/hd.avi", size: "16")
# # => <video height="16" src="/trailers/hd.avi" width="16"></video>
# video_tag("/trailers/hd.avi", height: '32', width: '32')
# # => <video height="32" src="/trailers/hd.avi" width="32"></video>
# video_tag("trailer.ogg", "trailer.flv")
# # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
# video_tag(["trailer.ogg", "trailer.flv"])
# # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
# video_tag(["trailer.ogg", "trailer.flv"], size: "160x120")
# # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
- 9
def video_tag(*sources)
options = sources.extract_options!.symbolize_keys
public_poster_folder = options.delete(:poster_skip_pipeline)
sources << options
multiple_sources_tag_builder("video", sources) do |tag_options|
tag_options[:poster] = path_to_image(tag_options[:poster], skip_pipeline: public_poster_folder) if tag_options[:poster]
tag_options[:width], tag_options[:height] = extract_dimensions(tag_options.delete(:size)) if tag_options[:size]
end
end
# Returns an HTML audio tag for the +sources+. If +sources+ is a string,
# a single audio tag will be returned. If +sources+ is an array, an audio
# tag with nested source tags for each source will be returned. The
# +sources+ can be full paths or files that exist in your public audios
# directory.
#
# When the last parameter is a hash you can add HTML attributes using that
# parameter.
#
# audio_tag("sound")
# # => <audio src="/audios/sound"></audio>
# audio_tag("sound.wav")
# # => <audio src="/audios/sound.wav"></audio>
# audio_tag("sound.wav", autoplay: true, controls: true)
# # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
# audio_tag("sound.wav", "sound.mid")
# # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
- 9
def audio_tag(*sources)
multiple_sources_tag_builder("audio", sources)
end
- 9
private
- 9
def multiple_sources_tag_builder(type, sources)
options = sources.extract_options!.symbolize_keys
skip_pipeline = options.delete(:skip_pipeline)
sources.flatten!
yield options if block_given?
if sources.size > 1
content_tag(type, options) do
safe_join sources.map { |source| tag("source", src: send("path_to_#{type}", source, skip_pipeline: skip_pipeline)) }
end
else
options[:src] = send("path_to_#{type}", sources.first, skip_pipeline: skip_pipeline)
content_tag(type, nil, options)
end
end
- 9
def resolve_image_source(source, skip_pipeline)
if source.is_a?(Symbol) || source.is_a?(String)
path_to_image(source, skip_pipeline: skip_pipeline)
else
polymorphic_url(source)
end
rescue NoMethodError => e
raise ArgumentError, "Can't resolve image into URL: #{e}"
end
- 9
def extract_dimensions(size)
size = size.to_s
if /\A\d+x\d+\z/.match?(size)
size.split("x")
elsif /\A\d+\z/.match?(size)
[size, size]
end
end
- 9
def check_for_image_tag_errors(options)
if options[:size] && (options[:height] || options[:width])
raise ArgumentError, "Cannot pass a :size option with a :height or :width option"
end
end
- 9
def resolve_link_as(extname, mime_type)
if extname == "js"
"script"
elsif extname == "css"
"style"
elsif extname == "vtt"
"track"
elsif (type = mime_type.to_s.split("/")[0]) && type.in?(%w(audio video font))
type
end
end
- 9
def send_preload_links_header(preload_links)
if respond_to?(:request) && request
request.send_early_hints("Link" => preload_links.join("\n"))
end
if respond_to?(:response) && response
response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",")
end
end
end
end
end
# frozen_string_literal: true
- 9
require "zlib"
- 9
module ActionView
# = Action View Asset URL Helpers
- 9
module Helpers #:nodoc:
# This module provides methods for generating asset paths and
# URLs.
#
# image_path("rails.png")
# # => "/assets/rails.png"
#
# image_url("rails.png")
# # => "http://www.example.com/assets/rails.png"
#
# === Using asset hosts
#
# By default, Rails links to these assets on the current host in the public
# folder, but you can direct Rails to link to assets from a dedicated asset
# server by setting <tt>ActionController::Base.asset_host</tt> in the application
# configuration, typically in <tt>config/environments/production.rb</tt>.
# For example, you'd define <tt>assets.example.com</tt> to be your asset
# host this way, inside the <tt>configure</tt> block of your environment-specific
# configuration files or <tt>config/application.rb</tt>:
#
# config.action_controller.asset_host = "assets.example.com"
#
# Helpers take that into account:
#
# image_tag("rails.png")
# # => <img src="http://assets.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
# Browsers open a limited number of simultaneous connections to a single
# host. The exact number varies by browser and version. This limit may cause
# some asset downloads to wait for previous assets to finish before they can
# begin. You can use the <tt>%d</tt> wildcard in the +asset_host+ to
# distribute the requests over four hosts. For example,
# <tt>assets%d.example.com</tt> will spread the asset requests over
# "assets0.example.com", ..., "assets3.example.com".
#
# image_tag("rails.png")
# # => <img src="http://assets0.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
# This may improve the asset loading performance of your application.
# It is also possible the combination of additional connection overhead
# (DNS, SSL) and the overall browser connection limits may result in this
# solution being slower. You should be sure to measure your actual
# performance across targeted browsers both before and after this change.
#
# To implement the corresponding hosts you can either set up four actual
# hosts or use wildcard DNS to CNAME the wildcard to a single asset host.
# You can read more about setting up your DNS CNAME records from your ISP.
#
# Note: This is purely a browser performance optimization and is not meant
# for server load balancing. See https://www.die.net/musings/page_load_time/
# for background and https://www.browserscope.org/?category=network for
# connection limit data.
#
# Alternatively, you can exert more control over the asset host by setting
# +asset_host+ to a proc like this:
#
# ActionController::Base.asset_host = Proc.new { |source|
# "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com"
# }
# image_tag("rails.png")
# # => <img src="http://assets1.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
# The example above generates "http://assets1.example.com" and
# "http://assets2.example.com". This option is useful for example if
# you need fewer/more than four hosts, custom host names, etc.
#
# As you see the proc takes a +source+ parameter. That's a string with the
# absolute path of the asset, for example "/assets/rails.png".
#
# ActionController::Base.asset_host = Proc.new { |source|
# if source.end_with?('.css')
# "http://stylesheets.example.com"
# else
# "http://assets.example.com"
# end
# }
# image_tag("rails.png")
# # => <img src="http://assets.example.com/assets/rails.png" />
# stylesheet_link_tag("application")
# # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" />
#
# Alternatively you may ask for a second parameter +request+. That one is
# particularly useful for serving assets from an SSL-protected page. The
# example proc below disables asset hosting for HTTPS connections, while
# still sending assets for plain HTTP requests from asset hosts. If you don't
# have SSL certificates for each of the asset hosts this technique allows you
# to avoid warnings in the client about mixed media.
# Note that the +request+ parameter might not be supplied, e.g. when the assets
# are precompiled with the command `bin/rails assets:precompile`. Make sure to use a
# +Proc+ instead of a lambda, since a +Proc+ allows missing parameters and sets them
# to +nil+.
#
# config.action_controller.asset_host = Proc.new { |source, request|
# if request && request.ssl?
# "#{request.protocol}#{request.host_with_port}"
# else
# "#{request.protocol}assets.example.com"
# end
# }
#
# You can also implement a custom asset host object that responds to +call+
# and takes either one or two parameters just like the proc.
#
# config.action_controller.asset_host = AssetHostingWithMinimumSsl.new(
# "http://asset%d.example.com", "https://asset1.example.com"
# )
#
- 9
module AssetUrlHelper
- 9
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i
# This is the entry point for all assets.
# When using the asset pipeline (i.e. sprockets and sprockets-rails), the
# behavior is "enhanced". You can bypass the asset pipeline by passing in
# <tt>skip_pipeline: true</tt> to the options.
#
# All other asset *_path helpers delegate through this method.
#
# === With the asset pipeline
#
# All options passed to +asset_path+ will be passed to +compute_asset_path+
# which is implemented by sprockets-rails.
#
# asset_path("application.js") # => "/assets/application-60aa4fdc5cea14baf5400fba1abf4f2a46a5166bad4772b1effe341570f07de9.js"
# asset_path('application.js', host: 'example.com') # => "//example.com/assets/application.js"
# asset_path("application.js", host: 'example.com', protocol: 'https') # => "https://example.com/assets/application.js"
#
# === Without the asset pipeline (<tt>skip_pipeline: true</tt>)
#
# Accepts a <tt>type</tt> option that can specify the asset's extension. No error
# checking is done to verify the source passed into +asset_path+ is valid
# and that the file exists on disk.
#
# asset_path("application.js", skip_pipeline: true) # => "application.js"
# asset_path("filedoesnotexist.png", skip_pipeline: true) # => "filedoesnotexist.png"
# asset_path("application", type: :javascript, skip_pipeline: true) # => "/javascripts/application.js"
# asset_path("application", type: :stylesheet, skip_pipeline: true) # => "/stylesheets/application.css"
#
# === Options applying to all assets
#
# Below lists scenarios that apply to +asset_path+ whether or not you're
# using the asset pipeline.
#
# - All fully qualified URLs are returned immediately. This bypasses the
# asset pipeline and all other behavior described.
#
# asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js"
#
# - All assets that begin with a forward slash are assumed to be full
# URLs and will not be expanded. This will bypass the asset pipeline.
#
# asset_path("/foo.png") # => "/foo.png"
#
# - All blank strings will be returned immediately. This bypasses the
# asset pipeline and all other behavior described.
#
# asset_path("") # => ""
#
# - If <tt>config.relative_url_root</tt> is specified, all assets will have that
# root prepended.
#
# Rails.application.config.relative_url_root = "bar"
# asset_path("foo.js", skip_pipeline: true) # => "bar/foo.js"
#
# - A different asset host can be specified via <tt>config.action_controller.asset_host</tt>
# this is commonly used in conjunction with a CDN.
#
# Rails.application.config.action_controller.asset_host = "assets.example.com"
# asset_path("foo.js", skip_pipeline: true) # => "http://assets.example.com/foo.js"
#
# - An extension name can be specified manually with <tt>extname</tt>.
#
# asset_path("foo", skip_pipeline: true, extname: ".js") # => "/foo.js"
# asset_path("foo.css", skip_pipeline: true, extname: ".js") # => "/foo.css.js"
- 9
def asset_path(source, options = {})
raise ArgumentError, "nil is not a valid asset source" if source.nil?
source = source.to_s
return "" if source.blank?
return source if URI_REGEXP.match?(source)
tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "")
if extname = compute_asset_extname(source, options)
source = "#{source}#{extname}"
end
if source[0] != ?/
if options[:skip_pipeline]
source = public_compute_asset_path(source, options)
else
source = compute_asset_path(source, options)
end
end
relative_url_root = defined?(config.relative_url_root) && config.relative_url_root
if relative_url_root
source = File.join(relative_url_root, source) unless source.start_with?("#{relative_url_root}/")
end
if host = compute_asset_host(source, options)
source = File.join(host, source)
end
"#{source}#{tail}"
end
- 9
alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route
# Computes the full URL to an asset in the public directory. This
# will use +asset_path+ internally, so most of their behaviors
# will be the same. If :host options is set, it overwrites global
# +config.action_controller.asset_host+ setting.
#
# All other options provided are forwarded to +asset_path+ call.
#
# asset_url "application.js" # => http://example.com/assets/application.js
# asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js
#
- 9
def asset_url(source, options = {})
path_to_asset(source, options.merge(protocol: :request))
end
- 9
alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route
- 9
ASSET_EXTENSIONS = {
javascript: ".js",
stylesheet: ".css"
}
# Compute extname to append to asset path. Returns +nil+ if
# nothing should be added.
- 9
def compute_asset_extname(source, options = {})
return if options[:extname] == false
extname = options[:extname] || ASSET_EXTENSIONS[options[:type]]
if extname && File.extname(source) != extname
extname
else
nil
end
end
# Maps asset types to public directory.
- 9
ASSET_PUBLIC_DIRECTORIES = {
audio: "/audios",
font: "/fonts",
image: "/images",
javascript: "/javascripts",
stylesheet: "/stylesheets",
video: "/videos"
}
# Computes asset path to public directory. Plugins and
# extensions can override this method to point to custom assets
# or generate digested paths or query strings.
- 9
def compute_asset_path(source, options = {})
dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || ""
File.join(dir, source)
end
- 9
alias :public_compute_asset_path :compute_asset_path
# Pick an asset host for this source. Returns +nil+ if no host is set,
# the host if no wildcard is set, the host interpolated with the
# numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
# or the value returned from invoking call on an object responding to call
# (proc or otherwise).
- 9
def compute_asset_host(source = "", options = {})
request = self.request if respond_to?(:request)
host = options[:host]
host ||= config.asset_host if defined? config.asset_host
if host
if host.respond_to?(:call)
arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
args = [source]
args << request if request && (arity > 1 || arity < 0)
host = host.call(*args)
elsif host.include?("%d")
host = host % (Zlib.crc32(source) % 4)
end
end
host ||= request.base_url if request && options[:protocol] == :request
return unless host
if URI_REGEXP.match?(host)
host
else
protocol = options[:protocol] || config.default_asset_host_protocol || (request ? :request : :relative)
case protocol
when :relative
"//#{host}"
when :request
"#{request.protocol}#{host}"
else
"#{protocol}://#{host}"
end
end
end
# Computes the path to a JavaScript asset in the public javascripts directory.
# If the +source+ filename has no extension, .js will be appended (except for explicit URIs)
# Full paths from the document root will be passed through.
# Used internally by +javascript_include_tag+ to build the script path.
#
# javascript_path "xmlhr" # => /assets/xmlhr.js
# javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js
# javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
# javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr
# javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
- 9
def javascript_path(source, options = {})
path_to_asset(source, { type: :javascript }.merge!(options))
end
- 9
alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route
# Computes the full URL to a JavaScript asset in the public javascripts directory.
# This will use +javascript_path+ internally, so most of their behaviors will be the same.
# Since +javascript_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
# javascript_url "js/xmlhr.js", host: "http://stage.example.com" # => http://stage.example.com/assets/js/xmlhr.js
#
- 9
def javascript_url(source, options = {})
url_to_asset(source, { type: :javascript }.merge!(options))
end
- 9
alias_method :url_to_javascript, :javascript_url # aliased to avoid conflicts with a javascript_url named route
# Computes the path to a stylesheet asset in the public stylesheets directory.
# If the +source+ filename has no extension, .css will be appended (except for explicit URIs).
# Full paths from the document root will be passed through.
# Used internally by +stylesheet_link_tag+ to build the stylesheet path.
#
# stylesheet_path "style" # => /assets/style.css
# stylesheet_path "dir/style.css" # => /assets/dir/style.css
# stylesheet_path "/dir/style.css" # => /dir/style.css
# stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style
# stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css
- 9
def stylesheet_path(source, options = {})
path_to_asset(source, { type: :stylesheet }.merge!(options))
end
- 9
alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
# Computes the full URL to a stylesheet asset in the public stylesheets directory.
# This will use +stylesheet_path+ internally, so most of their behaviors will be the same.
# Since +stylesheet_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
# stylesheet_url "css/style.css", host: "http://stage.example.com" # => http://stage.example.com/assets/css/style.css
#
- 9
def stylesheet_url(source, options = {})
url_to_asset(source, { type: :stylesheet }.merge!(options))
end
- 9
alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route
# Computes the path to an image asset.
# Full paths from the document root will be passed through.
# Used internally by +image_tag+ to build the image path:
#
# image_path("edit") # => "/assets/edit"
# image_path("edit.png") # => "/assets/edit.png"
# image_path("icons/edit.png") # => "/assets/icons/edit.png"
# image_path("/icons/edit.png") # => "/icons/edit.png"
# image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png"
#
# If you have images as application resources this method may conflict with their named routes.
# The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and
# plugin authors are encouraged to do so.
- 9
def image_path(source, options = {})
path_to_asset(source, { type: :image }.merge!(options))
end
- 9
alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
# Computes the full URL to an image asset.
# This will use +image_path+ internally, so most of their behaviors will be the same.
# Since +image_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
# image_url "edit.png", host: "http://stage.example.com" # => http://stage.example.com/assets/edit.png
#
- 9
def image_url(source, options = {})
url_to_asset(source, { type: :image }.merge!(options))
end
- 9
alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route
# Computes the path to a video asset in the public videos directory.
# Full paths from the document root will be passed through.
# Used internally by +video_tag+ to build the video path.
#
# video_path("hd") # => /videos/hd
# video_path("hd.avi") # => /videos/hd.avi
# video_path("trailers/hd.avi") # => /videos/trailers/hd.avi
# video_path("/trailers/hd.avi") # => /trailers/hd.avi
# video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi
- 9
def video_path(source, options = {})
path_to_asset(source, { type: :video }.merge!(options))
end
- 9
alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route
# Computes the full URL to a video asset in the public videos directory.
# This will use +video_path+ internally, so most of their behaviors will be the same.
# Since +video_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
# video_url "hd.avi", host: "http://stage.example.com" # => http://stage.example.com/videos/hd.avi
#
- 9
def video_url(source, options = {})
url_to_asset(source, { type: :video }.merge!(options))
end
- 9
alias_method :url_to_video, :video_url # aliased to avoid conflicts with a video_url named route
# Computes the path to an audio asset in the public audios directory.
# Full paths from the document root will be passed through.
# Used internally by +audio_tag+ to build the audio path.
#
# audio_path("horse") # => /audios/horse
# audio_path("horse.wav") # => /audios/horse.wav
# audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav
# audio_path("/sounds/horse.wav") # => /sounds/horse.wav
# audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav
- 9
def audio_path(source, options = {})
path_to_asset(source, { type: :audio }.merge!(options))
end
- 9
alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route
# Computes the full URL to an audio asset in the public audios directory.
# This will use +audio_path+ internally, so most of their behaviors will be the same.
# Since +audio_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
# audio_url "horse.wav", host: "http://stage.example.com" # => http://stage.example.com/audios/horse.wav
#
- 9
def audio_url(source, options = {})
url_to_asset(source, { type: :audio }.merge!(options))
end
- 9
alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route
# Computes the path to a font asset.
# Full paths from the document root will be passed through.
#
# font_path("font") # => /fonts/font
# font_path("font.ttf") # => /fonts/font.ttf
# font_path("dir/font.ttf") # => /fonts/dir/font.ttf
# font_path("/dir/font.ttf") # => /dir/font.ttf
# font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf
- 9
def font_path(source, options = {})
path_to_asset(source, { type: :font }.merge!(options))
end
- 9
alias_method :path_to_font, :font_path # aliased to avoid conflicts with a font_path named route
# Computes the full URL to a font asset.
# This will use +font_path+ internally, so most of their behaviors will be the same.
# Since +font_url+ is based on +asset_url+ method you can set :host options. If :host
# options is set, it overwrites global +config.action_controller.asset_host+ setting.
#
# font_url "font.ttf", host: "http://stage.example.com" # => http://stage.example.com/fonts/font.ttf
#
- 9
def font_url(source, options = {})
url_to_asset(source, { type: :font }.merge!(options))
end
- 9
alias_method :url_to_font, :font_url # aliased to avoid conflicts with a font_url named route
end
end
end
# frozen_string_literal: true
- 9
require "set"
- 9
require "active_support/core_ext/symbol/starts_ends_with"
- 9
module ActionView
# = Action View Atom Feed Helpers
- 9
module Helpers #:nodoc:
- 9
module AtomFeedHelper
# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other
# template languages).
#
# Full usage example:
#
# config/routes.rb:
# Rails.application.routes.draw do
# resources :posts
# root to: "posts#index"
# end
#
# app/controllers/posts_controller.rb:
# class PostsController < ApplicationController
# # GET /posts.html
# # GET /posts.atom
# def index
# @posts = Post.all
#
# respond_to do |format|
# format.html
# format.atom
# end
# end
# end
#
# app/views/posts/index.atom.builder:
# atom_feed do |feed|
# feed.title("My great blog!")
# feed.updated(@posts[0].created_at) if @posts.length > 0
#
# @posts.each do |post|
# feed.entry(post) do |entry|
# entry.title(post.title)
# entry.content(post.body, type: 'html')
#
# entry.author do |author|
# author.name("DHH")
# end
# end
# end
# end
#
# The options for atom_feed are:
#
# * <tt>:language</tt>: Defaults to "en-US".
# * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
# * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
# * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case.
# * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
# created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
# 2005 is used (as an "I don't care" value).
# * <tt>:instruct</tt>: Hash of XML processing instructions in the form {target => {attribute => value, }} or {target => [{attribute => value, }, ]}
#
# Other namespaces can be added to the root element:
#
# app/views/posts/index.atom.builder:
# atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
# 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
# feed.title("My great blog!")
# feed.updated((@posts.first.created_at))
# feed.tag!('openSearch:totalResults', 10)
#
# @posts.each do |post|
# feed.entry(post) do |entry|
# entry.title(post.title)
# entry.content(post.body, type: 'html')
# entry.tag!('app:edited', Time.now)
#
# entry.author do |author|
# author.name("DHH")
# end
# end
# end
# end
#
# The Atom spec defines five elements (content rights title subtitle
# summary) which may directly contain xhtml content if type: 'xhtml'
# is specified as an attribute. If so, this helper will take care of
# the enclosing div and xhtml namespace declaration. Example usage:
#
# entry.summary type: 'xhtml' do |xhtml|
# xhtml.p pluralize(order.line_items.count, "line item")
# xhtml.p "Shipped to #{order.address}"
# xhtml.p "Paid by #{order.pay_type}"
# end
#
#
# <tt>atom_feed</tt> yields an +AtomFeedBuilder+ instance. Nested elements yield
# an +AtomBuilder+ instance.
- 9
def atom_feed(options = {}, &block)
if options[:schema_date]
options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime)
else
options[:schema_date] = "2005" # The Atom spec copyright date
end
xml = options.delete(:xml) || eval("xml", block.binding)
xml.instruct!
if options[:instruct]
options[:instruct].each do |target, attrs|
if attrs.respond_to?(:keys)
xml.instruct!(target, attrs)
elsif attrs.respond_to?(:each)
attrs.each { |attr_group| xml.instruct!(target, attr_group) }
end
end
end
feed_opts = { "xml:lang" => options[:language] || "en-US", "xmlns" => "http://www.w3.org/2005/Atom" }
feed_opts.merge!(options).select! { |k, _| k.start_with?("xml") }
xml.feed(feed_opts) do
xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}")
xml.link(rel: "alternate", type: "text/html", href: options[:root_url] || (request.protocol + request.host_with_port))
xml.link(rel: "self", type: "application/atom+xml", href: options[:url] || request.url)
yield AtomFeedBuilder.new(xml, self, options)
end
end
- 9
class AtomBuilder #:nodoc:
- 9
XHTML_TAG_NAMES = %w(content rights title subtitle summary).to_set
- 9
def initialize(xml)
@xml = xml
end
- 9
private
# Delegate to xml builder, first wrapping the element in an xhtml
# namespaced div element if the method and arguments indicate
# that an xhtml_block? is desired.
- 9
def method_missing(method, *arguments, &block)
if xhtml_block?(method, arguments)
@xml.__send__(method, *arguments) do
@xml.div(xmlns: "http://www.w3.org/1999/xhtml") do |xhtml|
block.call(xhtml)
end
end
else
@xml.__send__(method, *arguments, &block)
end
end
# True if the method name matches one of the five elements defined
# in the Atom spec as potentially containing XHTML content and
# if type: 'xhtml' is, in fact, specified.
- 9
def xhtml_block?(method, arguments)
if XHTML_TAG_NAMES.include?(method.to_s)
last = arguments.last
last.is_a?(Hash) && last[:type].to_s == "xhtml"
end
end
end
- 9
class AtomFeedBuilder < AtomBuilder #:nodoc:
- 9
def initialize(xml, view, feed_options = {})
@xml, @view, @feed_options = xml, view, feed_options
end
# Accepts a Date or Time object and inserts it in the proper format. If +nil+ is passed, current time in UTC is used.
- 9
def updated(date_or_time = nil)
@xml.updated((date_or_time || Time.now.utc).xmlschema)
end
# Creates an entry tag for a specific record and prefills the id using class and id.
#
# Options:
#
# * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
# * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
# * <tt>:url</tt>: The URL for this entry or +false+ or +nil+ for not having a link tag. Defaults to the +polymorphic_url+ for the record.
# * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
# * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html".
- 9
def entry(record, options = {})
@xml.entry do
@xml.id(options[:id] || "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}")
if options[:published] || (record.respond_to?(:created_at) && record.created_at)
@xml.published((options[:published] || record.created_at).xmlschema)
end
if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at)
@xml.updated((options[:updated] || record.updated_at).xmlschema)
end
type = options.fetch(:type, "text/html")
url = options.fetch(:url) { @view.polymorphic_url(record) }
@xml.link(rel: "alternate", type: type, href: url) if url
yield AtomBuilder.new(@xml)
end
end
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
# = Action View Cache Helper
- 9
module Helpers #:nodoc:
- 9
module CacheHelper
# This helper exposes a method for caching fragments of a view
# rather than an entire action or page. This technique is useful
# caching pieces like menus, lists of new topics, static HTML
# fragments, and so on. This method takes a block that contains
# the content you wish to cache.
#
# The best way to use this is by doing recyclable key-based cache expiration
# on top of a cache store like Memcached or Redis that'll automatically
# kick out old entries.
#
# When using this method, you list the cache dependency as the name of the cache, like so:
#
# <% cache project do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
#
# This approach will assume that when a new topic is added, you'll touch
# the project. The cache key generated from this call will be something like:
#
# views/template/action:7a1156131a6928cb0026877f8b749ac9/projects/123
# ^template path ^template tree digest ^class ^id
#
# This cache key is stable, but it's combined with a cache version derived from the project
# record. When the project updated_at is touched, the #cache_version changes, even
# if the key stays stable. This means that unlike a traditional key-based cache expiration
# approach, you won't be generating cache trash, unused keys, simply because the dependent
# record is updated.
#
# If your template cache depends on multiple sources (try to avoid this to keep things simple),
# you can name all these dependencies as part of an array:
#
# <% cache [ project, current_user ] do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
#
# This will include both records as part of the cache key and updating either of them will
# expire the cache.
#
# ==== \Template digest
#
# The template digest that's added to the cache key is computed by taking an MD5 of the
# contents of the entire template file. This ensures that your caches will automatically
# expire when you change the template file.
#
# Note that the MD5 is taken of the entire template file, not just what's within the
# cache do/end call. So it's possible that changing something outside of that call will
# still expire the cache.
#
# Additionally, the digestor will automatically look through your template file for
# explicit and implicit dependencies, and include those as part of the digest.
#
# The digestor can be bypassed by passing skip_digest: true as an option to the cache call:
#
# <% cache project, skip_digest: true do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
#
# ==== Implicit dependencies
#
# Most template dependencies can be derived from calls to render in the template itself.
# Here are some examples of render calls that Cache Digests knows how to decode:
#
# render partial: "comments/comment", collection: commentable.comments
# render "comments/comments"
# render 'comments/comments'
# render('comments/comments')
#
# render "header" translates to render("comments/header")
#
# render(@topic) translates to render("topics/topic")
# render(topics) translates to render("topics/topic")
# render(message.topics) translates to render("topics/topic")
#
# It's not possible to derive all render calls like that, though.
# Here are a few examples of things that can't be derived:
#
# render group_of_attachments
# render @project.documents.where(published: true).order('created_at')
#
# You will have to rewrite those to the explicit form:
#
# render partial: 'attachments/attachment', collection: group_of_attachments
# render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
#
# === Explicit dependencies
#
# Sometimes you'll have template dependencies that can't be derived at all. This is typically
# the case when you have template rendering that happens in helpers. Here's an example:
#
# <%= render_sortable_todolists @project.todolists %>
#
# You'll need to use a special comment format to call those out:
#
# <%# Template Dependency: todolists/todolist %>
# <%= render_sortable_todolists @project.todolists %>
#
# In some cases, like a single table inheritance setup, you might have
# a bunch of explicit dependencies. Instead of writing every template out,
# you can use a wildcard to match any template in a directory:
#
# <%# Template Dependency: events/* %>
# <%= render_categorizable_events @person.events %>
#
# This marks every template in the directory as a dependency. To find those
# templates, the wildcard path must be absolutely defined from <tt>app/views</tt> or paths
# otherwise added with +prepend_view_path+ or +append_view_path+.
# This way the wildcard for <tt>app/views/recordings/events</tt> would be <tt>recordings/events/*</tt> etc.
#
# The pattern used to match explicit dependencies is <tt>/# Template Dependency: (\S+)/</tt>,
# so it's important that you type it out just so.
# You can only declare one template dependency per line.
#
# === External dependencies
#
# If you use a helper method, for example, inside a cached block and
# you then update that helper, you'll have to bump the cache as well.
# It doesn't really matter how you do it, but the MD5 of the template file
# must change. One recommendation is to simply be explicit in a comment, like:
#
# <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
# <%= some_helper_method(person) %>
#
# Now all you have to do is change that timestamp when the helper method changes.
#
# === Collection Caching
#
# When rendering a collection of objects that each use the same partial, a <tt>:cached</tt>
# option can be passed.
#
# For collections rendered such:
#
# <%= render partial: 'projects/project', collection: @projects, cached: true %>
#
# The <tt>cached: true</tt> will make Action View's rendering read several templates
# from cache at once instead of one call per template.
#
# Templates in the collection not already cached are written to cache.
#
# Works great alongside individual template fragment caching.
# For instance if the template the collection renders is cached like:
#
# # projects/_project.html.erb
# <% cache project do %>
# <%# ... %>
# <% end %>
#
# Any collection renders will find those cached templates when attempting
# to read multiple templates at once.
#
# If your collection cache depends on multiple sources (try to avoid this to keep things simple),
# you can name all these dependencies as part of a block that returns an array:
#
# <%= render partial: 'projects/project', collection: @projects, cached: -> project { [ project, current_user ] } %>
#
# This will include both records as part of the cache key and updating either of them will
# expire the cache.
- 9
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
name_options = options.slice(:skip_digest)
safe_concat(fragment_for(cache_fragment_name(name, **name_options), options, &block))
else
yield
end
nil
end
# Cache fragments of a view if +condition+ is true
#
# <% cache_if admin?, project do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
- 9
def cache_if(condition, name = {}, options = {}, &block)
if condition
cache(name, options, &block)
else
yield
end
nil
end
# Cache fragments of a view unless +condition+ is true
#
# <% cache_unless admin?, project do %>
# <b>All the topics on this project</b>
# <%= render project.topics %>
# <% end %>
- 9
def cache_unless(condition, name = {}, options = {}, &block)
cache_if !condition, name, options, &block
end
# This helper returns the name of a cache key for a given fragment cache
# call. By supplying <tt>skip_digest: true</tt> to cache, the digestion of cache
# fragments can be manually bypassed. This is useful when cache fragments
# cannot be manually expired unless you know the exact key which is the
# case when using memcached.
- 9
def cache_fragment_name(name = {}, skip_digest: nil, digest_path: nil)
if skip_digest
name
else
fragment_name_with_digest(name, digest_path)
end
end
- 9
def digest_path_from_template(template) # :nodoc:
digest = Digestor.digest(name: template.virtual_path, format: template.format, finder: lookup_context, dependencies: view_cache_dependencies)
if digest.present?
"#{template.virtual_path}:#{digest}"
else
template.virtual_path
end
end
- 9
private
- 9
def fragment_name_with_digest(name, digest_path)
name = controller.url_for(name).split("://").last if name.is_a?(Hash)
if @current_template&.virtual_path || digest_path
digest_path ||= digest_path_from_template(@current_template)
[ digest_path, name ]
else
name
end
end
- 9
def fragment_for(name = {}, options = nil, &block)
if content = read_fragment_for(name, options)
@view_renderer.cache_hits[@current_template&.virtual_path] = :hit if defined?(@view_renderer)
content
else
@view_renderer.cache_hits[@current_template&.virtual_path] = :miss if defined?(@view_renderer)
write_fragment_for(name, options, &block)
end
end
- 9
def read_fragment_for(name, options)
controller.read_fragment(name, options)
end
- 9
def write_fragment_for(name, options)
pos = output_buffer.length
yield
output_safe = output_buffer.html_safe?
fragment = output_buffer.slice!(pos..-1)
if output_safe
self.output_buffer = output_buffer.class.new(output_buffer)
end
controller.write_fragment(name, fragment, options)
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/string/output_safety"
- 9
module ActionView
# = Action View Capture Helper
- 9
module Helpers #:nodoc:
# CaptureHelper exposes methods to let you extract generated markup which
# can be used in other parts of a template or layout file.
#
# It provides a method to capture blocks into variables through capture and
# a way to capture a block of markup for use in a layout through {content_for}[rdoc-ref:ActionView::Helpers::CaptureHelper#content_for].
- 9
module CaptureHelper
# The capture method extracts part of a template as a String object.
# You can then use this object anywhere in your templates, layout, or helpers.
#
# The capture method can be used in ERB templates...
#
# <% @greeting = capture do %>
# Welcome to my shiny new web page! The date and time is
# <%= Time.now %>
# <% end %>
#
# ...and Builder (RXML) templates.
#
# @timestamp = capture do
# "The current timestamp is #{Time.now}."
# end
#
# You can then use that variable anywhere else. For example:
#
# <html>
# <head><title><%= @greeting %></title></head>
# <body>
# <b><%= @greeting %></b>
# </body>
# </html>
#
# The return of capture is the string generated by the block. For Example:
#
# @greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500"
#
- 9
def capture(*args)
value = nil
buffer = with_output_buffer { value = yield(*args) }
if (string = buffer.presence || value) && string.is_a?(String)
ERB::Util.html_escape string
end
end
# Calling <tt>content_for</tt> stores a block of markup in an identifier for later use.
# In order to access this stored content in other templates, helper modules
# or the layout, you would pass the identifier as an argument to <tt>content_for</tt>.
#
# Note: <tt>yield</tt> can still be used to retrieve the stored content, but calling
# <tt>yield</tt> doesn't work in helper modules, while <tt>content_for</tt> does.
#
# <% content_for :not_authorized do %>
# alert('You are not authorized to do that!')
# <% end %>
#
# You can then use <tt>content_for :not_authorized</tt> anywhere in your templates.
#
# <%= content_for :not_authorized if current_user.nil? %>
#
# This is equivalent to:
#
# <%= yield :not_authorized if current_user.nil? %>
#
# <tt>content_for</tt>, however, can also be used in helper modules.
#
# module StorageHelper
# def stored_content
# content_for(:storage) || "Your storage is empty"
# end
# end
#
# This helper works just like normal helpers.
#
# <%= stored_content %>
#
# You can also use the <tt>yield</tt> syntax alongside an existing call to
# <tt>yield</tt> in a layout. For example:
#
# <%# This is the layout %>
# <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
# <head>
# <title>My Website</title>
# <%= yield :script %>
# </head>
# <body>
# <%= yield %>
# </body>
# </html>
#
# And now, we'll create a view that has a <tt>content_for</tt> call that
# creates the <tt>script</tt> identifier.
#
# <%# This is our view %>
# Please login!
#
# <% content_for :script do %>
# <script>alert('You are not authorized to view this page!')</script>
# <% end %>
#
# Then, in another view, you could to do something like this:
#
# <%= link_to 'Logout', action: 'logout', remote: true %>
#
# <% content_for :script do %>
# <%= javascript_include_tag :defaults %>
# <% end %>
#
# That will place +script+ tags for your default set of JavaScript files on the page;
# this technique is useful if you'll only be using these scripts in a few views.
#
# Note that <tt>content_for</tt> concatenates (default) the blocks it is given for a particular
# identifier in order. For example:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Home', action: 'index' %></li>
# <% end %>
#
# And in another place:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Login', action: 'login' %></li>
# <% end %>
#
# Then, in another template or layout, this code would render both links in order:
#
# <ul><%= content_for :navigation %></ul>
#
# If the flush parameter is +true+ <tt>content_for</tt> replaces the blocks it is given. For example:
#
# <% content_for :navigation do %>
# <li><%= link_to 'Home', action: 'index' %></li>
# <% end %>
#
# <%# Add some other content, or use a different template: %>
#
# <% content_for :navigation, flush: true do %>
# <li><%= link_to 'Login', action: 'login' %></li>
# <% end %>
#
# Then, in another template or layout, this code would render only the last link:
#
# <ul><%= content_for :navigation %></ul>
#
# Lastly, simple content can be passed as a parameter:
#
# <% content_for :script, javascript_include_tag(:defaults) %>
#
# WARNING: <tt>content_for</tt> is ignored in caches. So you shouldn't use it for elements that will be fragment cached.
- 9
def content_for(name, content = nil, options = {}, &block)
if content || block_given?
if block_given?
options = content if content
content = capture(&block)
end
if content
options[:flush] ? @view_flow.set(name, content) : @view_flow.append(name, content)
end
nil
else
@view_flow.get(name).presence
end
end
# The same as +content_for+ but when used with streaming flushes
# straight back to the layout. In other words, if you want to
# concatenate several times to the same buffer when rendering a given
# template, you should use +content_for+, if not, use +provide+ to tell
# the layout to stop looking for more contents.
- 9
def provide(name, content = nil, &block)
content = capture(&block) if block_given?
result = @view_flow.append!(name, content) if content
result unless content
end
# <tt>content_for?</tt> checks whether any content has been captured yet using <tt>content_for</tt>.
# Useful to render parts of your layout differently based on what is in your views.
#
# <%# This is the layout %>
# <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
# <head>
# <title>My Website</title>
# <%= yield :script %>
# </head>
# <body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>">
# <%= yield %>
# <%= yield :right_col %>
# </body>
# </html>
- 9
def content_for?(name)
@view_flow.get(name).present?
end
# Use an alternate output buffer for the duration of the block.
# Defaults to a new empty string.
- 9
def with_output_buffer(buf = nil) #:nodoc:
unless buf
buf = ActionView::OutputBuffer.new
if output_buffer && output_buffer.respond_to?(:encoding)
buf.force_encoding(output_buffer.encoding)
end
end
self.output_buffer, old_buffer = buf, output_buffer
yield
output_buffer
ensure
self.output_buffer = old_buffer
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/module/attr_internal"
- 9
module ActionView
- 9
module Helpers #:nodoc:
# This module keeps all methods and behavior in ActionView
# that simply delegates to the controller.
- 9
module ControllerHelper #:nodoc:
- 9
attr_internal :controller, :request
- 9
CONTROLLER_DELEGATES = [:request_forgery_protection_token, :params,
:session, :cookies, :response, :headers, :flash, :action_name,
:controller_name, :controller_path]
- 9
delegate(*CONTROLLER_DELEGATES, to: :controller)
- 9
def assign_controller(controller)
if @_controller = controller
@_request = controller.request if controller.respond_to?(:request)
@_config = controller.config.inheritable_copy if controller.respond_to?(:config)
@_default_form_builder = controller.default_form_builder if controller.respond_to?(:default_form_builder)
end
end
- 9
def logger
controller.logger if controller.respond_to?(:logger)
end
- 9
def respond_to?(method_name, include_private = false)
return controller.respond_to?(method_name) if CONTROLLER_DELEGATES.include?(method_name.to_sym)
super
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
# = Action View CSP Helper
- 9
module Helpers #:nodoc:
- 9
module CspHelper
# Returns a meta tag "csp-nonce" with the per-session nonce value
# for allowing inline <script> tags.
#
# <head>
# <%= csp_meta_tag %>
# </head>
#
# This is used by the Rails UJS helper to create dynamically
# loaded inline <script> elements.
#
- 9
def csp_meta_tag(**options)
if content_security_policy?
options[:name] = "csp-nonce"
options[:content] = content_security_policy_nonce
tag("meta", options)
end
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
# = Action View CSRF Helper
- 9
module Helpers #:nodoc:
- 9
module CsrfHelper
# Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
# request forgery protection parameter and token, respectively.
#
# <head>
# <%= csrf_meta_tags %>
# </head>
#
# These are used to generate the dynamic forms that implement non-remote links with
# <tt>:method</tt>.
#
# You don't need to use these tags for regular forms as they generate their own hidden fields.
#
# For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the
# "X-CSRF-Token" HTTP header. If you are using rails-ujs this happens automatically.
#
- 9
def csrf_meta_tags
if defined?(protect_against_forgery?) && protect_against_forgery?
[
tag("meta", name: "csrf-param", content: request_forgery_protection_token),
tag("meta", name: "csrf-token", content: form_authenticity_token)
].join("\n").html_safe
end
end
# For backwards compatibility.
- 9
alias csrf_meta_tag csrf_meta_tags
end
end
end
# frozen_string_literal: true
- 9
require "date"
- 9
require "action_view/helpers/tag_helper"
- 9
require "active_support/core_ext/array/extract_options"
- 9
require "active_support/core_ext/date/conversions"
- 9
require "active_support/core_ext/hash/slice"
- 9
require "active_support/core_ext/object/acts_like"
- 9
require "active_support/core_ext/object/with_options"
- 9
module ActionView
- 9
module Helpers #:nodoc:
# = Action View Date Helpers
#
# The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time
# elements. All of the select-type methods share a number of common options that are as follows:
#
# * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday"
# would give \birthday[month] instead of \date[month] if passed to the <tt>select_month</tt> method.
# * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
# * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true,
# the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead
# of \date[month].
- 9
module DateHelper
- 9
MINUTES_IN_YEAR = 525600
- 9
MINUTES_IN_QUARTER_YEAR = 131400
- 9
MINUTES_IN_THREE_QUARTERS_YEAR = 394200
# Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds.
# Pass <tt>include_seconds: true</tt> if you want more detailed approximations when distance < 1 min, 29 secs.
# Distances are reported based on the following table:
#
# 0 <-> 29 secs # => less than a minute
# 30 secs <-> 1 min, 29 secs # => 1 minute
# 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
# 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
# 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
# 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day
# 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
# 29 days, 23 hrs, 59 mins, 30 secs <-> 44 days, 23 hrs, 59 mins, 29 secs # => about 1 month
# 44 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 2 months
# 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
# 1 yr <-> 1 yr, 3 months # => about 1 year
# 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year
# 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years
# 2 yrs <-> max time or date # => (same rules as 1 yr)
#
# With <tt>include_seconds: true</tt> and the difference < 1 minute 29 seconds:
# 0-4 secs # => less than 5 seconds
# 5-9 secs # => less than 10 seconds
# 10-19 secs # => less than 20 seconds
# 20-39 secs # => half a minute
# 40-59 secs # => less than a minute
# 60-89 secs # => 1 minute
#
# from_time = Time.now
# distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
# distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
# distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
# distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds
# distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years
# distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days
# distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute
# distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute
# distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
# distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
# distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
# distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
#
# to_time = Time.now + 6.years + 19.days
# distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years
# distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years
# distance_of_time_in_words(Time.now, Time.now) # => less than a minute
#
# With the <tt>scope</tt> option, you can define a custom scope for Rails
# to look up the translation.
#
# For example you can define the following in your locale (e.g. en.yml).
#
# datetime:
# distance_in_words:
# short:
# about_x_hours:
# one: 'an hour'
# other: '%{count} hours'
#
# See https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/en.yml
# for more examples.
#
# Which will then result in the following:
#
# from_time = Time.now
# distance_of_time_in_words(from_time, from_time + 50.minutes, scope: 'datetime.distance_in_words.short') # => "an hour"
# distance_of_time_in_words(from_time, from_time + 3.hours, scope: 'datetime.distance_in_words.short') # => "3 hours"
- 9
def distance_of_time_in_words(from_time, to_time = 0, options = {})
options = {
scope: :'datetime.distance_in_words'
}.merge!(options)
from_time = normalize_distance_of_time_argument_to_time(from_time)
to_time = normalize_distance_of_time_argument_to_time(to_time)
from_time, to_time = to_time, from_time if from_time > to_time
distance_in_minutes = ((to_time - from_time) / 60.0).round
distance_in_seconds = (to_time - from_time).round
I18n.with_options locale: options[:locale], scope: options[:scope] do |locale|
case distance_in_minutes
when 0..1
return distance_in_minutes == 0 ?
locale.t(:less_than_x_minutes, count: 1) :
locale.t(:x_minutes, count: distance_in_minutes) unless options[:include_seconds]
case distance_in_seconds
when 0..4 then locale.t :less_than_x_seconds, count: 5
when 5..9 then locale.t :less_than_x_seconds, count: 10
when 10..19 then locale.t :less_than_x_seconds, count: 20
when 20..39 then locale.t :half_a_minute
when 40..59 then locale.t :less_than_x_minutes, count: 1
else locale.t :x_minutes, count: 1
end
when 2...45 then locale.t :x_minutes, count: distance_in_minutes
when 45...90 then locale.t :about_x_hours, count: 1
# 90 mins up to 24 hours
when 90...1440 then locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round
# 24 hours up to 42 hours
when 1440...2520 then locale.t :x_days, count: 1
# 42 hours up to 30 days
when 2520...43200 then locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round
# 30 days up to 60 days
when 43200...86400 then locale.t :about_x_months, count: (distance_in_minutes.to_f / 43200.0).round
# 60 days up to 365 days
when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round
else
from_year = from_time.year
from_year += 1 if from_time.month >= 3
to_year = to_time.year
to_year -= 1 if to_time.month < 3
leap_years = (from_year > to_year) ? 0 : (from_year..to_year).count { |x| Date.leap?(x) }
minute_offset_for_leap_year = leap_years * 1440
# Discount the leap year days when calculating year distance.
# e.g. if there are 20 leap year days between 2 dates having the same day
# and month then the based on 365 days calculation
# the distance in years will come out to over 80 years when in written
# English it would read better as about 80 years.
minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
remainder = (minutes_with_offset % MINUTES_IN_YEAR)
distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR)
if remainder < MINUTES_IN_QUARTER_YEAR
locale.t(:about_x_years, count: distance_in_years)
elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR
locale.t(:over_x_years, count: distance_in_years)
else
locale.t(:almost_x_years, count: distance_in_years + 1)
end
end
end
end
# Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
#
# time_ago_in_words(3.minutes.from_now) # => 3 minutes
# time_ago_in_words(3.minutes.ago) # => 3 minutes
# time_ago_in_words(Time.now - 15.hours) # => about 15 hours
# time_ago_in_words(Time.now) # => less than a minute
# time_ago_in_words(Time.now, include_seconds: true) # => less than 5 seconds
#
# from_time = Time.now - 3.days - 14.minutes - 25.seconds
# time_ago_in_words(from_time) # => 3 days
#
# from_time = (3.days + 14.minutes + 25.seconds).ago
# time_ago_in_words(from_time) # => 3 days
#
# Note that you cannot pass a <tt>Numeric</tt> value to <tt>time_ago_in_words</tt>.
#
- 9
def time_ago_in_words(from_time, options = {})
distance_of_time_in_words(from_time, Time.now, options)
end
- 9
alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
# Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based
# attribute (identified by +method+) on an object assigned to the template (identified by +object+).
#
# ==== Options
# * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
# "2" instead of "February").
# * <tt>:use_two_digit_numbers</tt> - Set to true if you want to display two digit month and day numbers (e.g.
# "02" instead of "February" and "08" instead of "8").
# * <tt>:use_short_month</tt> - Set to true if you want to use abbreviated month names instead of full
# month names (e.g. "Feb" instead of "February").
# * <tt>:add_month_numbers</tt> - Set to true if you want to use both month numbers and month names (e.g.
# "2 - February" instead of "February").
# * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names.
# Note: You can also use Rails' i18n functionality for this.
# * <tt>:month_format_string</tt> - Set to a format string. The string gets passed keys +:number+ (integer)
# and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example.
# See <tt>Kernel.sprintf</tt> for documentation on format sequences.
# * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
# * <tt>:time_separator</tt> - Specifies a string to separate the time fields. Default is " : ".
# * <tt>:datetime_separator</tt>- Specifies a string to separate the date and time fields. Default is " — ".
# * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if
# you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to
# the current selected year minus 5.
# * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if
# you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to
# the current selected year plus 5.
# * <tt>:year_format</tt> - Set format of years for year select. Lambda should be passed.
# * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
# as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
# first of the given month in order to not create invalid dates like 31 February.
# * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month
# as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true.
# * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year
# as a hidden field instead of showing a select field.
# * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> to
# customize the order in which the select fields are shown. If you leave out any of the symbols, the respective
# select will not be shown (like when you set <tt>discard_xxx: true</tt>. Defaults to the order defined in
# the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails).
# * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty
# dates.
# * <tt>:default</tt> - Set a default date if the affected date isn't set or is +nil+.
# * <tt>:selected</tt> - Set a date that overrides the actual value.
# * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
# * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings
# for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>.
# Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds)
# or the given prompt string.
# * <tt>:with_css_classes</tt> - Set to true or a hash of strings. Use true if you want to assign generic styles for
# select tags. This automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second'. A hash of
# strings for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt>, <tt>:second</tt>
# will extend the select type with the given value. Use +html_options+ to modify every select tag in the set.
# * <tt>:use_hidden</tt> - Set to true if you only want to generate hidden input tags.
#
# If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
#
# NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed.
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute.
# date_select("article", "written_on")
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
# # with the year in the year drop down box starting at 1995.
# date_select("article", "written_on", start_year: 1995)
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
# # with the year in the year drop down box starting at 1995, numbers used for months instead of words,
# # and without a day select box.
# date_select("article", "written_on", start_year: 1995, use_month_numbers: true,
# discard_day: true, include_blank: true)
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
# # with two digit numbers used for months and days.
# date_select("article", "written_on", use_two_digit_numbers: true)
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
# # with the fields ordered as day, month, year rather than month, day, year.
# date_select("article", "written_on", order: [:day, :month, :year])
#
# # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
# # lacking a year field.
# date_select("user", "birthday", order: [:month, :day])
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
# # which is initially set to the date 3 days from the current date
# date_select("article", "written_on", default: 3.days.from_now)
#
# # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
# # which is set in the form with today's date, regardless of the value in the Active Record object.
# date_select("article", "written_on", selected: Date.today)
#
# # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute
# # that will have a default day of 20.
# date_select("credit_card", "bill_due", default: { day: 20 })
#
# # Generates a date select with custom prompts.
# date_select("article", "written_on", prompt: { day: 'Select day', month: 'Select month', year: 'Select year' })
#
# # Generates a date select with custom year format.
# date_select("article", "written_on", year_format: ->(year) { "Heisei #{year - 1988}" })
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
# all month choices are valid.
- 9
def date_select(object_name, method, options = {}, html_options = {})
Tags::DateSelect.new(object_name, method, self, options, html_options).render
end
# Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
# specified time-based attribute (identified by +method+) on an object assigned to the template (identified by
# +object+). You can include the seconds with <tt>:include_seconds</tt>. You can get hours in the AM/PM format
# with <tt>:ampm</tt> option.
#
# This method will also generate 3 input hidden tags, for the actual year, month and day unless the option
# <tt>:ignore_date</tt> is set to +true+. If you set the <tt>:ignore_date</tt> to +true+, you must have a
# +date_select+ on the same method within the form otherwise an exception will be raised.
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
# # Creates a time select tag that, when POSTed, will be stored in the article variable in the sunrise attribute.
# time_select("article", "sunrise")
#
# # Creates a time select tag with a seconds field that, when POSTed, will be stored in the article variables in
# # the sunrise attribute.
# time_select("article", "start_time", include_seconds: true)
#
# # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45.
# time_select 'game', 'game_time', { minute_step: 15 }
#
# # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
# time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' })
# time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
# time_select("article", "written_on", prompt: true) # generic prompts for all
#
# # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM.
# time_select 'game', 'game_time', { ampm: true }
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
#
# Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
# all month choices are valid.
- 9
def time_select(object_name, method, options = {}, html_options = {})
Tags::TimeSelect.new(object_name, method, self, options, html_options).render
end
# Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
# specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified
# by +object+).
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
# # Generates a datetime select that, when POSTed, will be stored in the article variable in the written_on
# # attribute.
# datetime_select("article", "written_on")
#
# # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
# # article variable in the written_on attribute.
# datetime_select("article", "written_on", start_year: 1995)
#
# # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will
# # be stored in the trip variable in the departing attribute.
# datetime_select("trip", "departing", default: 3.days.from_now)
#
# # Generate a datetime select with hours in the AM/PM format
# datetime_select("article", "written_on", ampm: true)
#
# # Generates a datetime select that discards the type that, when POSTed, will be stored in the article variable
# # as the written_on attribute.
# datetime_select("article", "written_on", discard_type: true)
#
# # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
# datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
# datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours
# datetime_select("article", "written_on", prompt: true) # generic prompts for all
#
# The selects are prepared for multi-parameter assignment to an Active Record object.
- 9
def datetime_select(object_name, method, options = {}, html_options = {})
Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render
end
# Returns a set of HTML select-tags (one for year, month, day, hour, minute, and second) pre-selected with the
# +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
# an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
# supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
# <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to
# control visual display of the elements.
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
# my_date_time = Time.now + 4.days
#
# # Generates a datetime select that defaults to the datetime in my_date_time (four days after today).
# select_datetime(my_date_time)
#
# # Generates a datetime select that defaults to today (no specified datetime)
# select_datetime()
#
# # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
# # with the fields ordered year, month, day rather than month, day, year.
# select_datetime(my_date_time, order: [:year, :month, :day])
#
# # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
# # with a '/' between each date field.
# select_datetime(my_date_time, date_separator: '/')
#
# # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
# # with a date fields separated by '/', time fields separated by '' and the date and time fields
# # separated by a comma (',').
# select_datetime(my_date_time, date_separator: '/', time_separator: '', datetime_separator: ',')
#
# # Generates a datetime select that discards the type of the field and defaults to the datetime in
# # my_date_time (four days after today)
# select_datetime(my_date_time, discard_type: true)
#
# # Generate a datetime field with hours in the AM/PM format
# select_datetime(my_date_time, ampm: true)
#
# # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
# # prefixed with 'payday' rather than 'date'
# select_datetime(my_date_time, prefix: 'payday')
#
# # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
# select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
# select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours
# select_datetime(my_date_time, prompt: true) # generic prompts for all
- 9
def select_datetime(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_datetime
end
# Returns a set of HTML select-tags (one for year, month, and day) pre-selected with the +date+.
# It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
# symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order.
# If the array passed to the <tt>:order</tt> option does not contain all the three symbols, all tags will be hidden.
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
# my_date = Time.now + 6.days
#
# # Generates a date select that defaults to the date in my_date (six days after today).
# select_date(my_date)
#
# # Generates a date select that defaults to today (no specified date).
# select_date()
#
# # Generates a date select that defaults to the date in my_date (six days after today)
# # with the fields ordered year, month, day rather than month, day, year.
# select_date(my_date, order: [:year, :month, :day])
#
# # Generates a date select that discards the type of the field and defaults to the date in
# # my_date (six days after today).
# select_date(my_date, discard_type: true)
#
# # Generates a date select that defaults to the date in my_date,
# # which has fields separated by '/'.
# select_date(my_date, date_separator: '/')
#
# # Generates a date select that defaults to the datetime in my_date (six days after today)
# # prefixed with 'payday' rather than 'date'.
# select_date(my_date, prefix: 'payday')
#
# # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
# select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
# select_date(my_date, prompt: { hour: true }) # generic prompt for hours
# select_date(my_date, prompt: true) # generic prompts for all
- 9
def select_date(date = Date.current, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_date
end
# Returns a set of HTML select-tags (one for hour and minute).
# You can set <tt>:time_separator</tt> key to format the output, and
# the <tt>:include_seconds</tt> option to include an input for seconds.
#
# If anything is passed in the html_options hash it will be applied to every select tag in the set.
#
# my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds
#
# # Generates a time select that defaults to the time in my_time.
# select_time(my_time)
#
# # Generates a time select that defaults to the current time (no specified time).
# select_time()
#
# # Generates a time select that defaults to the time in my_time,
# # which has fields separated by ':'.
# select_time(my_time, time_separator: ':')
#
# # Generates a time select that defaults to the time in my_time,
# # that also includes an input for seconds.
# select_time(my_time, include_seconds: true)
#
# # Generates a time select that defaults to the time in my_time, that has fields
# # separated by ':' and includes an input for seconds.
# select_time(my_time, time_separator: ':', include_seconds: true)
#
# # Generate a time select field with hours in the AM/PM format
# select_time(my_time, ampm: true)
#
# # Generates a time select field with hours that range from 2 to 14
# select_time(my_time, start_hour: 2, end_hour: 14)
#
# # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts.
# select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' })
# select_time(my_time, prompt: { hour: true }) # generic prompt for hours
# select_time(my_time, prompt: true) # generic prompts for all
- 9
def select_time(datetime = Time.current, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_time
end
# Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
# The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
# Override the field name using the <tt>:field_name</tt> option, 'second' by default.
#
# my_time = Time.now + 16.seconds
#
# # Generates a select field for seconds that defaults to the seconds for the time in my_time.
# select_second(my_time)
#
# # Generates a select field for seconds that defaults to the number given.
# select_second(33)
#
# # Generates a select field for seconds that defaults to the seconds for the time in my_time
# # that is named 'interval' rather than 'second'.
# select_second(my_time, field_name: 'interval')
#
# # Generates a select field for seconds with a custom prompt. Use <tt>prompt: true</tt> for a
# # generic prompt.
# select_second(14, prompt: 'Choose seconds')
- 9
def select_second(datetime, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_second
end
# Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
# Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute
# selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
# Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
#
# my_time = Time.now + 10.minutes
#
# # Generates a select field for minutes that defaults to the minutes for the time in my_time.
# select_minute(my_time)
#
# # Generates a select field for minutes that defaults to the number given.
# select_minute(14)
#
# # Generates a select field for minutes that defaults to the minutes for the time in my_time
# # that is named 'moment' rather than 'minute'.
# select_minute(my_time, field_name: 'moment')
#
# # Generates a select field for minutes with a custom prompt. Use <tt>prompt: true</tt> for a
# # generic prompt.
# select_minute(14, prompt: 'Choose minutes')
- 9
def select_minute(datetime, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_minute
end
# Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
# The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
# Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
#
# my_time = Time.now + 6.hours
#
# # Generates a select field for hours that defaults to the hour for the time in my_time.
# select_hour(my_time)
#
# # Generates a select field for hours that defaults to the number given.
# select_hour(13)
#
# # Generates a select field for hours that defaults to the hour for the time in my_time
# # that is named 'stride' rather than 'hour'.
# select_hour(my_time, field_name: 'stride')
#
# # Generates a select field for hours with a custom prompt. Use <tt>prompt: true</tt> for a
# # generic prompt.
# select_hour(13, prompt: 'Choose hour')
#
# # Generate a select field for hours in the AM/PM format
# select_hour(my_time, ampm: true)
#
# # Generates a select field that includes options for hours from 2 to 14.
# select_hour(my_time, start_hour: 2, end_hour: 14)
- 9
def select_hour(datetime, options = {}, html_options = {})
DateTimeSelector.new(datetime, options, html_options).select_hour
end
# Returns a select tag with options for each of the days 1 through 31 with the current day selected.
# The <tt>date</tt> can also be substituted for a day number.
# If you want to display days with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
# Override the field name using the <tt>:field_name</tt> option, 'day' by default.
#
# my_date = Time.now + 2.days
#
# # Generates a select field for days that defaults to the day for the date in my_date.
# select_day(my_date)
#
# # Generates a select field for days that defaults to the number given.
# select_day(5)
#
# # Generates a select field for days that defaults to the number given, but displays it with two digits.
# select_day(5, use_two_digit_numbers: true)
#
# # Generates a select field for days that defaults to the day for the date in my_date
# # that is named 'due' rather than 'day'.
# select_day(my_date, field_name: 'due')
#
# # Generates a select field for days with a custom prompt. Use <tt>prompt: true</tt> for a
# # generic prompt.
# select_day(5, prompt: 'Choose day')
- 9
def select_day(date, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_day
end
# Returns a select tag with options for each of the months January through December with the current month
# selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are
# used as values (what's submitted to the server). It's also possible to use month numbers for the presentation
# instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you
# want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer
# to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want
# to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
# If you want to display months with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
# Override the field name using the <tt>:field_name</tt> option, 'month' by default.
#
# # Generates a select field for months that defaults to the current month that
# # will use keys like "January", "March".
# select_month(Date.today)
#
# # Generates a select field for months that defaults to the current month that
# # is named "start" rather than "month".
# select_month(Date.today, field_name: 'start')
#
# # Generates a select field for months that defaults to the current month that
# # will use keys like "1", "3".
# select_month(Date.today, use_month_numbers: true)
#
# # Generates a select field for months that defaults to the current month that
# # will use keys like "1 - January", "3 - March".
# select_month(Date.today, add_month_numbers: true)
#
# # Generates a select field for months that defaults to the current month that
# # will use keys like "Jan", "Mar".
# select_month(Date.today, use_short_month: true)
#
# # Generates a select field for months that defaults to the current month that
# # will use keys like "Januar", "Marts."
# select_month(Date.today, use_month_names: %w(Januar Februar Marts ...))
#
# # Generates a select field for months that defaults to the current month that
# # will use keys with two digit numbers like "01", "03".
# select_month(Date.today, use_two_digit_numbers: true)
#
# # Generates a select field for months with a custom prompt. Use <tt>prompt: true</tt> for a
# # generic prompt.
# select_month(14, prompt: 'Choose month')
- 9
def select_month(date, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_month
end
# Returns a select tag with options for each of the five years on each side of the current, which is selected.
# The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the
# +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or
# greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number.
# Override the field name using the <tt>:field_name</tt> option, 'year' by default.
#
# # Generates a select field for years that defaults to the current year that
# # has ascending year values.
# select_year(Date.today, start_year: 1992, end_year: 2007)
#
# # Generates a select field for years that defaults to the current year that
# # is named 'birth' rather than 'year'.
# select_year(Date.today, field_name: 'birth')
#
# # Generates a select field for years that defaults to the current year that
# # has descending year values.
# select_year(Date.today, start_year: 2005, end_year: 1900)
#
# # Generates a select field for years that defaults to the year 2006 that
# # has ascending year values.
# select_year(2006, start_year: 2000, end_year: 2010)
#
# # Generates a select field for years with a custom prompt. Use <tt>prompt: true</tt> for a
# # generic prompt.
# select_year(14, prompt: 'Choose year')
- 9
def select_year(date, options = {}, html_options = {})
DateTimeSelector.new(date, options, html_options).select_year
end
# Returns an HTML time tag for the given date or time.
#
# time_tag Date.today # =>
# <time datetime="2010-11-04">November 04, 2010</time>
# time_tag Time.now # =>
# <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time>
# time_tag Date.yesterday, 'Yesterday' # =>
# <time datetime="2010-11-03">Yesterday</time>
# time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # =>
# <time datetime="2010-W44">November 04, 2010</time>
#
# <%= time_tag Time.now do %>
# <span>Right now</span>
# <% end %>
# # => <time datetime="2010-11-04T17:55:45+01:00"><span>Right now</span></time>
- 9
def time_tag(date_or_time, *args, &block)
options = args.extract_options!
format = options.delete(:format) || :long
content = args.first || I18n.l(date_or_time, format: format)
content_tag("time", content, options.reverse_merge(datetime: date_or_time.iso8601), &block)
end
- 9
private
- 9
def normalize_distance_of_time_argument_to_time(value)
if value.is_a?(Numeric)
Time.at(value)
elsif value.respond_to?(:to_time)
value.to_time
else
raise ArgumentError, "#{value.inspect} can't be converted to a Time value"
end
end
end
- 9
class DateTimeSelector #:nodoc:
- 9
include ActionView::Helpers::TagHelper
- 9
DEFAULT_PREFIX = "date"
- 9
POSITION = {
year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6
}.freeze
- 9
AMPM_TRANSLATION = Hash[
[[0, "12 AM"], [1, "01 AM"], [2, "02 AM"], [3, "03 AM"],
[4, "04 AM"], [5, "05 AM"], [6, "06 AM"], [7, "07 AM"],
[8, "08 AM"], [9, "09 AM"], [10, "10 AM"], [11, "11 AM"],
[12, "12 PM"], [13, "01 PM"], [14, "02 PM"], [15, "03 PM"],
[16, "04 PM"], [17, "05 PM"], [18, "06 PM"], [19, "07 PM"],
[20, "08 PM"], [21, "09 PM"], [22, "10 PM"], [23, "11 PM"]]
].freeze
- 9
def initialize(datetime, options = {}, html_options = {})
@options = options.dup
@html_options = html_options.dup
@datetime = datetime
@options[:datetime_separator] ||= " — "
@options[:time_separator] ||= " : "
end
- 9
def select_datetime
order = date_order.dup
order -= [:hour, :minute, :second]
@options[:discard_year] ||= true unless order.include?(:year)
@options[:discard_month] ||= true unless order.include?(:month)
@options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
@options[:discard_minute] ||= true if @options[:discard_hour]
@options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
set_day_if_discarded
if @options[:tag] && @options[:ignore_date]
select_time
else
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
order += [:hour, :minute, :second] unless @options[:discard_hour]
build_selects_from_types(order)
end
end
- 9
def select_date
order = date_order.dup
@options[:discard_hour] = true
@options[:discard_minute] = true
@options[:discard_second] = true
@options[:discard_year] ||= true unless order.include?(:year)
@options[:discard_month] ||= true unless order.include?(:month)
@options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
set_day_if_discarded
[:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
build_selects_from_types(order)
end
- 9
def select_time
order = []
@options[:discard_month] = true
@options[:discard_year] = true
@options[:discard_day] = true
@options[:discard_second] ||= true unless @options[:include_seconds]
order += [:year, :month, :day] unless @options[:ignore_date]
order += [:hour, :minute]
order << :second if @options[:include_seconds]
build_selects_from_types(order)
end
- 9
def select_second
if @options[:use_hidden] || @options[:discard_second]
build_hidden(:second, sec) if @options[:include_seconds]
else
build_options_and_select(:second, sec)
end
end
- 9
def select_minute
if @options[:use_hidden] || @options[:discard_minute]
build_hidden(:minute, min)
else
build_options_and_select(:minute, min, step: @options[:minute_step])
end
end
- 9
def select_hour
if @options[:use_hidden] || @options[:discard_hour]
build_hidden(:hour, hour)
else
options = {}
options[:ampm] = @options[:ampm] || false
options[:start] = @options[:start_hour] || 0
options[:end] = @options[:end_hour] || 23
build_options_and_select(:hour, hour, options)
end
end
- 9
def select_day
if @options[:use_hidden] || @options[:discard_day]
build_hidden(:day, day || 1)
else
build_options_and_select(:day, day, start: 1, end: 31, leading_zeros: false, use_two_digit_numbers: @options[:use_two_digit_numbers])
end
end
- 9
def select_month
if @options[:use_hidden] || @options[:discard_month]
build_hidden(:month, month || 1)
else
month_options = []
1.upto(12) do |month_number|
options = { value: month_number }
options[:selected] = "selected" if month == month_number
month_options << content_tag("option", month_name(month_number), options) + "\n"
end
build_select(:month, month_options.join)
end
end
- 9
def select_year
if !year || @datetime == 0
val = "1"
middle_year = Date.today.year
else
val = middle_year = year
end
if @options[:use_hidden] || @options[:discard_year]
build_hidden(:year, val)
else
options = {}
options[:start] = @options[:start_year] || middle_year - 5
options[:end] = @options[:end_year] || middle_year + 5
options[:step] = options[:start] < options[:end] ? 1 : -1
options[:leading_zeros] = false
options[:max_years_allowed] = @options[:max_years_allowed] || 1000
if (options[:end] - options[:start]).abs > options[:max_years_allowed]
raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter."
end
build_select(:year, build_year_options(val, options))
end
end
- 9
private
- 9
%w( sec min hour day month year ).each do |method|
- 54
define_method(method) do
case @datetime
when Hash then @datetime[method.to_sym]
when Numeric then @datetime
when nil then nil
else @datetime.send(method)
end
end
end
# If the day is hidden, the day should be set to the 1st so all month and year choices are
# valid. Otherwise, February 31st or February 29th, 2011 can be selected, which are invalid.
- 9
def set_day_if_discarded
if @datetime && @options[:discard_day]
@datetime = @datetime.change(day: 1)
end
end
# Returns translated month names, but also ensures that a custom month
# name array has a leading +nil+ element.
- 9
def month_names
@month_names ||= begin
month_names = @options[:use_month_names] || translated_month_names
month_names.unshift(nil) if month_names.size < 13
month_names
end
end
# Returns translated month names.
# => [nil, "January", "February", "March",
# "April", "May", "June", "July",
# "August", "September", "October",
# "November", "December"]
#
# If <tt>:use_short_month</tt> option is set
# => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
# "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
- 9
def translated_month_names
key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
I18n.translate(key, locale: @options[:locale])
end
# Looks up month names by number (1-based):
#
# month_name(1) # => "January"
#
# If the <tt>:use_month_numbers</tt> option is passed:
#
# month_name(1) # => 1
#
# If the <tt>:use_two_month_numbers</tt> option is passed:
#
# month_name(1) # => '01'
#
# If the <tt>:add_month_numbers</tt> option is passed:
#
# month_name(1) # => "1 - January"
#
# If the <tt>:month_format_string</tt> option is passed:
#
# month_name(1) # => "January (01)"
#
# depending on the format string.
- 9
def month_name(number)
if @options[:use_month_numbers]
number
elsif @options[:use_two_digit_numbers]
"%02d" % number
elsif @options[:add_month_numbers]
"#{number} - #{month_names[number]}"
elsif format_string = @options[:month_format_string]
format_string % { number: number, name: month_names[number] }
else
month_names[number]
end
end
# Looks up year names by number.
#
# year_name(1998) # => 1998
#
# If the <tt>:year_format</tt> option is passed:
#
# year_name(1998) # => "Heisei 10"
- 9
def year_name(number)
if year_format_lambda = @options[:year_format]
year_format_lambda.call(number)
else
number
end
end
- 9
def date_order
@date_order ||= @options[:order] || translated_date_order
end
- 9
def translated_date_order
date_order = I18n.translate(:'date.order', locale: @options[:locale], default: [])
date_order = date_order.map(&:to_sym)
forbidden_elements = date_order - [:year, :month, :day]
if forbidden_elements.any?
raise StandardError,
"#{@options[:locale]}.date.order only accepts :year, :month and :day"
end
date_order
end
# Build full select tag from date type and options.
- 9
def build_options_and_select(type, selected, options = {})
build_select(type, build_options(selected, options))
end
# Build select option HTML from date value and options.
# build_options(15, start: 1, end: 31)
# => "<option value="1">1</option>
# <option value="2">2</option>
# <option value="3">3</option>..."
#
# If <tt>use_two_digit_numbers: true</tt> option is passed
# build_options(15, start: 1, end: 31, use_two_digit_numbers: true)
# => "<option value="1">01</option>
# <option value="2">02</option>
# <option value="3">03</option>..."
#
# If <tt>:step</tt> options is passed
# build_options(15, start: 1, end: 31, step: 2)
# => "<option value="1">1</option>
# <option value="3">3</option>
# <option value="5">5</option>..."
- 9
def build_options(selected, options = {})
options = {
leading_zeros: true, ampm: false, use_two_digit_numbers: false
}.merge!(options)
start = options.delete(:start) || 0
stop = options.delete(:end) || 59
step = options.delete(:step) || 1
leading_zeros = options.delete(:leading_zeros)
select_options = []
start.step(stop, step) do |i|
value = leading_zeros ? sprintf("%02d", i) : i
tag_options = { value: value }
tag_options[:selected] = "selected" if selected == i
text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
text = options[:ampm] ? AMPM_TRANSLATION[i] : text
select_options << content_tag("option", text, tag_options)
end
(select_options.join("\n") + "\n").html_safe
end
# Build select option HTML for year.
# If <tt>year_format</tt> option is not passed
# build_year_options(1998, start: 1998, end: 2000)
# => "<option value="1998" selected="selected">1998</option>
# <option value="1999">1999</option>
# <option value="2000">2000</option>"
#
# If <tt>year_format</tt> option is passed
# build_year_options(1998, start: 1998, end: 2000, year_format: ->year { "Heisei #{ year - 1988 }" })
# => "<option value="1998" selected="selected">Heisei 10</option>
# <option value="1999">Heisei 11</option>
# <option value="2000">Heisei 12</option>"
- 9
def build_year_options(selected, options = {})
start = options.delete(:start)
stop = options.delete(:end)
step = options.delete(:step)
select_options = []
start.step(stop, step) do |value|
tag_options = { value: value }
tag_options[:selected] = "selected" if selected == value
text = year_name(value)
select_options << content_tag("option", text, tag_options)
end
(select_options.join("\n") + "\n").html_safe
end
# Builds select tag from date type and HTML select options.
# build_select(:month, "<option value="1">January</option>...")
# => "<select id="post_written_on_2i" name="post[written_on(2i)]">
# <option value="1">January</option>...
# </select>"
- 9
def build_select(type, select_options_as_html)
select_options = {
id: input_id_from_type(type),
name: input_name_from_type(type)
}.merge!(@html_options)
select_options[:disabled] = "disabled" if @options[:disabled]
select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes]
select_html = +"\n"
select_html << content_tag("option", "", value: "", label: " ") + "\n" if @options[:include_blank]
select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
select_html << select_options_as_html
(content_tag("select", select_html.html_safe, select_options) + "\n").html_safe
end
# Builds the css class value for the select element
# css_class_attribute(:year, 'date optional', { year: 'my-year' })
# => "date optional my-year"
- 9
def css_class_attribute(type, html_options_class, options) # :nodoc:
css_class = \
case options
when Hash
options[type.to_sym]
else
type
end
[html_options_class, css_class].compact.join(" ")
end
# Builds a prompt option tag with supplied options or from default options.
# prompt_option_tag(:month, prompt: 'Select month')
# => "<option value="">Select month</option>"
- 9
def prompt_option_tag(type, options)
prompt = \
case options
when Hash
default_options = { year: false, month: false, day: false, hour: false, minute: false, second: false }
default_options.merge!(options)[type.to_sym]
when String
options
else
I18n.translate(:"datetime.prompts.#{type}", locale: @options[:locale])
end
prompt ? content_tag("option", prompt, value: "") : ""
end
# Builds hidden input tag for date part and value.
# build_hidden(:year, 2008)
# => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
- 9
def build_hidden(type, value)
select_options = {
type: "hidden",
id: input_id_from_type(type),
name: input_name_from_type(type),
value: value
}.merge!(@html_options.slice(:disabled))
select_options[:disabled] = "disabled" if @options[:disabled]
tag(:input, select_options) + "\n".html_safe
end
# Returns the name attribute for the input tag.
# => post[written_on(1i)]
- 9
def input_name_from_type(type)
prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
prefix += "[#{@options[:index]}]" if @options.has_key?(:index)
field_name = @options[:field_name] || type.to_s
if @options[:include_position]
field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
end
@options[:discard_type] ? prefix : "#{prefix}[#{field_name}]"
end
# Returns the id attribute for the input tag.
# => "post_written_on_1i"
- 9
def input_id_from_type(type)
id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, "_").gsub(/[\]\)]/, "")
id = @options[:namespace] + "_" + id if @options[:namespace]
id
end
# Given an ordering of datetime components, create the selection HTML
# and join them with their appropriate separators.
- 9
def build_selects_from_types(order)
select = +""
first_visible = order.find { |type| !@options[:"discard_#{type}"] }
order.reverse_each do |type|
separator = separator(type) unless type == first_visible # don't add before first visible field
select.insert(0, separator.to_s + send("select_#{type}").to_s)
end
select.html_safe
end
# Returns the separator for a given datetime component.
- 9
def separator(type)
return "" if @options[:use_hidden]
case type
when :year, :month, :day
@options[:"discard_#{type}"] ? "" : @options[:date_separator]
when :hour
(@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
when :minute, :second
@options[:"discard_#{type}"] ? "" : @options[:time_separator]
end
end
end
- 9
class FormBuilder
# Wraps ActionView::Helpers::DateHelper#date_select for form builders:
#
# <%= form_for @person do |f| %>
# <%= f.date_select :birth_date %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def date_select(method, options = {}, html_options = {})
@template.date_select(@object_name, method, objectify_options(options), html_options)
end
# Wraps ActionView::Helpers::DateHelper#time_select for form builders:
#
# <%= form_for @race do |f| %>
# <%= f.time_select :average_lap %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def time_select(method, options = {}, html_options = {})
@template.time_select(@object_name, method, objectify_options(options), html_options)
end
# Wraps ActionView::Helpers::DateHelper#datetime_select for form builders:
#
# <%= form_for @person do |f| %>
# <%= f.datetime_select :last_request_at %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def datetime_select(method, options = {}, html_options = {})
@template.datetime_select(@object_name, method, objectify_options(options), html_options)
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
# = Action View Debug Helper
#
# Provides a set of methods for making it easier to debug Rails objects.
- 9
module Helpers #:nodoc:
- 9
module DebugHelper
- 9
include TagHelper
# Returns a YAML representation of +object+ wrapped with <pre> and </pre>.
# If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead.
# Useful for inspecting an object at the time of rendering.
#
# @user = User.new({ username: 'testing', password: 'xyz', age: 42})
# debug(@user)
# # =>
# <pre class='debug_dump'>--- !ruby/object:User
# attributes:
# updated_at:
# username: testing
# age: 42
# password: xyz
# created_at:
# </pre>
- 9
def debug(object)
Marshal.dump(object)
object = ERB::Util.html_escape(object.to_yaml)
content_tag(:pre, object, class: "debug_dump")
rescue # errors from Marshal or YAML
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
content_tag(:code, object.inspect, class: "debug_dump")
end
end
end
end
# frozen_string_literal: true
- 9
require "cgi"
- 9
require "action_view/helpers/date_helper"
- 9
require "action_view/helpers/tag_helper"
- 9
require "action_view/helpers/form_tag_helper"
- 9
require "action_view/helpers/active_model_helper"
- 9
require "action_view/model_naming"
- 9
require "action_view/record_identifier"
- 9
require "active_support/core_ext/module/attribute_accessors"
- 9
require "active_support/core_ext/hash/slice"
- 9
require "active_support/core_ext/string/output_safety"
- 9
require "active_support/core_ext/string/inflections"
- 9
require "active_support/core_ext/symbol/starts_ends_with"
- 9
module ActionView
# = Action View Form Helpers
- 9
module Helpers #:nodoc:
# Form helpers are designed to make working with resources much easier
# compared to using vanilla HTML.
#
# Typically, a form designed to create or update a resource reflects the
# identity of the resource in several ways: (i) the URL that the form is
# sent to (the form element's +action+ attribute) should result in a request
# being routed to the appropriate controller action (with the appropriate <tt>:id</tt>
# parameter in the case of an existing resource), (ii) input fields should
# be named in such a way that in the controller their values appear in the
# appropriate places within the +params+ hash, and (iii) for an existing record,
# when the form is initially displayed, input fields corresponding to attributes
# of the resource should show the current values of those attributes.
#
# In Rails, this is usually achieved by creating the form using +form_for+ and
# a number of related helper methods. +form_for+ generates an appropriate <tt>form</tt>
# tag and yields a form builder object that knows the model the form is about.
# Input fields are created by calling methods defined on the form builder, which
# means they are able to generate the appropriate names and default values
# corresponding to the model attributes, as well as convenient IDs, etc.
# Conventions in the generated field names allow controllers to receive form data
# nicely structured in +params+ with no effort on your side.
#
# For example, to create a new person you typically set up a new instance of
# +Person+ in the <tt>PeopleController#new</tt> action, <tt>@person</tt>, and
# in the view template pass that object to +form_for+:
#
# <%= form_for @person do |f| %>
# <%= f.label :first_name %>:
# <%= f.text_field :first_name %><br />
#
# <%= f.label :last_name %>:
# <%= f.text_field :last_name %><br />
#
# <%= f.submit %>
# <% end %>
#
# The HTML generated for this would be (modulus formatting):
#
# <form action="/people" class="new_person" id="new_person" method="post">
# <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
# <label for="person_first_name">First name</label>:
# <input id="person_first_name" name="person[first_name]" type="text" /><br />
#
# <label for="person_last_name">Last name</label>:
# <input id="person_last_name" name="person[last_name]" type="text" /><br />
#
# <input name="commit" type="submit" value="Create Person" />
# </form>
#
# As you see, the HTML reflects knowledge about the resource in several spots,
# like the path the form should be submitted to, or the names of the input fields.
#
# In particular, thanks to the conventions followed in the generated field names, the
# controller gets a nested hash <tt>params[:person]</tt> with the person attributes
# set in the form. That hash is ready to be passed to <tt>Person.new</tt>:
#
# @person = Person.new(params[:person])
# if @person.save
# # success
# else
# # error handling
# end
#
# Interestingly, the exact same view code in the previous example can be used to edit
# a person. If <tt>@person</tt> is an existing record with name "John Smith" and ID 256,
# the code above as is would yield instead:
#
# <form action="/people/256" class="edit_person" id="edit_person_256" method="post">
# <input name="_method" type="hidden" value="patch" />
# <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
# <label for="person_first_name">First name</label>:
# <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br />
#
# <label for="person_last_name">Last name</label>:
# <input id="person_last_name" name="person[last_name]" type="text" value="Smith" /><br />
#
# <input name="commit" type="submit" value="Update Person" />
# </form>
#
# Note that the endpoint, default values, and submit button label are tailored for <tt>@person</tt>.
# That works that way because the involved helpers know whether the resource is a new record or not,
# and generate HTML accordingly.
#
# The controller would receive the form data again in <tt>params[:person]</tt>, ready to be
# passed to <tt>Person#update</tt>:
#
# if @person.update(params[:person])
# # success
# else
# # error handling
# end
#
# That's how you typically work with resources.
- 9
module FormHelper
- 9
extend ActiveSupport::Concern
- 9
include FormTagHelper
- 9
include UrlHelper
- 9
include ModelNaming
- 9
include RecordIdentifier
- 9
attr_internal :default_form_builder
# Creates a form that allows the user to create or update the attributes
# of a specific model object.
#
# The method can be used in several slightly different ways, depending on
# how much you wish to rely on Rails to infer automatically from the model
# how the form should be constructed. For a generic model object, a form
# can be created by passing +form_for+ a string or symbol representing
# the object we are concerned with:
#
# <%= form_for :person do |f| %>
# First name: <%= f.text_field :first_name %><br />
# Last name : <%= f.text_field :last_name %><br />
# Biography : <%= f.text_area :biography %><br />
# Admin? : <%= f.check_box :admin %><br />
# <%= f.submit %>
# <% end %>
#
# The variable +f+ yielded to the block is a FormBuilder object that
# incorporates the knowledge about the model object represented by
# <tt>:person</tt> passed to +form_for+. Methods defined on the FormBuilder
# are used to generate fields bound to this model. Thus, for example,
#
# <%= f.text_field :first_name %>
#
# will get expanded to
#
# <%= text_field :person, :first_name %>
#
# which results in an HTML <tt><input></tt> tag whose +name+ attribute is
# <tt>person[first_name]</tt>. This means that when the form is submitted,
# the value entered by the user will be available in the controller as
# <tt>params[:person][:first_name]</tt>.
#
# For fields generated in this way using the FormBuilder,
# if <tt>:person</tt> also happens to be the name of an instance variable
# <tt>@person</tt>, the default value of the field shown when the form is
# initially displayed (e.g. in the situation where you are editing an
# existing record) will be the value of the corresponding attribute of
# <tt>@person</tt>.
#
# The rightmost argument to +form_for+ is an
# optional hash of options -
#
# * <tt>:url</tt> - The URL the form is to be submitted to. This may be
# represented in the same way as values passed to +url_for+ or +link_to+.
# So for example you may use a named route directly. When the model is
# represented by a string or symbol, as in the example above, if the
# <tt>:url</tt> option is not specified, by default the form will be
# sent back to the current URL (We will describe below an alternative
# resource-oriented usage of +form_for+ in which the URL does not need
# to be specified explicitly).
# * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of
# id attributes on form elements. The namespace attribute will be prefixed
# with underscore on the generated HTML id.
# * <tt>:method</tt> - The method to use when submitting the form, usually
# either "get" or "post". If "patch", "put", "delete", or another verb
# is used, a hidden input with name <tt>_method</tt> is added to
# simulate the verb over post.
# * <tt>:authenticity_token</tt> - Authenticity token to use in the form.
# Use only if you need to pass custom authenticity token string, or to
# not add authenticity_token field at all (by passing <tt>false</tt>).
# Remote forms may omit the embedded authenticity token by setting
# <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
# This is helpful when you're fragment-caching the form. Remote forms
# get the authenticity token from the <tt>meta</tt> tag, so embedding is
# unnecessary unless you support browsers without JavaScript.
# * <tt>:remote</tt> - If set to true, will allow the Unobtrusive
# JavaScript drivers to control the submit behavior. By default this
# behavior is an ajax submit.
# * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name
# utf8 is not output.
# * <tt>:html</tt> - Optional HTML attributes for the form tag.
#
# Also note that +form_for+ doesn't create an exclusive scope. It's still
# possible to use both the stand-alone FormHelper methods and methods
# from FormTagHelper. For example:
#
# <%= form_for :person do |f| %>
# First name: <%= f.text_field :first_name %>
# Last name : <%= f.text_field :last_name %>
# Biography : <%= text_area :person, :biography %>
# Admin? : <%= check_box_tag "person[admin]", "1", @person.company.admin? %>
# <%= f.submit %>
# <% end %>
#
# This also works for the methods in FormOptionsHelper and DateHelper that
# are designed to work with an object as base, like
# FormOptionsHelper#collection_select and DateHelper#datetime_select.
#
# === #form_for with a model object
#
# In the examples above, the object to be created or edited was
# represented by a symbol passed to +form_for+, and we noted that
# a string can also be used equivalently. It is also possible, however,
# to pass a model object itself to +form_for+. For example, if <tt>@post</tt>
# is an existing record you wish to edit, you can create the form using
#
# <%= form_for @post do |f| %>
# ...
# <% end %>
#
# This behaves in almost the same way as outlined previously, with a
# couple of small exceptions. First, the prefix used to name the input
# elements within the form (hence the key that denotes them in the +params+
# hash) is actually derived from the object's _class_, e.g. <tt>params[:post]</tt>
# if the object's class is +Post+. However, this can be overwritten using
# the <tt>:as</tt> option, e.g. -
#
# <%= form_for(@person, as: :client) do |f| %>
# ...
# <% end %>
#
# would result in <tt>params[:client]</tt>.
#
# Secondly, the field values shown when the form is initially displayed
# are taken from the attributes of the object passed to +form_for+,
# regardless of whether the object is an instance
# variable. So, for example, if we had a _local_ variable +post+
# representing an existing record,
#
# <%= form_for post do |f| %>
# ...
# <% end %>
#
# would produce a form with fields whose initial state reflect the current
# values of the attributes of +post+.
#
# === Resource-oriented style
#
# In the examples just shown, although not indicated explicitly, we still
# need to use the <tt>:url</tt> option in order to specify where the
# form is going to be sent. However, further simplification is possible
# if the record passed to +form_for+ is a _resource_, i.e. it corresponds
# to a set of RESTful routes, e.g. defined using the +resources+ method
# in <tt>config/routes.rb</tt>. In this case Rails will simply infer the
# appropriate URL from the record itself. For example,
#
# <%= form_for @post do |f| %>
# ...
# <% end %>
#
# is then equivalent to something like:
#
# <%= form_for @post, as: :post, url: post_path(@post), method: :patch, html: { class: "edit_post", id: "edit_post_45" } do |f| %>
# ...
# <% end %>
#
# And for a new record
#
# <%= form_for(Post.new) do |f| %>
# ...
# <% end %>
#
# is equivalent to something like:
#
# <%= form_for @post, as: :post, url: posts_path, html: { class: "new_post", id: "new_post" } do |f| %>
# ...
# <% end %>
#
# However you can still overwrite individual conventions, such as:
#
# <%= form_for(@post, url: super_posts_path) do |f| %>
# ...
# <% end %>
#
# You can also set the answer format, like this:
#
# <%= form_for(@post, format: :json) do |f| %>
# ...
# <% end %>
#
# For namespaced routes, like +admin_post_url+:
#
# <%= form_for([:admin, @post]) do |f| %>
# ...
# <% end %>
#
# If your resource has associations defined, for example, you want to add comments
# to the document given that the routes are set correctly:
#
# <%= form_for([@document, @comment]) do |f| %>
# ...
# <% end %>
#
# Where <tt>@document = Document.find(params[:id])</tt> and
# <tt>@comment = Comment.new</tt>.
#
# === Setting the method
#
# You can force the form to use the full array of HTTP verbs by setting
#
# method: (:get|:post|:patch|:put|:delete)
#
# in the options hash. If the verb is not GET or POST, which are natively
# supported by HTML forms, the form will be set to POST and a hidden input
# called _method will carry the intended verb for the server to interpret.
#
# === Unobtrusive JavaScript
#
# Specifying:
#
# remote: true
#
# in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its
# behavior. The expected default behavior is an XMLHttpRequest in the background instead of the regular
# POST arrangement, but ultimately the behavior is the choice of the JavaScript driver implementor.
# Even though it's using JavaScript to serialize the form elements, the form submission will work just like
# a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>).
#
# Example:
#
# <%= form_for(@post, remote: true) do |f| %>
# ...
# <% end %>
#
# The HTML generated for this would be:
#
# <form action='http://www.example.com' method='post' data-remote='true'>
# <input name='_method' type='hidden' value='patch' />
# ...
# </form>
#
# === Setting HTML options
#
# You can set data attributes directly by passing in a data hash, but all other HTML options must be wrapped in
# the HTML key. Example:
#
# <%= form_for(@post, data: { behavior: "autosave" }, html: { name: "go" }) do |f| %>
# ...
# <% end %>
#
# The HTML generated for this would be:
#
# <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'>
# <input name='_method' type='hidden' value='patch' />
# ...
# </form>
#
# === Removing hidden model id's
#
# The form_for method automatically includes the model id as a hidden field in the form.
# This is used to maintain the correlation between the form data and its associated model.
# Some ORM systems do not use IDs on nested models so in this case you want to be able
# to disable the hidden id.
#
# In the following example the Post model has many Comments stored within it in a NoSQL database,
# thus there is no primary key for comments.
#
# Example:
#
# <%= form_for(@post) do |f| %>
# <%= f.fields_for(:comments, include_id: false) do |cf| %>
# ...
# <% end %>
# <% end %>
#
# === Customized form builders
#
# You can also build forms using a customized FormBuilder class. Subclass
# FormBuilder and override or define some more helpers, then use your
# custom builder. For example, let's say you made a helper to
# automatically add labels to form inputs.
#
# <%= form_for @person, url: { action: "create" }, builder: LabellingFormBuilder do |f| %>
# <%= f.text_field :first_name %>
# <%= f.text_field :last_name %>
# <%= f.text_area :biography %>
# <%= f.check_box :admin %>
# <%= f.submit %>
# <% end %>
#
# In this case, if you use this:
#
# <%= render f %>
#
# The rendered template is <tt>people/_labelling_form</tt> and the local
# variable referencing the form builder is called
# <tt>labelling_form</tt>.
#
# The custom FormBuilder class is automatically merged with the options
# of a nested fields_for call, unless it's explicitly set.
#
# In many cases you will want to wrap the above in another helper, so you
# could do something like the following:
#
# def labelled_form_for(record_or_name_or_array, *args, &block)
# options = args.extract_options!
# form_for(record_or_name_or_array, *(args << options.merge(builder: LabellingFormBuilder)), &block)
# end
#
# If you don't need to attach a form to a model instance, then check out
# FormTagHelper#form_tag.
#
# === Form to external resources
#
# When you build forms to external resources sometimes you need to set an authenticity token or just render a form
# without it, for example when you submit data to a payment gateway number and types of fields could be limited.
#
# To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter
#
# <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
# ...
# <% end %>
#
# If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>:
#
# <%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
# ...
# <% end %>
- 9
def form_for(record, options = {}, &block)
raise ArgumentError, "Missing block" unless block_given?
html_options = options[:html] ||= {}
case record
when String, Symbol
object_name = record
object = nil
else
object = record.is_a?(Array) ? record.last : record
raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
object_name = options[:as] || model_name_from_record_or_class(object).param_key
apply_form_for_options!(record, object, options)
end
html_options[:data] = options.delete(:data) if options.has_key?(:data)
html_options[:remote] = options.delete(:remote) if options.has_key?(:remote)
html_options[:method] = options.delete(:method) if options.has_key?(:method)
html_options[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8)
html_options[:authenticity_token] = options.delete(:authenticity_token)
builder = instantiate_builder(object_name, object, options)
output = capture(builder, &block)
html_options[:multipart] ||= builder.multipart?
html_options = html_options_for_form(options[:url] || {}, html_options)
form_tag_with_body(html_options, output)
end
- 9
def apply_form_for_options!(record, object, options) #:nodoc:
object = convert_to_model(object)
as = options[:as]
namespace = options[:namespace]
action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
options[:html].reverse_merge!(
class: as ? "#{action}_#{as}" : dom_class(object, action),
id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
method: method
)
options[:url] ||= if options.key?(:format)
polymorphic_path(record, format: options.delete(:format))
else
polymorphic_path(record, {})
end
end
- 9
private :apply_form_for_options!
- 9
mattr_accessor :form_with_generates_remote_forms, default: true
- 9
mattr_accessor :form_with_generates_ids, default: false
# Creates a form tag based on mixing URLs, scopes, or models.
#
# # Using just a URL:
# <%= form_with url: posts_path do |form| %>
# <%= form.text_field :title %>
# <% end %>
# # =>
# <form action="/posts" method="post" data-remote="true">
# <input type="text" name="title">
# </form>
#
# # Adding a scope prefixes the input field names:
# <%= form_with scope: :post, url: posts_path do |form| %>
# <%= form.text_field :title %>
# <% end %>
# # =>
# <form action="/posts" method="post" data-remote="true">
# <input type="text" name="post[title]">
# </form>
#
# # Using a model infers both the URL and scope:
# <%= form_with model: Post.new do |form| %>
# <%= form.text_field :title %>
# <% end %>
# # =>
# <form action="/posts" method="post" data-remote="true">
# <input type="text" name="post[title]">
# </form>
#
# # An existing model makes an update form and fills out field values:
# <%= form_with model: Post.first do |form| %>
# <%= form.text_field :title %>
# <% end %>
# # =>
# <form action="/posts/1" method="post" data-remote="true">
# <input type="hidden" name="_method" value="patch">
# <input type="text" name="post[title]" value="<the title of the post>">
# </form>
#
# # Though the fields don't have to correspond to model attributes:
# <%= form_with model: Cat.new do |form| %>
# <%= form.text_field :cats_dont_have_gills %>
# <%= form.text_field :but_in_forms_they_can %>
# <% end %>
# # =>
# <form action="/cats" method="post" data-remote="true">
# <input type="text" name="cat[cats_dont_have_gills]">
# <input type="text" name="cat[but_in_forms_they_can]">
# </form>
#
# The parameters in the forms are accessible in controllers according to
# their name nesting. So inputs named +title+ and <tt>post[title]</tt> are
# accessible as <tt>params[:title]</tt> and <tt>params[:post][:title]</tt>
# respectively.
#
# By default +form_with+ attaches the <tt>data-remote</tt> attribute
# submitting the form via an XMLHTTPRequest in the background if an
# Unobtrusive JavaScript driver, like rails-ujs, is used. See the
# <tt>:local</tt> option for more.
#
# For ease of comparison the examples above left out the submit button,
# as well as the auto generated hidden fields that enable UTF-8 support
# and adds an authenticity token needed for cross site request forgery
# protection.
#
# === Resource-oriented style
#
# In many of the examples just shown, the +:model+ passed to +form_with+
# is a _resource_. It corresponds to a set of RESTful routes, most likely
# defined via +resources+ in <tt>config/routes.rb</tt>.
#
# So when passing such a model record, Rails infers the URL and method.
#
# <%= form_with model: @post do |form| %>
# ...
# <% end %>
#
# is then equivalent to something like:
#
# <%= form_with scope: :post, url: post_path(@post), method: :patch do |form| %>
# ...
# <% end %>
#
# And for a new record
#
# <%= form_with model: Post.new do |form| %>
# ...
# <% end %>
#
# is equivalent to something like:
#
# <%= form_with scope: :post, url: posts_path do |form| %>
# ...
# <% end %>
#
# ==== +form_with+ options
#
# * <tt>:url</tt> - The URL the form submits to. Akin to values passed to
# +url_for+ or +link_to+. For example, you may use a named route
# directly. When a <tt>:scope</tt> is passed without a <tt>:url</tt> the
# form just submits to the current URL.
# * <tt>:method</tt> - The method to use when submitting the form, usually
# either "get" or "post". If "patch", "put", "delete", or another verb
# is used, a hidden input named <tt>_method</tt> is added to
# simulate the verb over post.
# * <tt>:format</tt> - The format of the route the form submits to.
# Useful when submitting to another resource type, like <tt>:json</tt>.
# Skipped if a <tt>:url</tt> is passed.
# * <tt>:scope</tt> - The scope to prefix input field names with and
# thereby how the submitted parameters are grouped in controllers.
# * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of
# id attributes on form elements. The namespace attribute will be prefixed
# with underscore on the generated HTML id.
# * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and
# <tt>:scope</tt> by, plus fill out input field values.
# So if a +title+ attribute is set to "Ahoy!" then a +title+ input
# field's value would be "Ahoy!".
# If the model is a new record a create form is generated, if an
# existing record, however, an update form is generated.
# Pass <tt>:scope</tt> or <tt>:url</tt> to override the defaults.
# E.g. turn <tt>params[:post]</tt> into <tt>params[:article]</tt>.
# * <tt>:authenticity_token</tt> - Authenticity token to use in the form.
# Override with a custom authenticity token or pass <tt>false</tt> to
# skip the authenticity token field altogether.
# Useful when submitting to an external resource like a payment gateway
# that might limit the valid fields.
# Remote forms may omit the embedded authenticity token by setting
# <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
# This is helpful when fragment-caching the form. Remote forms
# get the authenticity token from the <tt>meta</tt> tag, so embedding is
# unnecessary unless you support browsers without JavaScript.
# * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs.
# Disable remote submits with <tt>local: true</tt>.
# * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
# utf8 is not output.
# * <tt>:builder</tt> - Override the object used to build the form.
# * <tt>:id</tt> - Optional HTML id attribute.
# * <tt>:class</tt> - Optional HTML class attribute.
# * <tt>:data</tt> - Optional HTML data attributes.
# * <tt>:html</tt> - Other optional HTML attributes for the form tag.
#
# === Examples
#
# When not passing a block, +form_with+ just generates an opening form tag.
#
# <%= form_with(model: @post, url: super_posts_path) %>
# <%= form_with(model: @post, scope: :article) %>
# <%= form_with(model: @post, format: :json) %>
# <%= form_with(model: @post, authenticity_token: false) %> # Disables the token.
#
# For namespaced routes, like +admin_post_url+:
#
# <%= form_with(model: [ :admin, @post ]) do |form| %>
# ...
# <% end %>
#
# If your resource has associations defined, for example, you want to add comments
# to the document given that the routes are set correctly:
#
# <%= form_with(model: [ @document, Comment.new ]) do |form| %>
# ...
# <% end %>
#
# Where <tt>@document = Document.find(params[:id])</tt>.
#
# === Mixing with other form helpers
#
# While +form_with+ uses a FormBuilder object it's possible to mix and
# match the stand-alone FormHelper methods and methods
# from FormTagHelper:
#
# <%= form_with scope: :person do |form| %>
# <%= form.text_field :first_name %>
# <%= form.text_field :last_name %>
#
# <%= text_area :person, :biography %>
# <%= check_box_tag "person[admin]", "1", @person.company.admin? %>
#
# <%= form.submit %>
# <% end %>
#
# Same goes for the methods in FormOptionsHelper and DateHelper designed
# to work with an object as a base, like
# FormOptionsHelper#collection_select and DateHelper#datetime_select.
#
# === Setting the method
#
# You can force the form to use the full array of HTTP verbs by setting
#
# method: (:get|:post|:patch|:put|:delete)
#
# in the options hash. If the verb is not GET or POST, which are natively
# supported by HTML forms, the form will be set to POST and a hidden input
# called _method will carry the intended verb for the server to interpret.
#
# === Setting HTML options
#
# You can set data attributes directly in a data hash, but HTML options
# besides id and class must be wrapped in an HTML key:
#
# <%= form_with(model: @post, data: { behavior: "autosave" }, html: { name: "go" }) do |form| %>
# ...
# <% end %>
#
# generates
#
# <form action="/posts/123" method="post" data-behavior="autosave" name="go">
# <input name="_method" type="hidden" value="patch" />
# ...
# </form>
#
# === Removing hidden model id's
#
# The +form_with+ method automatically includes the model id as a hidden field in the form.
# This is used to maintain the correlation between the form data and its associated model.
# Some ORM systems do not use IDs on nested models so in this case you want to be able
# to disable the hidden id.
#
# In the following example the Post model has many Comments stored within it in a NoSQL database,
# thus there is no primary key for comments.
#
# <%= form_with(model: @post) do |form| %>
# <%= form.fields(:comments, skip_id: true) do |fields| %>
# ...
# <% end %>
# <% end %>
#
# === Customized form builders
#
# You can also build forms using a customized FormBuilder class. Subclass
# FormBuilder and override or define some more helpers, then use your
# custom builder. For example, let's say you made a helper to
# automatically add labels to form inputs.
#
# <%= form_with model: @person, url: { action: "create" }, builder: LabellingFormBuilder do |form| %>
# <%= form.text_field :first_name %>
# <%= form.text_field :last_name %>
# <%= form.text_area :biography %>
# <%= form.check_box :admin %>
# <%= form.submit %>
# <% end %>
#
# In this case, if you use:
#
# <%= render form %>
#
# The rendered template is <tt>people/_labelling_form</tt> and the local
# variable referencing the form builder is called
# <tt>labelling_form</tt>.
#
# The custom FormBuilder class is automatically merged with the options
# of a nested +fields+ call, unless it's explicitly set.
#
# In many cases you will want to wrap the above in another helper, so you
# could do something like the following:
#
# def labelled_form_with(**options, &block)
# form_with(**options.merge(builder: LabellingFormBuilder), &block)
# end
- 9
def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
options[:allow_method_names_outside_object] = true
options[:skip_default_ids] = !form_with_generates_ids
if model
url ||= polymorphic_path(model, format: format)
model = model.last if model.is_a?(Array)
scope ||= model_name_from_record_or_class(model).param_key
end
if block_given?
builder = instantiate_builder(scope, model, options)
output = capture(builder, &block)
options[:multipart] ||= builder.multipart?
html_options = html_options_for_form_with(url, model, **options)
form_tag_with_body(html_options, output)
else
html_options = html_options_for_form_with(url, model, **options)
form_tag_html(html_options)
end
end
# Creates a scope around a specific model object like form_for, but
# doesn't create the form tags themselves. This makes fields_for suitable
# for specifying additional model objects in the same form.
#
# Although the usage and purpose of +fields_for+ is similar to +form_for+'s,
# its method signature is slightly different. Like +form_for+, it yields
# a FormBuilder object associated with a particular model object to a block,
# and within the block allows methods to be called on the builder to
# generate fields associated with the model object. Fields may reflect
# a model object in two ways - how they are named (hence how submitted
# values appear within the +params+ hash in the controller) and what
# default values are shown when the form the fields appear in is first
# displayed. In order for both of these features to be specified independently,
# both an object name (represented by either a symbol or string) and the
# object itself can be passed to the method separately -
#
# <%= form_for @person do |person_form| %>
# First name: <%= person_form.text_field :first_name %>
# Last name : <%= person_form.text_field :last_name %>
#
# <%= fields_for :permission, @person.permission do |permission_fields| %>
# Admin? : <%= permission_fields.check_box :admin %>
# <% end %>
#
# <%= person_form.submit %>
# <% end %>
#
# In this case, the checkbox field will be represented by an HTML +input+
# tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted
# value will appear in the controller as <tt>params[:permission][:admin]</tt>.
# If <tt>@person.permission</tt> is an existing record with an attribute
# +admin+, the initial state of the checkbox when first displayed will
# reflect the value of <tt>@person.permission.admin</tt>.
#
# Often this can be simplified by passing just the name of the model
# object to +fields_for+ -
#
# <%= fields_for :permission do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# ...in which case, if <tt>:permission</tt> also happens to be the name of an
# instance variable <tt>@permission</tt>, the initial state of the input
# field will reflect the value of that variable's attribute <tt>@permission.admin</tt>.
#
# Alternatively, you can pass just the model object itself (if the first
# argument isn't a string or symbol +fields_for+ will realize that the
# name has been omitted) -
#
# <%= fields_for @person.permission do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# and +fields_for+ will derive the required name of the field from the
# _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
# of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
#
# Note: This also works for the methods in FormOptionsHelper and
# DateHelper that are designed to work with an object as base, like
# FormOptionsHelper#collection_select and DateHelper#datetime_select.
#
# === Nested Attributes Examples
#
# When the object belonging to the current scope has a nested attribute
# writer for a certain attribute, fields_for will yield a new scope
# for that attribute. This allows you to create forms that set or change
# the attributes of a parent object and its associations in one go.
#
# Nested attribute writers are normal setter methods named after an
# association. The most common way of defining these writers is either
# with +accepts_nested_attributes_for+ in a model definition or by
# defining a method with the proper name. For example: the attribute
# writer for the association <tt>:address</tt> is called
# <tt>address_attributes=</tt>.
#
# Whether a one-to-one or one-to-many style form builder will be yielded
# depends on whether the normal reader method returns a _single_ object
# or an _array_ of objects.
#
# ==== One-to-one
#
# Consider a Person class which returns a _single_ Address from the
# <tt>address</tt> reader method and responds to the
# <tt>address_attributes=</tt> writer method:
#
# class Person
# def address
# @address
# end
#
# def address_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# This model can now be used with a nested fields_for, like so:
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :address do |address_fields| %>
# Street : <%= address_fields.text_field :street %>
# Zip code: <%= address_fields.text_field :zip_code %>
# <% end %>
# ...
# <% end %>
#
# When address is already an association on a Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address
# end
#
# If you want to destroy the associated model through the form, you have
# to enable it first using the <tt>:allow_destroy</tt> option for
# +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address, allow_destroy: true
# end
#
# Now, when you use a form element with the <tt>_destroy</tt> parameter,
# with a value that evaluates to +true+, you will destroy the associated
# model (e.g. 1, '1', true, or 'true'):
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :address do |address_fields| %>
# ...
# Delete: <%= address_fields.check_box :_destroy %>
# <% end %>
# ...
# <% end %>
#
# ==== One-to-many
#
# Consider a Person class which returns an _array_ of Project instances
# from the <tt>projects</tt> reader method and responds to the
# <tt>projects_attributes=</tt> writer method:
#
# class Person
# def projects
# [@project1, @project2]
# end
#
# def projects_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# Note that the <tt>projects_attributes=</tt> writer method is in fact
# required for fields_for to correctly identify <tt>:projects</tt> as a
# collection, and the correct indices to be set in the form markup.
#
# When projects is already an association on Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects
# end
#
# This model can now be used with a nested fields_for. The block given to
# the nested fields_for call will be repeated for each instance in the
# collection:
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects do |project_fields| %>
# <% if project_fields.object.active? %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# ...
# <% end %>
#
# It's also possible to specify the instance to be used:
#
# <%= form_for @person do |person_form| %>
# ...
# <% @person.projects.each do |project| %>
# <% if project.active? %>
# <%= person_form.fields_for :projects, project do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# <% end %>
# ...
# <% end %>
#
# Or a collection to be used:
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# ...
# <% end %>
#
# If you want to destroy any of the associated models through the
# form, you have to enable it first using the <tt>:allow_destroy</tt>
# option for +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects, allow_destroy: true
# end
#
# This will allow you to specify which models to destroy in the
# attributes hash by adding a form element for the <tt>_destroy</tt>
# parameter with a value that evaluates to +true+
# (e.g. 1, '1', true, or 'true'):
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects do |project_fields| %>
# Delete: <%= project_fields.check_box :_destroy %>
# <% end %>
# ...
# <% end %>
#
# When a collection is used you might want to know the index of each
# object into the array. For this purpose, the <tt>index</tt> method
# is available in the FormBuilder object.
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects do |project_fields| %>
# Project #<%= project_fields.index %>
# ...
# <% end %>
# ...
# <% end %>
#
# Note that fields_for will automatically generate a hidden field
# to store the ID of the record. There are circumstances where this
# hidden field is not needed and you can pass <tt>include_id: false</tt>
# to prevent fields_for from rendering it automatically.
- 9
def fields_for(record_name, record_object = nil, options = {}, &block)
builder = instantiate_builder(record_name, record_object, options)
capture(builder, &block)
end
# Scopes input fields with either an explicit scope or model.
# Like +form_with+ does with <tt>:scope</tt> or <tt>:model</tt>,
# except it doesn't output the form tags.
#
# # Using a scope prefixes the input field names:
# <%= fields :comment do |fields| %>
# <%= fields.text_field :body %>
# <% end %>
# # => <input type="text" name="comment[body]">
#
# # Using a model infers the scope and assigns field values:
# <%= fields model: Comment.new(body: "full bodied") do |fields| %>
# <%= fields.text_field :body %>
# <% end %>
# # => <input type="text" name="comment[body]" value="full bodied">
#
# # Using +fields+ with +form_with+:
# <%= form_with model: @post do |form| %>
# <%= form.text_field :title %>
#
# <%= form.fields :comment do |fields| %>
# <%= fields.text_field :body %>
# <% end %>
# <% end %>
#
# Much like +form_with+ a FormBuilder instance associated with the scope
# or model is yielded, so any generated field names are prefixed with
# either the passed scope or the scope inferred from the <tt>:model</tt>.
#
# === Mixing with other form helpers
#
# While +form_with+ uses a FormBuilder object it's possible to mix and
# match the stand-alone FormHelper methods and methods
# from FormTagHelper:
#
# <%= fields model: @comment do |fields| %>
# <%= fields.text_field :body %>
#
# <%= text_area :commenter, :biography %>
# <%= check_box_tag "comment[all_caps]", "1", @comment.commenter.hulk_mode? %>
# <% end %>
#
# Same goes for the methods in FormOptionsHelper and DateHelper designed
# to work with an object as a base, like
# FormOptionsHelper#collection_select and DateHelper#datetime_select.
- 9
def fields(scope = nil, model: nil, **options, &block)
options[:allow_method_names_outside_object] = true
options[:skip_default_ids] = !form_with_generates_ids
if model
scope ||= model_name_from_record_or_class(model).param_key
end
builder = instantiate_builder(scope, model, options)
capture(builder, &block)
end
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
# is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
# Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
# onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
# target labels for radio_button tags (where the value is used in the ID of the input tag).
#
# ==== Examples
# label(:post, :title)
# # => <label for="post_title">Title</label>
#
# You can localize your labels based on model and attribute names.
# For example you can define the following in your locale (e.g. en.yml)
#
# helpers:
# label:
# post:
# body: "Write your entire text here"
#
# Which then will result in
#
# label(:post, :body)
# # => <label for="post_body">Write your entire text here</label>
#
# Localization can also be based purely on the translation of the attribute-name
# (if you are using ActiveRecord):
#
# activerecord:
# attributes:
# post:
# cost: "Total cost"
#
# label(:post, :cost)
# # => <label for="post_cost">Total cost</label>
#
# label(:post, :title, "A short title")
# # => <label for="post_title">A short title</label>
#
# label(:post, :title, "A short title", class: "title_label")
# # => <label for="post_title" class="title_label">A short title</label>
#
# label(:post, :privacy, "Public Post", value: "public")
# # => <label for="post_privacy_public">Public Post</label>
#
# label(:post, :terms) do
# raw('Accept <a href="/terms">Terms</a>.')
# end
# # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
- 9
def label(object_name, method, content_or_options = nil, options = nil, &block)
Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
end
# Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
# shown.
#
# ==== Examples
# text_field(:post, :title, size: 20)
# # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
#
# text_field(:post, :title, class: "create_input")
# # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
#
# text_field(:post, :title, maxlength: 30, class: "title_input")
# # => <input type="text" id="post_title" name="post[title]" maxlength="30" size="30" value="#{@post.title}" class="title_input" />
#
# text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }")
# # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/>
#
# text_field(:snippet, :code, size: 20, class: 'code_input')
# # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
- 9
def text_field(object_name, method, options = {})
Tags::TextField.new(object_name, method, self, options).render
end
# Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
# shown. For security reasons this field is blank by default; pass in a value via +options+ if this is not desired.
#
# ==== Examples
# password_field(:login, :pass, size: 20)
# # => <input type="password" id="login_pass" name="login[pass]" size="20" />
#
# password_field(:account, :secret, class: "form_input", value: @account.secret)
# # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
#
# password_field(:user, :password, onchange: "if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }")
# # => <input type="password" id="user_password" name="user[password]" onchange="if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }"/>
#
# password_field(:account, :pin, size: 20, class: 'form_input')
# # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" />
- 9
def password_field(object_name, method, options = {})
Tags::PasswordField.new(object_name, method, self, options).render
end
# Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
# shown.
#
# ==== Examples
# hidden_field(:signup, :pass_confirm)
# # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
#
# hidden_field(:post, :tag_list)
# # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
#
# hidden_field(:user, :token)
# # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
- 9
def hidden_field(object_name, method, options = {})
Tags::HiddenField.new(object_name, method, self, options).render
end
# Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
# shown.
#
# Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
#
# ==== Options
# * Creates standard HTML attributes for the tag.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
# * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
#
# ==== Examples
# file_field(:user, :avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
# file_field(:post, :image, multiple: true)
# # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
# file_field(:post, :attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
#
# file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg')
# # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
#
# file_field(:attachment, :file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
- 9
def file_field(object_name, method, options = {})
Tags::FileField.new(object_name, method, self, convert_direct_upload_option_to_url(options.dup)).render
end
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
# on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+.
#
# ==== Examples
# text_area(:post, :body, cols: 20, rows: 40)
# # => <textarea cols="20" rows="40" id="post_body" name="post[body]">
# # #{@post.body}
# # </textarea>
#
# text_area(:comment, :text, size: "20x30")
# # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
# # #{@comment.text}
# # </textarea>
#
# text_area(:application, :notes, cols: 40, rows: 15, class: 'app_input')
# # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input">
# # #{@application.notes}
# # </textarea>
#
# text_area(:entry, :body, size: "20x20", disabled: 'disabled')
# # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled">
# # #{@entry.body}
# # </textarea>
- 9
def text_area(object_name, method, options = {})
Tags::TextArea.new(object_name, method, self, options).render
end
# Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
# It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
# Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
# while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
#
# ==== Gotcha
#
# The HTML specification says unchecked check boxes are not successful, and
# thus web browsers do not send them. Unfortunately this introduces a gotcha:
# if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
# invoice the user unchecks its check box, no +paid+ parameter is sent. So,
# any mass-assignment idiom like
#
# @invoice.update(params[:invoice])
#
# wouldn't update the flag.
#
# To prevent this the helper generates an auxiliary hidden field before
# the very check box. The hidden field has the same name and its
# attributes mimic an unchecked check box.
#
# This way, the client either sends only the hidden field (representing
# the check box is unchecked), or both fields. Since the HTML specification
# says key/value pairs have to be sent in the same order they appear in the
# form, and parameters extraction gets the last occurrence of any repeated
# key in the query string, that works for ordinary forms.
#
# Unfortunately that workaround does not work when the check box goes
# within an array-like parameter, as in
#
# <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %>
# <%= form.check_box :paid %>
# ...
# <% end %>
#
# because parameter name repetition is precisely what Rails seeks to distinguish
# the elements of the array. For each item with a checked check box you
# get an extra ghost item with only that attribute, assigned to "0".
#
# In that case it is preferable to either use +check_box_tag+ or to use
# hashes instead of arrays.
#
# # Let's say that @post.validated? is 1:
# check_box("post", "validated")
# # => <input name="post[validated]" type="hidden" value="0" />
# # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
#
# # Let's say that @puppy.gooddog is "no":
# check_box("puppy", "gooddog", {}, "yes", "no")
# # => <input name="puppy[gooddog]" type="hidden" value="no" />
# # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
#
# check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no")
# # => <input name="eula[accepted]" type="hidden" value="no" />
# # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
- 9
def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
Tags::CheckBox.new(object_name, method, self, checked_value, unchecked_value, options).render
end
# Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
# radio button will be checked.
#
# To force the radio button to be checked pass <tt>checked: true</tt> in the
# +options+ hash. You may pass HTML options there as well.
#
# # Let's say that @post.category returns "rails":
# radio_button("post", "category", "rails")
# radio_button("post", "category", "java")
# # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
# # <input type="radio" id="post_category_java" name="post[category]" value="java" />
#
# # Let's say that @user.receive_newsletter returns "no":
# radio_button("user", "receive_newsletter", "yes")
# radio_button("user", "receive_newsletter", "no")
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
# # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
- 9
def radio_button(object_name, method, tag_value, options = {})
Tags::RadioButton.new(object_name, method, self, tag_value, options).render
end
# Returns a text_field of type "color".
#
# color_field("car", "color")
# # => <input id="car_color" name="car[color]" type="color" value="#000000" />
- 9
def color_field(object_name, method, options = {})
Tags::ColorField.new(object_name, method, self, options).render
end
# Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object_name+). Inputs of type "search" may be styled differently by
# some browsers.
#
# search_field(:user, :name)
# # => <input id="user_name" name="user[name]" type="search" />
# search_field(:user, :name, autosave: false)
# # => <input autosave="false" id="user_name" name="user[name]" type="search" />
# search_field(:user, :name, results: 3)
# # => <input id="user_name" name="user[name]" results="3" type="search" />
# # Assume request.host returns "www.example.com"
# search_field(:user, :name, autosave: true)
# # => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" type="search" />
# search_field(:user, :name, onsearch: true)
# # => <input id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
# search_field(:user, :name, autosave: false, onsearch: true)
# # => <input autosave="false" id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
# search_field(:user, :name, autosave: true, onsearch: true)
# # => <input autosave="com.example.www" id="user_name" incremental="true" name="user[name]" onsearch="true" results="10" type="search" />
- 9
def search_field(object_name, method, options = {})
Tags::SearchField.new(object_name, method, self, options).render
end
# Returns a text_field of type "tel".
#
# telephone_field("user", "phone")
# # => <input id="user_phone" name="user[phone]" type="tel" />
#
- 9
def telephone_field(object_name, method, options = {})
Tags::TelField.new(object_name, method, self, options).render
end
# aliases telephone_field
- 9
alias phone_field telephone_field
# Returns a text_field of type "date".
#
# date_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" />
#
# The default value is generated by trying to call +strftime+ with "%Y-%m-%d"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone. You can still override that
# by passing the "value" option explicitly, e.g.
#
# @user.born_on = Date.new(1984, 1, 27)
# date_field("user", "born_on", value: "1984-05-12")
# # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" />
#
# You can create values for the "min" and "max" attributes by passing
# instances of Date or Time to the options hash.
#
# date_field("user", "born_on", min: Date.today)
# # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
#
# Alternatively, you can pass a String formatted as an ISO8601 date as the
# values for "min" and "max."
#
# date_field("user", "born_on", min: "2014-05-20")
# # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
#
- 9
def date_field(object_name, method, options = {})
Tags::DateField.new(object_name, method, self, options).render
end
# Returns a text_field of type "time".
#
# The default value is generated by trying to call +strftime+ with "%T.%L"
# on the object's value. It is still possible to override that
# by passing the "value" option.
#
# === Options
# * Accepts same options as time_field_tag
#
# === Example
# time_field("task", "started_at")
# # => <input id="task_started_at" name="task[started_at]" type="time" />
#
# You can create values for the "min" and "max" attributes by passing
# instances of Date or Time to the options hash.
#
# time_field("task", "started_at", min: Time.now)
# # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
#
# Alternatively, you can pass a String formatted as an ISO8601 time as the
# values for "min" and "max."
#
# time_field("task", "started_at", min: "01:00:00")
# # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
#
- 9
def time_field(object_name, method, options = {})
Tags::TimeField.new(object_name, method, self, options).render
end
# Returns a text_field of type "datetime-local".
#
# datetime_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" />
#
# The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone.
#
# @user.born_on = Date.new(1984, 1, 12)
# datetime_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" />
#
# You can create values for the "min" and "max" attributes by passing
# instances of Date or Time to the options hash.
#
# datetime_field("user", "born_on", min: Date.today)
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
#
# Alternatively, you can pass a String formatted as an ISO8601 datetime as
# the values for "min" and "max."
#
# datetime_field("user", "born_on", min: "2014-05-20T00:00:00")
# # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
#
- 9
def datetime_field(object_name, method, options = {})
Tags::DatetimeLocalField.new(object_name, method, self, options).render
end
- 9
alias datetime_local_field datetime_field
# Returns a text_field of type "month".
#
# month_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="month" />
#
# The default value is generated by trying to call +strftime+ with "%Y-%m"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone.
#
# @user.born_on = Date.new(1984, 1, 27)
# month_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-01" />
#
- 9
def month_field(object_name, method, options = {})
Tags::MonthField.new(object_name, method, self, options).render
end
# Returns a text_field of type "week".
#
# week_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="week" />
#
# The default value is generated by trying to call +strftime+ with "%Y-W%W"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone.
#
# @user.born_on = Date.new(1984, 5, 12)
# week_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-W19" />
#
- 9
def week_field(object_name, method, options = {})
Tags::WeekField.new(object_name, method, self, options).render
end
# Returns a text_field of type "url".
#
# url_field("user", "homepage")
# # => <input id="user_homepage" name="user[homepage]" type="url" />
#
- 9
def url_field(object_name, method, options = {})
Tags::UrlField.new(object_name, method, self, options).render
end
# Returns a text_field of type "email".
#
# email_field("user", "address")
# # => <input id="user_address" name="user[address]" type="email" />
#
- 9
def email_field(object_name, method, options = {})
Tags::EmailField.new(object_name, method, self, options).render
end
# Returns an input tag of type "number".
#
# ==== Options
# * Accepts same options as number_field_tag
- 9
def number_field(object_name, method, options = {})
Tags::NumberField.new(object_name, method, self, options).render
end
# Returns an input tag of type "range".
#
# ==== Options
# * Accepts same options as range_field_tag
- 9
def range_field(object_name, method, options = {})
Tags::RangeField.new(object_name, method, self, options).render
end
- 9
private
- 9
def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
skip_enforcing_utf8: nil, **options)
html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil?
html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart)
# The following URL is unescaped, this is just a hash of options, and it is the
# responsibility of the caller to escape all the values.
html_options[:action] = url_for(url_for_options || {})
html_options[:"accept-charset"] = "UTF-8"
html_options[:"data-remote"] = true unless local
html_options[:authenticity_token] = options.delete(:authenticity_token)
if !local && html_options[:authenticity_token].blank?
html_options[:authenticity_token] = embed_authenticity_token_in_remote_forms
end
if html_options[:authenticity_token] == true
# Include the default authenticity_token, which is only generated when it's set to nil,
# but we needed the true value to override the default of no authenticity_token on data-remote.
html_options[:authenticity_token] = nil
end
html_options.stringify_keys!
end
- 9
def instantiate_builder(record_name, record_object, options)
case record_name
when String, Symbol
object = record_object
object_name = record_name
else
object = record_name
object_name = model_name_from_record_or_class(object).param_key if object
end
builder = options[:builder] || default_form_builder_class
builder.new(object_name, object, self, options)
end
- 9
def default_form_builder_class
builder = default_form_builder || ActionView::Base.default_form_builder
builder.respond_to?(:constantize) ? builder.constantize : builder
end
end
# A +FormBuilder+ object is associated with a particular model object and
# allows you to generate fields associated with the model object. The
# +FormBuilder+ object is yielded when using +form_for+ or +fields_for+.
# For example:
#
# <%= form_for @person do |person_form| %>
# Name: <%= person_form.text_field :name %>
# Admin: <%= person_form.check_box :admin %>
# <% end %>
#
# In the above block, a +FormBuilder+ object is yielded as the
# +person_form+ variable. This allows you to generate the +text_field+
# and +check_box+ fields by specifying their eponymous methods, which
# modify the underlying template and associates the <tt>@person</tt> model object
# with the form.
#
# The +FormBuilder+ object can be thought of as serving as a proxy for the
# methods in the +FormHelper+ module. This class, however, allows you to
# call methods with the model object you are building the form for.
#
# You can create your own custom FormBuilder templates by subclassing this
# class. For example:
#
# class MyFormBuilder < ActionView::Helpers::FormBuilder
# def div_radio_button(method, tag_value, options = {})
# @template.content_tag(:div,
# @template.radio_button(
# @object_name, method, tag_value, objectify_options(options)
# )
# )
# end
# end
#
# The above code creates a new method +div_radio_button+ which wraps a div
# around the new radio button. Note that when options are passed in, you
# must call +objectify_options+ in order for the model object to get
# correctly passed to the method. If +objectify_options+ is not called,
# then the newly created helper will not be linked back to the model.
#
# The +div_radio_button+ code from above can now be used as follows:
#
# <%= form_for @person, :builder => MyFormBuilder do |f| %>
# I am a child: <%= f.div_radio_button(:admin, "child") %>
# I am an adult: <%= f.div_radio_button(:admin, "adult") %>
# <% end -%>
#
# The standard set of helper methods for form building are located in the
# +field_helpers+ class attribute.
- 9
class FormBuilder
- 9
include ModelNaming
# The methods which wrap a form helper call.
- 9
class_attribute :field_helpers, default: [
:fields_for, :fields, :label, :text_field, :password_field,
:hidden_field, :file_field, :text_area, :check_box,
:radio_button, :color_field, :search_field,
:telephone_field, :phone_field, :date_field,
:time_field, :datetime_field, :datetime_local_field,
:month_field, :week_field, :url_field, :email_field,
:number_field, :range_field
]
- 9
attr_accessor :object_name, :object, :options
- 9
attr_reader :multipart, :index
- 9
alias :multipart? :multipart
- 9
def multipart=(multipart)
@multipart = multipart
if parent_builder = @options[:parent_builder]
parent_builder.multipart = multipart
end
end
- 9
def self._to_partial_path
@_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, "")
end
- 9
def to_partial_path
self.class._to_partial_path
end
- 9
def to_model
self
end
- 9
def initialize(object_name, object, template, options)
@nested_child_index = {}
@object_name, @object, @template, @options = object_name, object, template, options
@default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {}
@default_html_options = @default_options.except(:skip_default_ids, :allow_method_names_outside_object)
convert_to_legacy_options(@options)
if @object_name&.end_with?("[]")
if (object ||= @template.instance_variable_get("@#{@object_name[0..-3]}")) && object.respond_to?(:to_param)
@auto_index = object.to_param
else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
end
end
@multipart = nil
@index = options[:index] || options[:child_index]
end
##
# :method: text_field
#
# :call-seq: text_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#text_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.text_field :name %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: password_field
#
# :call-seq: password_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#password_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.password_field :password %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: text_area
#
# :call-seq: text_area(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#text_area for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.text_area :detail %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: color_field
#
# :call-seq: color_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#color_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.color_field :favorite_color %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: search_field
#
# :call-seq: search_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#search_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.search_field :name %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: telephone_field
#
# :call-seq: telephone_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#telephone_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.telephone_field :phone %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: phone_field
#
# :call-seq: phone_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#phone_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.phone_field :phone %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: date_field
#
# :call-seq: date_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#date_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.date_field :born_on %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: time_field
#
# :call-seq: time_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#time_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.time_field :born_at %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: datetime_field
#
# :call-seq: datetime_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#datetime_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.datetime_field :graduation_day %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: datetime_local_field
#
# :call-seq: datetime_local_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#datetime_local_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.datetime_local_field :graduation_day %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: month_field
#
# :call-seq: month_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#month_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.month_field :birthday_month %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: week_field
#
# :call-seq: week_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#week_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.week_field :birthday_week %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: url_field
#
# :call-seq: url_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#url_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.url_field :homepage %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: email_field
#
# :call-seq: email_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#email_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.email_field :address %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: number_field
#
# :call-seq: number_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#number_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.number_field :age %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
##
# :method: range_field
#
# :call-seq: range_field(method, options = {})
#
# Wraps ActionView::Helpers::FormHelper#range_field for form builders:
#
# <%= form_with model: @user do |f| %>
# <%= f.range_field :age %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
(field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector|
- 153
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
def #{selector}(method, options = {}) # def text_field(method, options = {})
@template.send( # @template.send(
#{selector.inspect}, # :text_field,
@object_name, # @object_name,
method, # method,
objectify_options(options)) # objectify_options(options))
end # end
RUBY_EVAL
end
# Creates a scope around a specific model object like form_for, but
# doesn't create the form tags themselves. This makes fields_for suitable
# for specifying additional model objects in the same form.
#
# Although the usage and purpose of +fields_for+ is similar to +form_for+'s,
# its method signature is slightly different. Like +form_for+, it yields
# a FormBuilder object associated with a particular model object to a block,
# and within the block allows methods to be called on the builder to
# generate fields associated with the model object. Fields may reflect
# a model object in two ways - how they are named (hence how submitted
# values appear within the +params+ hash in the controller) and what
# default values are shown when the form the fields appear in is first
# displayed. In order for both of these features to be specified independently,
# both an object name (represented by either a symbol or string) and the
# object itself can be passed to the method separately -
#
# <%= form_for @person do |person_form| %>
# First name: <%= person_form.text_field :first_name %>
# Last name : <%= person_form.text_field :last_name %>
#
# <%= fields_for :permission, @person.permission do |permission_fields| %>
# Admin? : <%= permission_fields.check_box :admin %>
# <% end %>
#
# <%= person_form.submit %>
# <% end %>
#
# In this case, the checkbox field will be represented by an HTML +input+
# tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted
# value will appear in the controller as <tt>params[:permission][:admin]</tt>.
# If <tt>@person.permission</tt> is an existing record with an attribute
# +admin+, the initial state of the checkbox when first displayed will
# reflect the value of <tt>@person.permission.admin</tt>.
#
# Often this can be simplified by passing just the name of the model
# object to +fields_for+ -
#
# <%= fields_for :permission do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# ...in which case, if <tt>:permission</tt> also happens to be the name of an
# instance variable <tt>@permission</tt>, the initial state of the input
# field will reflect the value of that variable's attribute <tt>@permission.admin</tt>.
#
# Alternatively, you can pass just the model object itself (if the first
# argument isn't a string or symbol +fields_for+ will realize that the
# name has been omitted) -
#
# <%= fields_for @person.permission do |permission_fields| %>
# Admin?: <%= permission_fields.check_box :admin %>
# <% end %>
#
# and +fields_for+ will derive the required name of the field from the
# _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
# of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
#
# Note: This also works for the methods in FormOptionsHelper and
# DateHelper that are designed to work with an object as base, like
# FormOptionsHelper#collection_select and DateHelper#datetime_select.
#
# === Nested Attributes Examples
#
# When the object belonging to the current scope has a nested attribute
# writer for a certain attribute, fields_for will yield a new scope
# for that attribute. This allows you to create forms that set or change
# the attributes of a parent object and its associations in one go.
#
# Nested attribute writers are normal setter methods named after an
# association. The most common way of defining these writers is either
# with +accepts_nested_attributes_for+ in a model definition or by
# defining a method with the proper name. For example: the attribute
# writer for the association <tt>:address</tt> is called
# <tt>address_attributes=</tt>.
#
# Whether a one-to-one or one-to-many style form builder will be yielded
# depends on whether the normal reader method returns a _single_ object
# or an _array_ of objects.
#
# ==== One-to-one
#
# Consider a Person class which returns a _single_ Address from the
# <tt>address</tt> reader method and responds to the
# <tt>address_attributes=</tt> writer method:
#
# class Person
# def address
# @address
# end
#
# def address_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# This model can now be used with a nested fields_for, like so:
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :address do |address_fields| %>
# Street : <%= address_fields.text_field :street %>
# Zip code: <%= address_fields.text_field :zip_code %>
# <% end %>
# ...
# <% end %>
#
# When address is already an association on a Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address
# end
#
# If you want to destroy the associated model through the form, you have
# to enable it first using the <tt>:allow_destroy</tt> option for
# +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_one :address
# accepts_nested_attributes_for :address, allow_destroy: true
# end
#
# Now, when you use a form element with the <tt>_destroy</tt> parameter,
# with a value that evaluates to +true+, you will destroy the associated
# model (e.g. 1, '1', true, or 'true'):
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :address do |address_fields| %>
# ...
# Delete: <%= address_fields.check_box :_destroy %>
# <% end %>
# ...
# <% end %>
#
# ==== One-to-many
#
# Consider a Person class which returns an _array_ of Project instances
# from the <tt>projects</tt> reader method and responds to the
# <tt>projects_attributes=</tt> writer method:
#
# class Person
# def projects
# [@project1, @project2]
# end
#
# def projects_attributes=(attributes)
# # Process the attributes hash
# end
# end
#
# Note that the <tt>projects_attributes=</tt> writer method is in fact
# required for fields_for to correctly identify <tt>:projects</tt> as a
# collection, and the correct indices to be set in the form markup.
#
# When projects is already an association on Person you can use
# +accepts_nested_attributes_for+ to define the writer method for you:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects
# end
#
# This model can now be used with a nested fields_for. The block given to
# the nested fields_for call will be repeated for each instance in the
# collection:
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects do |project_fields| %>
# <% if project_fields.object.active? %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# ...
# <% end %>
#
# It's also possible to specify the instance to be used:
#
# <%= form_for @person do |person_form| %>
# ...
# <% @person.projects.each do |project| %>
# <% if project.active? %>
# <%= person_form.fields_for :projects, project do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# <% end %>
# <% end %>
# ...
# <% end %>
#
# Or a collection to be used:
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
# Name: <%= project_fields.text_field :name %>
# <% end %>
# ...
# <% end %>
#
# If you want to destroy any of the associated models through the
# form, you have to enable it first using the <tt>:allow_destroy</tt>
# option for +accepts_nested_attributes_for+:
#
# class Person < ActiveRecord::Base
# has_many :projects
# accepts_nested_attributes_for :projects, allow_destroy: true
# end
#
# This will allow you to specify which models to destroy in the
# attributes hash by adding a form element for the <tt>_destroy</tt>
# parameter with a value that evaluates to +true+
# (e.g. 1, '1', true, or 'true'):
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects do |project_fields| %>
# Delete: <%= project_fields.check_box :_destroy %>
# <% end %>
# ...
# <% end %>
#
# When a collection is used you might want to know the index of each
# object into the array. For this purpose, the <tt>index</tt> method
# is available in the FormBuilder object.
#
# <%= form_for @person do |person_form| %>
# ...
# <%= person_form.fields_for :projects do |project_fields| %>
# Project #<%= project_fields.index %>
# ...
# <% end %>
# ...
# <% end %>
#
# Note that fields_for will automatically generate a hidden field
# to store the ID of the record. There are circumstances where this
# hidden field is not needed and you can pass <tt>include_id: false</tt>
# to prevent fields_for from rendering it automatically.
- 9
def fields_for(record_name, record_object = nil, fields_options = {}, &block)
fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
fields_options[:builder] ||= options[:builder]
fields_options[:namespace] = options[:namespace]
fields_options[:parent_builder] = self
case record_name
when String, Symbol
if nested_attributes_association?(record_name)
return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
end
else
record_object = record_name.is_a?(Array) ? record_name.last : record_name
record_name = model_name_from_record_or_class(record_object).param_key
end
object_name = @object_name
index = if options.has_key?(:index)
options[:index]
elsif defined?(@auto_index)
object_name = object_name.to_s.delete_suffix("[]")
@auto_index
end
record_name = if index
"#{object_name}[#{index}][#{record_name}]"
elsif record_name.end_with?("[]")
"#{object_name}[#{record_name[0..-3]}][#{record_object.id}]"
else
"#{object_name}[#{record_name}]"
end
fields_options[:child_index] = index
@template.fields_for(record_name, record_object, fields_options, &block)
end
# See the docs for the <tt>ActionView::FormHelper.fields</tt> helper method.
- 9
def fields(scope = nil, model: nil, **options, &block)
options[:allow_method_names_outside_object] = true
options[:skip_default_ids] = !FormHelper.form_with_generates_ids
convert_to_legacy_options(options)
fields_for(scope || model, model, options, &block)
end
# Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
# is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
# Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
# onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
# target labels for radio_button tags (where the value is used in the ID of the input tag).
#
# ==== Examples
# label(:title)
# # => <label for="post_title">Title</label>
#
# You can localize your labels based on model and attribute names.
# For example you can define the following in your locale (e.g. en.yml)
#
# helpers:
# label:
# post:
# body: "Write your entire text here"
#
# Which then will result in
#
# label(:body)
# # => <label for="post_body">Write your entire text here</label>
#
# Localization can also be based purely on the translation of the attribute-name
# (if you are using ActiveRecord):
#
# activerecord:
# attributes:
# post:
# cost: "Total cost"
#
# label(:cost)
# # => <label for="post_cost">Total cost</label>
#
# label(:title, "A short title")
# # => <label for="post_title">A short title</label>
#
# label(:title, "A short title", class: "title_label")
# # => <label for="post_title" class="title_label">A short title</label>
#
# label(:privacy, "Public Post", value: "public")
# # => <label for="post_privacy_public">Public Post</label>
#
# label(:terms) do
# raw('Accept <a href="/terms">Terms</a>.')
# end
# # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
- 9
def label(method, text = nil, options = {}, &block)
@template.label(@object_name, method, text, objectify_options(options), &block)
end
# Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
# It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
# Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
# while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
#
# ==== Gotcha
#
# The HTML specification says unchecked check boxes are not successful, and
# thus web browsers do not send them. Unfortunately this introduces a gotcha:
# if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
# invoice the user unchecks its check box, no +paid+ parameter is sent. So,
# any mass-assignment idiom like
#
# @invoice.update(params[:invoice])
#
# wouldn't update the flag.
#
# To prevent this the helper generates an auxiliary hidden field before
# the very check box. The hidden field has the same name and its
# attributes mimic an unchecked check box.
#
# This way, the client either sends only the hidden field (representing
# the check box is unchecked), or both fields. Since the HTML specification
# says key/value pairs have to be sent in the same order they appear in the
# form, and parameters extraction gets the last occurrence of any repeated
# key in the query string, that works for ordinary forms.
#
# Unfortunately that workaround does not work when the check box goes
# within an array-like parameter, as in
#
# <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %>
# <%= form.check_box :paid %>
# ...
# <% end %>
#
# because parameter name repetition is precisely what Rails seeks to distinguish
# the elements of the array. For each item with a checked check box you
# get an extra ghost item with only that attribute, assigned to "0".
#
# In that case it is preferable to either use +check_box_tag+ or to use
# hashes instead of arrays.
#
# # Let's say that @post.validated? is 1:
# check_box("validated")
# # => <input name="post[validated]" type="hidden" value="0" />
# # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
#
# # Let's say that @puppy.gooddog is "no":
# check_box("gooddog", {}, "yes", "no")
# # => <input name="puppy[gooddog]" type="hidden" value="no" />
# # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
#
# # Let's say that @eula.accepted is "no":
# check_box("accepted", { class: 'eula_check' }, "yes", "no")
# # => <input name="eula[accepted]" type="hidden" value="no" />
# # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
- 9
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
@template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
end
# Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
# radio button will be checked.
#
# To force the radio button to be checked pass <tt>checked: true</tt> in the
# +options+ hash. You may pass HTML options there as well.
#
# # Let's say that @post.category returns "rails":
# radio_button("category", "rails")
# radio_button("category", "java")
# # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
# # <input type="radio" id="post_category_java" name="post[category]" value="java" />
#
# # Let's say that @user.receive_newsletter returns "no":
# radio_button("receive_newsletter", "yes")
# radio_button("receive_newsletter", "no")
# # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
# # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
- 9
def radio_button(method, tag_value, options = {})
@template.radio_button(@object_name, method, tag_value, objectify_options(options))
end
# Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
# shown.
#
# ==== Examples
# # Let's say that @signup.pass_confirm returns true:
# hidden_field(:pass_confirm)
# # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="true" />
#
# # Let's say that @post.tag_list returns "blog, ruby":
# hidden_field(:tag_list)
# # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="blog, ruby" />
#
# # Let's say that @user.token returns "abcde":
# hidden_field(:token)
# # => <input type="hidden" id="user_token" name="user[token]" value="abcde" />
#
- 9
def hidden_field(method, options = {})
@emitted_hidden_id = true if method == :id
@template.hidden_field(@object_name, method, objectify_options(options))
end
# Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
# hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
# shown.
#
# Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
#
# ==== Options
# * Creates standard HTML attributes for the tag.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
# * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
#
# ==== Examples
# # Let's say that @user has avatar:
# file_field(:avatar)
# # => <input type="file" id="user_avatar" name="user[avatar]" />
#
# # Let's say that @post has image:
# file_field(:image, :multiple => true)
# # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" />
#
# # Let's say that @post has attached:
# file_field(:attached, accept: 'text/html')
# # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
#
# # Let's say that @post has image:
# file_field(:image, accept: 'image/png,image/gif,image/jpeg')
# # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
#
# # Let's say that @attachment has file:
# file_field(:file, class: 'file_input')
# # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
- 9
def file_field(method, options = {})
self.multipart = true
@template.file_field(@object_name, method, objectify_options(options))
end
# Add the submit button for the given form. When no value is given, it checks
# if the object is a new resource or not to create the proper label:
#
# <%= form_for @post do |f| %>
# <%= f.submit %>
# <% end %>
#
# In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as
# submit button label; otherwise, it uses "Update Post".
#
# Those labels can be customized using I18n under the +helpers.submit+ key and using
# <tt>%{model}</tt> for translation interpolation:
#
# en:
# helpers:
# submit:
# create: "Create a %{model}"
# update: "Confirm changes to %{model}"
#
# It also searches for a key specific to the given object:
#
# en:
# helpers:
# submit:
# post:
# create: "Add %{model}"
#
- 9
def submit(value = nil, options = {})
value, options = nil, value if value.is_a?(Hash)
value ||= submit_default_value
@template.submit_tag(value, options)
end
# Add the submit button for the given form. When no value is given, it checks
# if the object is a new resource or not to create the proper label:
#
# <%= form_for @post do |f| %>
# <%= f.button %>
# <% end %>
#
# In the example above, if <tt>@post</tt> is a new record, it will use "Create Post" as
# button label; otherwise, it uses "Update Post".
#
# Those labels can be customized using I18n under the +helpers.submit+ key
# (the same as submit helper) and using <tt>%{model}</tt> for translation interpolation:
#
# en:
# helpers:
# submit:
# create: "Create a %{model}"
# update: "Confirm changes to %{model}"
#
# It also searches for a key specific to the given object:
#
# en:
# helpers:
# submit:
# post:
# create: "Add %{model}"
#
# ==== Examples
# button("Create post")
# # => <button name='button' type='submit'>Create post</button>
#
# button do
# content_tag(:strong, 'Ask me!')
# end
# # => <button name='button' type='submit'>
# # <strong>Ask me!</strong>
# # </button>
#
- 9
def button(value = nil, options = {}, &block)
value, options = nil, value if value.is_a?(Hash)
value ||= submit_default_value
@template.button_tag(value, options, &block)
end
- 9
def emitted_hidden_id? # :nodoc:
@emitted_hidden_id ||= nil
end
- 9
private
- 9
def objectify_options(options)
result = @default_options.merge(options)
result[:object] = @object
result
end
- 9
def submit_default_value
object = convert_to_model(@object)
key = object ? (object.persisted? ? :update : :create) : :submit
model = if object.respond_to?(:model_name)
object.model_name.human
else
@object_name.to_s.humanize
end
defaults = []
# Object is a model and it is not overwritten by as and scope option.
if object.respond_to?(:model_name) && object_name.to_s == model.downcase
defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}"
else
defaults << :"helpers.submit.#{object_name}.#{key}"
end
defaults << :"helpers.submit.#{key}"
defaults << "#{key.to_s.humanize} #{model}"
I18n.t(defaults.shift, model: model, default: defaults)
end
- 9
def nested_attributes_association?(association_name)
@object.respond_to?("#{association_name}_attributes=")
end
- 9
def fields_for_with_nested_attributes(association_name, association, options, block)
name = "#{object_name}[#{association_name}_attributes]"
association = convert_to_model(association)
if association.respond_to?(:persisted?)
association = [association] if @object.send(association_name).respond_to?(:to_ary)
elsif !association.respond_to?(:to_ary)
association = @object.send(association_name)
end
if association.respond_to?(:to_ary)
explicit_child_index = options[:child_index]
output = ActiveSupport::SafeBuffer.new
association.each do |child|
if explicit_child_index
options[:child_index] = explicit_child_index.call if explicit_child_index.respond_to?(:call)
else
options[:child_index] = nested_child_index(name)
end
output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
end
output
elsif association
fields_for_nested_model(name, association, options, block)
end
end
- 9
def fields_for_nested_model(name, object, fields_options, block)
object = convert_to_model(object)
emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
options.fetch(:include_id, true)
}
@template.fields_for(name, object, fields_options) do |f|
output = @template.capture(f, &block)
output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id?
output
end
end
- 9
def nested_child_index(name)
@nested_child_index[name] ||= -1
@nested_child_index[name] += 1
end
- 9
def convert_to_legacy_options(options)
if options.key?(:skip_id)
options[:include_id] = !options.delete(:skip_id)
end
end
end
end
- 9
ActiveSupport.on_load(:action_view) do
- 3
cattr_accessor :default_form_builder, instance_writer: false, instance_reader: false, default: ::ActionView::Helpers::FormBuilder
end
end
# frozen_string_literal: true
- 9
require "cgi"
- 9
require "erb"
- 9
require "action_view/helpers/form_helper"
- 9
require "active_support/core_ext/string/output_safety"
- 9
require "active_support/core_ext/array/extract_options"
- 9
require "active_support/core_ext/array/wrap"
- 9
module ActionView
# = Action View Form Option Helpers
- 9
module Helpers #:nodoc:
# Provides a number of methods for turning different kinds of containers into a set of option tags.
#
# The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
#
# * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element.
#
# select("post", "category", Post::CATEGORIES, { include_blank: true })
#
# could become:
#
# <select name="post[category]" id="post_category">
# <option value="" label=" "></option>
# <option value="joke">joke</option>
# <option value="poem">poem</option>
# </select>
#
# Another common case is a select tag for a <tt>belongs_to</tt>-associated object.
#
# Example with <tt>@post.person_id => 2</tt>:
#
# select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' })
#
# could become:
#
# <select name="post[person_id]" id="post_person_id">
# <option value="">None</option>
# <option value="1">David</option>
# <option value="2" selected="selected">Eileen</option>
# <option value="3">Rafael</option>
# </select>
#
# * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string.
#
# select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' })
#
# could become:
#
# <select name="post[person_id]" id="post_person_id">
# <option value="">Select Person</option>
# <option value="1">David</option>
# <option value="2">Eileen</option>
# <option value="3">Rafael</option>
# </select>
#
# * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
# option to be in the +html_options+ parameter.
#
# select("album[]", "genre", %w[rap rock country], {}, { index: nil })
#
# becomes:
#
# <select name="album[][genre]" id="album__genre">
# <option value="rap">rap</option>
# <option value="rock">rock</option>
# <option value="country">country</option>
# </select>
#
# * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output.
#
# select("post", "category", Post::CATEGORIES, { disabled: 'restricted' })
#
# could become:
#
# <select name="post[category]" id="post_category">
# <option value="joke">joke</option>
# <option value="poem">poem</option>
# <option disabled="disabled" value="restricted">restricted</option>
# </select>
#
# When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled.
#
# collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } })
#
# If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return:
# <select name="post[category_id]" id="post_category_id">
# <option value="1" disabled="disabled">2008 stuff</option>
# <option value="2" disabled="disabled">Christmas</option>
# <option value="3">Jokes</option>
# <option value="4">Poems</option>
# </select>
#
- 9
module FormOptionsHelper
# ERB::Util can mask some helpers like textilize. Make sure to include them.
- 9
include TextHelper
# Create a select tag and a series of contained option tags for the provided object and method.
# The option currently held by the object will be selected, provided that the object is available.
#
# There are two possible formats for the +choices+ parameter, corresponding to other helpers' output:
#
# * A flat collection (see +options_for_select+).
#
# * A nested collection (see +grouped_options_for_select+).
#
# For example:
#
# select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true })
#
# would become:
#
# <select name="post[person_id]" id="post_person_id">
# <option value="" label=" "></option>
# <option value="1" selected="selected">David</option>
# <option value="2">Eileen</option>
# <option value="3">Rafael</option>
# </select>
#
# assuming the associated person has ID 1.
#
# This can be used to provide a default set of options in the standard way: before rendering the create form, a
# new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
# to the database. Instead, a second model object is created when the create request is received.
# This allows the user to submit a form page more than once with the expected results of creating multiple records.
# In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
#
# By default, <tt>post.person_id</tt> is the selected option. Specify <tt>selected: value</tt> to use a different selection
# or <tt>selected: nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
# tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
#
# A block can be passed to +select+ to customize how the options tags will be rendered. This
# is useful when the options tag has complex attributes.
#
# select(report, "campaign_ids") do
# available_campaigns.each do |c|
# content_tag(:option, c.name, value: c.id, data: { tags: c.tags.to_json })
# end
# end
#
# ==== Gotcha
#
# The HTML specification says when +multiple+ parameter passed to select and all options got deselected
# web browsers do not send any value to server. Unfortunately this introduces a gotcha:
# if a +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user
# the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So,
# any mass-assignment idiom like
#
# @user.update(params[:user])
#
# wouldn't update roles.
#
# To prevent this the helper generates an auxiliary hidden field before
# every multiple select. The hidden field has the same name as multiple select and blank value.
#
# <b>Note:</b> The client either sends only the hidden field (representing
# the deselected multiple select box), or both fields. This means that the resulting array
# always contains a blank string.
#
# In case if you don't want the helper to generate this hidden field you can specify
# <tt>include_hidden: false</tt> option.
#
- 9
def select(object, method, choices = nil, options = {}, html_options = {}, &block)
Tags::Select.new(object, method, self, choices, options, html_options, &block).render
end
# Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
# +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
# be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
# or <tt>:include_blank</tt> in the +options+ hash.
#
# The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
# of +collection+. The return values are used as the +value+ attribute and contents of each
# <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such
# as a +proc+, that will be called for each member of the +collection+ to
# retrieve the value/text.
#
# Example object structure for use with this method:
#
# class Post < ActiveRecord::Base
# belongs_to :author
# end
#
# class Author < ActiveRecord::Base
# has_many :posts
# def name_with_initial
# "#{first_name.first}. #{last_name}"
# end
# end
#
# Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
#
# collection_select(:post, :author_id, Author.all, :id, :name_with_initial, prompt: true)
#
# If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
# <select name="post[author_id]" id="post_author_id">
# <option value="">Please select</option>
# <option value="1" selected="selected">D. Heinemeier Hansson</option>
# <option value="2">D. Thomas</option>
# <option value="3">M. Clark</option>
# </select>
- 9
def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render
end
# Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of
# +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
# be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
# or <tt>:include_blank</tt> in the +options+ hash.
#
# Parameters:
# * +object+ - The instance of the class to be used for the select tag
# * +method+ - The attribute of +object+ corresponding to the select tag
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the <tt><option></tt> tags. It can also be any object that responds
# to +call+, such as a +proc+, that will be called for each member of the +collection+ to retrieve the
# value.
# * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
# string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. It can also be any object
# that responds to +call+, such as a +proc+, that will be called for each member of the +collection+ to
# retrieve the label.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
# * +option_value_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
#
# Example object structure for use with this method:
#
# class Continent < ActiveRecord::Base
# has_many :countries
# # attribs: id, name
# end
#
# class Country < ActiveRecord::Base
# belongs_to :continent
# # attribs: id, name, continent_id
# end
#
# class City < ActiveRecord::Base
# belongs_to :country
# # attribs: id, name, country_id
# end
#
# Sample usage:
#
# grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
#
# Possible output:
#
# <select name="city[country_id]" id="city_country_id">
# <optgroup label="Africa">
# <option value="1">South Africa</option>
# <option value="3">Somalia</option>
# </optgroup>
# <optgroup label="Europe">
# <option value="7" selected="selected">Denmark</option>
# <option value="2">Ireland</option>
# </optgroup>
# </select>
#
- 9
def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render
end
# Returns select and option tags for the given object and method, using
# #time_zone_options_for_select to generate the list of option tags.
#
# In addition to the <tt>:include_blank</tt> option documented above,
# this method also supports a <tt>:model</tt> option, which defaults
# to ActiveSupport::TimeZone. This may be used by users to specify a
# different time zone model object. (See +time_zone_options_for_select+
# for more information.)
#
# You can also supply an array of ActiveSupport::TimeZone objects
# as +priority_zones+ so that they will be listed above the rest of the
# (long) list. You can use ActiveSupport::TimeZone.us_zones for a list
# of US time zones, ActiveSupport::TimeZone.country_zones(country_code)
# for another country's time zones, or a Regexp to select the zones of
# your choice.
#
# Finally, this method supports a <tt>:default</tt> option, which selects
# a default ActiveSupport::TimeZone if the object's time zone is +nil+.
#
# time_zone_select("user", "time_zone", nil, include_blank: true)
#
# time_zone_select("user", "time_zone", nil, default: "Pacific Time (US & Canada)")
#
# time_zone_select("user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
#
# time_zone_select("user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
#
# time_zone_select("user", 'time_zone', /Australia/)
#
# time_zone_select("user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
- 9
def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
end
# Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
# where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
# the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
# become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
# may also be an array of values to be selected when using a multiple select.
#
# options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
# # => <option value="$">Dollar</option>
# # => <option value="DKK">Kroner</option>
#
# options_for_select([ "VISA", "MasterCard" ], "MasterCard")
# # => <option value="VISA">VISA</option>
# # => <option selected="selected" value="MasterCard">MasterCard</option>
#
# options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
# # => <option value="$20">Basic</option>
# # => <option value="$40" selected="selected">Plus</option>
#
# options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
# # => <option selected="selected" value="VISA">VISA</option>
# # => <option value="MasterCard">MasterCard</option>
# # => <option selected="selected" value="Discover">Discover</option>
#
# You can optionally provide HTML attributes as the last element of the array.
#
# options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"])
# # => <option value="Denmark">Denmark</option>
# # => <option value="USA" class="bold" selected="selected">USA</option>
# # => <option value="Sweden" selected="selected">Sweden</option>
#
# options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]])
# # => <option value="$" class="bold">Dollar</option>
# # => <option value="DKK" onclick="alert('HI');">Kroner</option>
#
# If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
# or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
#
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: "Super Platinum")
# # => <option value="Free">Free</option>
# # => <option value="Basic">Basic</option>
# # => <option value="Advanced">Advanced</option>
# # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
#
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: ["Advanced", "Super Platinum"])
# # => <option value="Free">Free</option>
# # => <option value="Basic">Basic</option>
# # => <option value="Advanced" disabled="disabled">Advanced</option>
# # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
#
# options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], selected: "Free", disabled: "Super Platinum")
# # => <option value="Free" selected="selected">Free</option>
# # => <option value="Basic">Basic</option>
# # => <option value="Advanced">Advanced</option>
# # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
#
# NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
- 9
def options_for_select(container, selected = nil)
return container if String === container
selected, disabled = extract_selected_and_disabled(selected).map do |r|
Array(r).map(&:to_s)
end
container.map do |element|
html_attributes = option_html_attributes(element)
text, value = option_text_and_value(element).map(&:to_s)
html_attributes[:selected] ||= option_value_selected?(value, selected)
html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled)
html_attributes[:value] = value
tag_builder.content_tag_string(:option, text, html_attributes)
end.join("\n").html_safe
end
# Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning
# the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
#
# options_from_collection_for_select(@people, 'id', 'name')
# # => <option value="#{person.id}">#{person.name}</option>
#
# This is more often than not used inside a #select_tag like this example:
#
# select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
#
# If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
# will be selected option tag(s).
#
# If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
# function are the selected values.
#
# +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
#
# Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
# Failure to do this will produce undesired results. Example:
# options_from_collection_for_select(@people, 'id', 'name', '1')
# Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
# options_from_collection_for_select(@people, 'id', 'name', 1)
# should produce the desired results.
- 9
def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
options = collection.map do |element|
[value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
end
selected, disabled = extract_selected_and_disabled(selected)
select_deselect = {
selected: extract_values_from_collection(collection, value_method, selected),
disabled: extract_values_from_collection(collection, value_method, disabled)
}
options_for_select(options, select_deselect)
end
# Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
# groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
#
# Parameters:
# * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
# * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
# array of child objects representing the <tt><option></tt> tags.
# * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
# string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
# * +option_key_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
# * +option_value_method+ - The name of a method which, when called on a child object of a member of
# +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
# * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
# which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
# to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
# to be specified.
#
# Example object structure for use with this method:
#
# class Continent < ActiveRecord::Base
# has_many :countries
# # attribs: id, name
# end
#
# class Country < ActiveRecord::Base
# belongs_to :continent
# # attribs: id, name, continent_id
# end
#
# Sample usage:
# option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
#
# Possible output:
# <optgroup label="Africa">
# <option value="1">Egypt</option>
# <option value="4">Rwanda</option>
# ...
# </optgroup>
# <optgroup label="Asia">
# <option value="3" selected="selected">China</option>
# <option value="12">India</option>
# <option value="5">Japan</option>
# ...
# </optgroup>
#
# <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
# wrap the output in an appropriate <tt><select></tt> tag.
- 9
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
collection.map do |group|
option_tags = options_from_collection_for_select(
value_for_collection(group, group_method), option_key_method, option_value_method, selected_key)
content_tag("optgroup", option_tags, label: value_for_collection(group, group_label_method))
end.join.html_safe
end
# Returns a string of <tt><option></tt> tags, like <tt>options_for_select</tt>, but
# wraps them with <tt><optgroup></tt> tags:
#
# grouped_options = [
# ['North America',
# [['United States','US'],'Canada']],
# ['Europe',
# ['Denmark','Germany','France']]
# ]
# grouped_options_for_select(grouped_options)
#
# grouped_options = {
# 'North America' => [['United States','US'], 'Canada'],
# 'Europe' => ['Denmark','Germany','France']
# }
# grouped_options_for_select(grouped_options)
#
# Possible output:
# <optgroup label="North America">
# <option value="US">United States</option>
# <option value="Canada">Canada</option>
# </optgroup>
# <optgroup label="Europe">
# <option value="Denmark">Denmark</option>
# <option value="Germany">Germany</option>
# <option value="France">France</option>
# </optgroup>
#
# Parameters:
# * +grouped_options+ - Accepts a nested array or hash of strings. The first value serves as the
# <tt><optgroup></tt> label while the second value must be an array of options. The second value can be a
# nested array of text-value pairs. See <tt>options_for_select</tt> for more info.
# Ex. ["North America",[["United States","US"],["Canada","CA"]]]
# * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
# which will have the +selected+ attribute set. Note: It is possible for this value to match multiple options
# as you might have the same option in multiple groups. Each will then get <tt>selected="selected"</tt>.
#
# Options:
# * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this
# prepends an option with a generic prompt - "Please select" - or the given prompt string.
# * <tt>:divider</tt> - the divider for the options groups.
#
# grouped_options = [
# [['United States','US'], 'Canada'],
# ['Denmark','Germany','France']
# ]
# grouped_options_for_select(grouped_options, nil, divider: '---------')
#
# Possible output:
# <optgroup label="---------">
# <option value="US">United States</option>
# <option value="Canada">Canada</option>
# </optgroup>
# <optgroup label="---------">
# <option value="Denmark">Denmark</option>
# <option value="Germany">Germany</option>
# <option value="France">France</option>
# </optgroup>
#
# <b>Note:</b> Only the <tt><optgroup></tt> and <tt><option></tt> tags are returned, so you still have to
# wrap the output in an appropriate <tt><select></tt> tag.
- 9
def grouped_options_for_select(grouped_options, selected_key = nil, options = {})
prompt = options[:prompt]
divider = options[:divider]
body = "".html_safe
if prompt
body.safe_concat content_tag("option", prompt_text(prompt), value: "")
end
grouped_options.each do |container|
html_attributes = option_html_attributes(container)
if divider
label = divider
else
label, container = container
end
html_attributes = { label: label }.merge!(html_attributes)
body.safe_concat content_tag("optgroup", options_for_select(container, selected_key), html_attributes)
end
body
end
# Returns a string of option tags for pretty much any time zone in the
# world. Supply an ActiveSupport::TimeZone name as +selected+ to have it
# marked as the selected option tag. You can also supply an array of
# ActiveSupport::TimeZone objects as +priority_zones+, so that they will
# be listed above the rest of the (long) list. (You can use
# ActiveSupport::TimeZone.us_zones as a convenience for obtaining a list
# of the US time zones, or a Regexp to select the zones of your choice)
#
# The +selected+ parameter must be either +nil+, or a string that names
# an ActiveSupport::TimeZone.
#
# By default, +model+ is the ActiveSupport::TimeZone constant (which can
# be obtained in Active Record as a value object). The +model+ parameter
# must respond to +all+ and return an array of objects that represent time
# zones; each object must respond to +name+. If a Regexp is given it will
# attempt to match the zones using <code>match?</code> method.
#
# NOTE: Only the option tags are returned, you have to wrap this call in
# a regular HTML select tag.
- 9
def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
zone_options = "".html_safe
zones = model.all
convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
if priority_zones
if priority_zones.is_a?(Regexp)
priority_zones = zones.select { |z| z.match?(priority_zones) }
end
zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
zone_options.safe_concat content_tag("option", "-------------", value: "", disabled: true)
zone_options.safe_concat "\n"
zones = zones - priority_zones
end
zone_options.safe_concat options_for_select(convert_zones[zones], selected)
end
# Returns radio button tags for the collection of existing return values
# of +method+ for +object+'s class. The value returned from calling
# +method+ on the instance +object+ will be selected. If calling +method+
# returns +nil+, no selection is made.
#
# The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
# methods to be called on each member of +collection+. The return values
# are used as the +value+ attribute and contents of each radio button tag,
# respectively. They can also be any object that responds to +call+, such
# as a +proc+, that will be called for each member of the +collection+ to
# retrieve the value/text.
#
# Example object structure for use with this method:
# class Post < ActiveRecord::Base
# belongs_to :author
# end
# class Author < ActiveRecord::Base
# has_many :posts
# def name_with_initial
# "#{first_name.first}. #{last_name}"
# end
# end
#
# Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
#
# If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
# <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" />
# <label for="post_author_id_1">D. Heinemeier Hansson</label>
# <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
# <label for="post_author_id_2">D. Thomas</label>
# <input id="post_author_id_3" name="post[author_id]" type="radio" value="3" />
# <label for="post_author_id_3">M. Clark</label>
#
# It is also possible to customize the way the elements will be shown by
# giving a block to the method:
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label { b.radio_button }
# end
#
# The argument passed to the block is a special kind of builder for this
# collection, which has the ability to generate the label and radio button
# for the current item in the collection, with proper text and value.
# Using it, you can change the label and radio button display order or
# even use the label as wrapper, as in the example above.
#
# The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept
# extra HTML options:
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(class: "radio_button") { b.radio_button(class: "radio_button") }
# end
#
# There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
# <tt>value</tt>, which are the current item being rendered, its text and value methods,
# respectively. You can use them like this:
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.radio_button + b.text }
# end
#
# ==== Gotcha
#
# The HTML specification says when nothing is selected on a collection of radio buttons
# web browsers do not send any value to server.
# Unfortunately this introduces a gotcha:
# if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So,
# any strong parameters idiom like:
#
# params.require(:user).permit(...)
#
# will raise an error since no <tt>{user: ...}</tt> will be present.
#
# To prevent this the helper generates an auxiliary hidden field before
# every collection of radio buttons. The hidden field has the same name as collection radio button and blank value.
#
# In case if you don't want the helper to generate this hidden field you can specify
# <tt>include_hidden: false</tt> option.
- 9
def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
# Returns check box tags for the collection of existing return values of
# +method+ for +object+'s class. The value returned from calling +method+
# on the instance +object+ will be selected. If calling +method+ returns
# +nil+, no selection is made.
#
# The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
# methods to be called on each member of +collection+. The return values
# are used as the +value+ attribute and contents of each check box tag,
# respectively. They can also be any object that responds to +call+, such
# as a +proc+, that will be called for each member of the +collection+ to
# retrieve the value/text.
#
# Example object structure for use with this method:
# class Post < ActiveRecord::Base
# has_and_belongs_to_many :authors
# end
# class Author < ActiveRecord::Base
# has_and_belongs_to_many :posts
# def name_with_initial
# "#{first_name.first}. #{last_name}"
# end
# end
#
# Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
# collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
#
# If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return:
# <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" />
# <label for="post_author_ids_1">D. Heinemeier Hansson</label>
# <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
# <label for="post_author_ids_2">D. Thomas</label>
# <input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" />
# <label for="post_author_ids_3">M. Clark</label>
# <input name="post[author_ids][]" type="hidden" value="" />
#
# It is also possible to customize the way the elements will be shown by
# giving a block to the method:
# collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
# b.label { b.check_box }
# end
#
# The argument passed to the block is a special kind of builder for this
# collection, which has the ability to generate the label and check box
# for the current item in the collection, with proper text and value.
# Using it, you can change the label and check box display order or even
# use the label as wrapper, as in the example above.
#
# The builder methods <tt>label</tt> and <tt>check_box</tt> also accept
# extra HTML options:
# collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
# b.label(class: "check_box") { b.check_box(class: "check_box") }
# end
#
# There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
# <tt>value</tt>, which are the current item being rendered, its text and value methods,
# respectively. You can use them like this:
# collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.check_box + b.text }
# end
#
# ==== Gotcha
#
# When no selection is made for a collection of checkboxes most
# web browsers will not send any value.
#
# For example, if we have a +User+ model with +category_ids+ field and we
# have the following code in our update action:
#
# @user.update(params[:user])
#
# If no +category_ids+ are selected then we can safely assume this field
# will not be updated.
#
# This is possible thanks to a hidden field generated by the helper method
# for every collection of checkboxes.
# This hidden field is given the same field name as the checkboxes with a
# blank value.
#
# In the rare case you don't want this hidden field, you can pass the
# <tt>include_hidden: false</tt> option to the helper method.
- 9
def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
- 9
private
- 9
def option_html_attributes(element)
if Array === element
element.select { |e| Hash === e }.reduce({}, :merge!)
else
{}
end
end
- 9
def option_text_and_value(option)
# Options are [text, value] pairs or strings used for both.
if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
option = option.reject { |e| Hash === e } if Array === option
[option.first, option.last]
else
[option, option]
end
end
- 9
def option_value_selected?(value, selected)
Array(selected).include? value
end
- 9
def extract_selected_and_disabled(selected)
if selected.is_a?(Proc)
[selected, nil]
else
selected = Array.wrap(selected)
options = selected.extract_options!.symbolize_keys
selected_items = options.fetch(:selected, selected)
[selected_items, options[:disabled]]
end
end
- 9
def extract_values_from_collection(collection, value_method, selected)
if selected.is_a?(Proc)
collection.map do |element|
public_or_deprecated_send(element, value_method) if selected.call(element)
end.compact
else
selected
end
end
- 9
def value_for_collection(item, value)
value.respond_to?(:call) ? value.call(item) : public_or_deprecated_send(item, value)
end
- 9
def public_or_deprecated_send(item, value)
item.public_send(value)
rescue NoMethodError
raise unless item.respond_to?(value, true) && !item.respond_to?(value)
ActiveSupport::Deprecation.warn "Using private methods from view helpers is deprecated (calling private #{item.class}##{value})"
item.send(value)
end
- 9
def prompt_text(prompt)
prompt.kind_of?(String) ? prompt : I18n.translate("helpers.select.prompt", default: "Please select")
end
end
- 9
class FormBuilder
# Wraps ActionView::Helpers::FormOptionsHelper#select for form builders:
#
# <%= form_for @post do |f| %>
# <%= f.select :person_id, Person.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def select(method, choices = nil, options = {}, html_options = {}, &block)
@template.select(@object_name, method, choices, objectify_options(options), @default_html_options.merge(html_options), &block)
end
# Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders:
#
# <%= form_for @post do |f| %>
# <%= f.collection_select :person_id, Author.all, :id, :name_with_initial, prompt: true %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
@template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options))
end
# Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders:
#
# <%= form_for @city do |f| %>
# <%= f.grouped_collection_select :country_id, @continents, :countries, :name, :id, :name %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
@template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_html_options.merge(html_options))
end
# Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders:
#
# <%= form_for @user do |f| %>
# <%= f.time_zone_select :time_zone, nil, include_blank: true %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
@template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_html_options.merge(html_options))
end
# Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders:
#
# <%= form_for @post do |f| %>
# <%= f.collection_check_boxes :author_ids, Author.all, :id, :name_with_initial %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
@template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
end
# Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders:
#
# <%= form_for @post do |f| %>
# <%= f.collection_radio_buttons :author_id, Author.all, :id, :name_with_initial %>
# <%= f.submit %>
# <% end %>
#
# Please refer to the documentation of the base helper for details.
- 9
def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
@template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block)
end
end
end
end
# frozen_string_literal: true
- 9
require "cgi"
- 9
require "action_view/helpers/tag_helper"
- 9
require "active_support/core_ext/string/output_safety"
- 9
require "active_support/core_ext/module/attribute_accessors"
- 9
require "active_support/core_ext/symbol/starts_ends_with"
- 9
module ActionView
# = Action View Form Tag Helpers
- 9
module Helpers #:nodoc:
# Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like
# FormHelper does. Instead, you provide the names and values manually.
#
# NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying
# <tt>disabled: true</tt> will give <tt>disabled="disabled"</tt>.
- 9
module FormTagHelper
- 9
extend ActiveSupport::Concern
- 9
include UrlHelper
- 9
include TextHelper
- 9
mattr_accessor :embed_authenticity_token_in_remote_forms
- 9
self.embed_authenticity_token_in_remote_forms = nil
- 9
mattr_accessor :default_enforce_utf8, default: true
# Starts a form tag that points the action to a URL configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
# ==== Options
# * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
# * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
# If "patch", "put", "delete", or another verb is used, a hidden input with name <tt>_method</tt>
# is added to simulate the verb over post.
# * <tt>:authenticity_token</tt> - Authenticity token to use in the form. Use only if you need to
# pass custom authenticity token string, or to not add authenticity_token field at all
# (by passing <tt>false</tt>). Remote forms may omit the embedded authenticity token
# by setting <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
# This is helpful when you're fragment-caching the form. Remote forms get the
# authenticity token from the <tt>meta</tt> tag, so embedding is unnecessary unless you
# support browsers without JavaScript.
# * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
# submit behavior. By default this behavior is an ajax submit.
# * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# form_tag('/posts')
# # => <form action="/posts" method="post">
#
# form_tag('/posts/1', method: :put)
# # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ...
#
# form_tag('/upload', multipart: true)
# # => <form action="/upload" method="post" enctype="multipart/form-data">
#
# <%= form_tag('/posts') do -%>
# <div><%= submit_tag 'Save' %></div>
# <% end -%>
# # => <form action="/posts" method="post"><div><input type="submit" name="commit" value="Save" /></div></form>
#
# <%= form_tag('/posts', remote: true) %>
# # => <form action="/posts" method="post" data-remote="true">
#
# form_tag('http://far.away.com/form', authenticity_token: false)
# # form without authenticity token
#
# form_tag('http://far.away.com/form', authenticity_token: "cf50faa3fe97702ca1ae")
# # form with custom authenticity token
#
- 9
def form_tag(url_for_options = {}, options = {}, &block)
html_options = html_options_for_form(url_for_options, options)
if block_given?
form_tag_with_body(html_options, capture(&block))
else
form_tag_html(html_options)
end
end
# Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
# choice selection box.
#
# Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or
# associated records. <tt>option_tags</tt> is a string containing the option tags for the select box.
#
# ==== Options
# * <tt>:multiple</tt> - If set to true, the selection will allow multiple choices.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:include_blank</tt> - If set to true, an empty option will be created. If set to a string, the string will be used as the option's content and the value will be empty.
# * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# select_tag "people", options_from_collection_for_select(@people, "id", "name")
# # <select id="people" name="people"><option value="1">David</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name", "1")
# # <select id="people" name="people"><option value="1" selected="selected">David</option></select>
#
# select_tag "people", raw("<option>David</option>")
# # => <select id="people" name="people"><option>David</option></select>
#
# select_tag "count", raw("<option>1</option><option>2</option><option>3</option><option>4</option>")
# # => <select id="count" name="count"><option>1</option><option>2</option>
# # <option>3</option><option>4</option></select>
#
# select_tag "colors", raw("<option>Red</option><option>Green</option><option>Blue</option>"), multiple: true
# # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option>
# # <option>Green</option><option>Blue</option></select>
#
# select_tag "locations", raw("<option>Home</option><option selected='selected'>Work</option><option>Out</option>")
# # => <select id="locations" name="locations"><option>Home</option><option selected='selected'>Work</option>
# # <option>Out</option></select>
#
# select_tag "access", raw("<option>Read</option><option>Write</option>"), multiple: true, class: 'form_input', id: 'unique_id'
# # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option>
# # <option>Write</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true
# # => <select id="people" name="people"><option value="" label=" "></option><option value="1">David</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All"
# # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select>
#
# select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something"
# # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select>
#
# select_tag "destination", raw("<option>NYC</option><option>Paris</option><option>Rome</option>"), disabled: true
# # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
# # <option>Paris</option><option>Rome</option></select>
#
# select_tag "credit_card", options_for_select([ "VISA", "MasterCard" ], "MasterCard")
# # => <select id="credit_card" name="credit_card"><option>VISA</option>
# # <option selected="selected">MasterCard</option></select>
- 9
def select_tag(name, option_tags = nil, options = {})
option_tags ||= ""
html_name = (options[:multiple] == true && !name.end_with?("[]")) ? "#{name}[]" : name
if options.include?(:include_blank)
include_blank = options[:include_blank]
options = options.except(:include_blank)
options_for_blank_options_tag = { value: "" }
if include_blank == true
include_blank = ""
options_for_blank_options_tag[:label] = " "
end
if include_blank
option_tags = content_tag("option", include_blank, options_for_blank_options_tag).safe_concat(option_tags)
end
end
if prompt = options.delete(:prompt)
option_tags = content_tag("option", prompt, value: "").safe_concat(option_tags)
end
content_tag "select", option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
end
# Creates a standard text field; use these text fields to input smaller chunks of text like a username
# or a search query.
#
# ==== Options
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:size</tt> - The number of visible characters that will fit in the input.
# * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
# * <tt>:placeholder</tt> - The text contained in the field by default which is removed when the field receives focus.
# If set to true, use a translation is found in the current I18n locale
# (through helpers.placeholders.<modelname>.<attribute>).
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# text_field_tag 'name'
# # => <input id="name" name="name" type="text" />
#
# text_field_tag 'query', 'Enter your search query here'
# # => <input id="query" name="query" type="text" value="Enter your search query here" />
#
# text_field_tag 'search', nil, placeholder: 'Enter search term...'
# # => <input id="search" name="search" placeholder="Enter search term..." type="text" />
#
# text_field_tag 'request', nil, class: 'special_input'
# # => <input class="special_input" id="request" name="request" type="text" />
#
# text_field_tag 'address', '', size: 75
# # => <input id="address" name="address" size="75" type="text" value="" />
#
# text_field_tag 'zip', nil, maxlength: 5
# # => <input id="zip" maxlength="5" name="zip" type="text" />
#
# text_field_tag 'payment_amount', '$0.00', disabled: true
# # => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" />
#
# text_field_tag 'ip', '0.0.0.0', maxlength: 15, size: 20, class: "ip-input"
# # => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" />
- 9
def text_field_tag(name, value = nil, options = {})
tag :input, { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
end
# Creates a label element. Accepts a block.
#
# ==== Options
# * Creates standard HTML attributes for the tag.
#
# ==== Examples
# label_tag 'name'
# # => <label for="name">Name</label>
#
# label_tag 'name', 'Your name'
# # => <label for="name">Your name</label>
#
# label_tag 'name', nil, class: 'small_label'
# # => <label for="name" class="small_label">Name</label>
- 9
def label_tag(name = nil, content_or_options = nil, options = nil, &block)
if block_given? && content_or_options.is_a?(Hash)
options = content_or_options = content_or_options.stringify_keys
else
options ||= {}
options = options.stringify_keys
end
options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for")
content_tag :label, content_or_options || name.to_s.humanize, options, &block
end
# Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or
# data that should be hidden from the user.
#
# ==== Options
# * Creates standard HTML attributes for the tag.
#
# ==== Examples
# hidden_field_tag 'tags_list'
# # => <input id="tags_list" name="tags_list" type="hidden" />
#
# hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
# # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
#
# hidden_field_tag 'collected_input', '', onchange: "alert('Input collected!')"
# # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
# # type="hidden" value="" />
- 9
def hidden_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :hidden))
end
# Creates a file upload field. If you are using file uploads then you will also need
# to set the multipart option for the form tag:
#
# <%= form_tag '/upload', multipart: true do %>
# <label for="file">File to Upload</label> <%= file_field_tag "file" %>
# <%= submit_tag %>
# <% end %>
#
# The specified URL will then be passed a File object containing the selected file, or if the field
# was left blank, a StringIO object.
#
# ==== Options
# * Creates standard HTML attributes for the tag.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
# * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
#
# ==== Examples
# file_field_tag 'attachment'
# # => <input id="attachment" name="attachment" type="file" />
#
# file_field_tag 'avatar', class: 'profile_input'
# # => <input class="profile_input" id="avatar" name="avatar" type="file" />
#
# file_field_tag 'picture', disabled: true
# # => <input disabled="disabled" id="picture" name="picture" type="file" />
#
# file_field_tag 'resume', value: '~/resume.doc'
# # => <input id="resume" name="resume" type="file" value="~/resume.doc" />
#
# file_field_tag 'user_pic', accept: 'image/png,image/gif,image/jpeg'
# # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" />
#
# file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
# # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
- 9
def file_field_tag(name, options = {})
text_field_tag(name, nil, convert_direct_upload_option_to_url(options.merge(type: :file)))
end
# Creates a password field, a masked text field that will hide the users input behind a mask character.
#
# ==== Options
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:size</tt> - The number of visible characters that will fit in the input.
# * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# password_field_tag 'pass'
# # => <input id="pass" name="pass" type="password" />
#
# password_field_tag 'secret', 'Your secret here'
# # => <input id="secret" name="secret" type="password" value="Your secret here" />
#
# password_field_tag 'masked', nil, class: 'masked_input_field'
# # => <input class="masked_input_field" id="masked" name="masked" type="password" />
#
# password_field_tag 'token', '', size: 15
# # => <input id="token" name="token" size="15" type="password" value="" />
#
# password_field_tag 'key', nil, maxlength: 16
# # => <input id="key" maxlength="16" name="key" type="password" />
#
# password_field_tag 'confirm_pass', nil, disabled: true
# # => <input disabled="disabled" id="confirm_pass" name="confirm_pass" type="password" />
#
# password_field_tag 'pin', '1234', maxlength: 4, size: 6, class: "pin_input"
# # => <input class="pin_input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" />
- 9
def password_field_tag(name = "password", value = nil, options = {})
text_field_tag(name, value, options.merge(type: :password))
end
# Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
#
# ==== Options
# * <tt>:size</tt> - A string specifying the dimensions (columns by rows) of the textarea (e.g., "25x10").
# * <tt>:rows</tt> - Specify the number of rows in the textarea
# * <tt>:cols</tt> - Specify the number of columns in the textarea
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped.
# If you need unescaped contents, set this to false.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# text_area_tag 'post'
# # => <textarea id="post" name="post"></textarea>
#
# text_area_tag 'bio', @user.bio
# # => <textarea id="bio" name="bio">This is my biography.</textarea>
#
# text_area_tag 'body', nil, rows: 10, cols: 25
# # => <textarea cols="25" id="body" name="body" rows="10"></textarea>
#
# text_area_tag 'body', nil, size: "25x10"
# # => <textarea name="body" id="body" cols="25" rows="10"></textarea>
#
# text_area_tag 'description', "Description goes here.", disabled: true
# # => <textarea disabled="disabled" id="description" name="description">Description goes here.</textarea>
#
# text_area_tag 'comment', nil, class: 'comment_input'
# # => <textarea class="comment_input" id="comment" name="comment"></textarea>
- 9
def text_area_tag(name, content = nil, options = {})
options = options.stringify_keys
if size = options.delete("size")
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
escape = options.delete("escape") { true }
content = ERB::Util.html_escape(content) if escape
content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options)
end
# Creates a check box form input tag.
#
# ==== Options
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * Any other key creates standard HTML options for the tag.
#
# ==== Examples
# check_box_tag 'accept'
# # => <input id="accept" name="accept" type="checkbox" value="1" />
#
# check_box_tag 'rock', 'rock music'
# # => <input id="rock" name="rock" type="checkbox" value="rock music" />
#
# check_box_tag 'receive_email', 'yes', true
# # => <input checked="checked" id="receive_email" name="receive_email" type="checkbox" value="yes" />
#
# check_box_tag 'tos', 'yes', false, class: 'accept_tos'
# # => <input class="accept_tos" id="tos" name="tos" type="checkbox" value="yes" />
#
# check_box_tag 'eula', 'accepted', false, disabled: true
# # => <input disabled="disabled" id="eula" name="eula" type="checkbox" value="accepted" />
- 9
def check_box_tag(name, value = "1", checked = false, options = {})
html_options = { "type" => "checkbox", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
html_options["checked"] = "checked" if checked
tag :input, html_options
end
# Creates a radio button; use groups of radio buttons named the same to allow users to
# select from a group of options.
#
# ==== Options
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * Any other key creates standard HTML options for the tag.
#
# ==== Examples
# radio_button_tag 'favorite_color', 'maroon'
# # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" />
#
# radio_button_tag 'receive_updates', 'no', true
# # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" />
#
# radio_button_tag 'time_slot', "3:00 p.m.", false, disabled: true
# # => <input disabled="disabled" id="time_slot_3:00_p.m." name="time_slot" type="radio" value="3:00 p.m." />
#
# radio_button_tag 'color', "green", true, class: "color_input"
# # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" />
- 9
def radio_button_tag(name, value, checked = false, options = {})
html_options = { "type" => "radio", "name" => name, "id" => "#{sanitize_to_id(name)}_#{sanitize_to_id(value)}", "value" => value }.update(options.stringify_keys)
html_options["checked"] = "checked" if checked
tag :input, html_options
end
# Creates a submit button with the text <tt>value</tt> as the caption.
#
# ==== Options
# * <tt>:data</tt> - This option can be used to add custom data attributes.
# * <tt>:disabled</tt> - If true, the user will not be able to use this input.
# * Any other key creates standard HTML options for the tag.
#
# ==== Data attributes
#
# * <tt>confirm: 'question?'</tt> - If present the unobtrusive JavaScript
# drivers will provide a prompt with the question specified. If the user accepts,
# the form is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
# disabled version of the submit button when the form is submitted. This feature is
# provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag
# pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute.
#
# ==== Examples
# submit_tag
# # => <input name="commit" data-disable-with="Save changes" type="submit" value="Save changes" />
#
# submit_tag "Edit this article"
# # => <input name="commit" data-disable-with="Edit this article" type="submit" value="Edit this article" />
#
# submit_tag "Save edits", disabled: true
# # => <input disabled="disabled" name="commit" data-disable-with="Save edits" type="submit" value="Save edits" />
#
# submit_tag "Complete sale", data: { disable_with: "Submitting..." }
# # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" />
#
# submit_tag nil, class: "form_submit"
# # => <input class="form_submit" name="commit" type="submit" />
#
# submit_tag "Edit", class: "edit_button"
# # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" />
#
# submit_tag "Save", data: { confirm: "Are you sure?" }
# # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" />
#
- 9
def submit_tag(value = "Save changes", options = {})
options = options.deep_stringify_keys
tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
set_default_disable_with value, tag_options
tag :input, tag_options
end
# Creates a button element that defines a <tt>submit</tt> button,
# <tt>reset</tt> button or a generic button which can be used in
# JavaScript, for example. You can use the button tag as a regular
# submit tag but it isn't supported in legacy browsers. However,
# the button tag does allow for richer labels such as images and emphasis,
# so this helper will also accept a block. By default, it will create
# a button tag with type <tt>submit</tt>, if type is not given.
#
# ==== Options
# * <tt>:data</tt> - This option can be used to add custom data attributes.
# * <tt>:disabled</tt> - If true, the user will not be able to
# use this input.
# * Any other key creates standard HTML options for the tag.
#
# ==== Data attributes
#
# * <tt>confirm: 'question?'</tt> - If present, the
# unobtrusive JavaScript drivers will provide a prompt with
# the question specified. If the user accepts, the form is
# processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be
# used as the value for a disabled version of the submit
# button when the form is submitted. This feature is provided
# by the unobtrusive JavaScript driver.
#
# ==== Examples
# button_tag
# # => <button name="button" type="submit">Button</button>
#
# button_tag 'Reset', type: 'reset'
# # => <button name="button" type="reset">Reset</button>
#
# button_tag 'Button', type: 'button'
# # => <button name="button" type="button">Button</button>
#
# button_tag 'Reset', type: 'reset', disabled: true
# # => <button name="button" type="reset" disabled="disabled">Reset</button>
#
# button_tag(type: 'button') do
# content_tag(:strong, 'Ask me!')
# end
# # => <button name="button" type="button">
# # <strong>Ask me!</strong>
# # </button>
#
# button_tag "Save", data: { confirm: "Are you sure?" }
# # => <button name="button" type="submit" data-confirm="Are you sure?">Save</button>
#
# button_tag "Checkout", data: { disable_with: "Please wait..." }
# # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button>
#
- 9
def button_tag(content_or_options = nil, options = nil, &block)
if content_or_options.is_a? Hash
options = content_or_options
else
options ||= {}
end
options = { "name" => "button", "type" => "submit" }.merge!(options.stringify_keys)
if block_given?
content_tag :button, options, &block
else
content_tag :button, content_or_options || "Button", options
end
end
# Displays an image which when clicked will submit the form.
#
# <tt>source</tt> is passed to AssetTagHelper#path_to_image
#
# ==== Options
# * <tt>:data</tt> - This option can be used to add custom data attributes.
# * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
# * Any other key creates standard HTML options for the tag.
#
# ==== Data attributes
#
# * <tt>confirm: 'question?'</tt> - This will add a JavaScript confirm
# prompt with the question specified. If the user accepts, the form is
# processed normally, otherwise no action is taken.
#
# ==== Examples
# image_submit_tag("login.png")
# # => <input src="/assets/login.png" type="image" />
#
# image_submit_tag("purchase.png", disabled: true)
# # => <input disabled="disabled" src="/assets/purchase.png" type="image" />
#
# image_submit_tag("search.png", class: 'search_button', alt: 'Find')
# # => <input class="search_button" src="/assets/search.png" type="image" />
#
# image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button")
# # => <input class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" />
#
# image_submit_tag("save.png", data: { confirm: "Are you sure?" })
# # => <input src="/assets/save.png" data-confirm="Are you sure?" type="image" />
- 9
def image_submit_tag(source, options = {})
options = options.stringify_keys
src = path_to_image(source, skip_pipeline: options.delete("skip_pipeline"))
tag :input, { "type" => "image", "src" => src }.update(options)
end
# Creates a field set for grouping HTML form elements.
#
# <tt>legend</tt> will become the fieldset's title (optional as per W3C).
# <tt>options</tt> accept the same values as tag.
#
# ==== Examples
# <%= field_set_tag do %>
# <p><%= text_field_tag 'name' %></p>
# <% end %>
# # => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
#
# <%= field_set_tag 'Your details' do %>
# <p><%= text_field_tag 'name' %></p>
# <% end %>
# # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset>
#
# <%= field_set_tag nil, class: 'format' do %>
# <p><%= text_field_tag 'name' %></p>
# <% end %>
# # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
- 9
def field_set_tag(legend = nil, options = nil, &block)
output = tag(:fieldset, options, true)
output.safe_concat(content_tag("legend", legend)) unless legend.blank?
output.concat(capture(&block)) if block_given?
output.safe_concat("</fieldset>")
end
# Creates a text field of type "color".
#
# ==== Options
# * Accepts the same options as text_field_tag.
#
# ==== Examples
# color_field_tag 'name'
# # => <input id="name" name="name" type="color" />
#
# color_field_tag 'color', '#DEF726'
# # => <input id="color" name="color" type="color" value="#DEF726" />
#
# color_field_tag 'color', nil, class: 'special_input'
# # => <input class="special_input" id="color" name="color" type="color" />
#
# color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" />
- 9
def color_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :color))
end
# Creates a text field of type "search".
#
# ==== Options
# * Accepts the same options as text_field_tag.
#
# ==== Examples
# search_field_tag 'name'
# # => <input id="name" name="name" type="search" />
#
# search_field_tag 'search', 'Enter your search query here'
# # => <input id="search" name="search" type="search" value="Enter your search query here" />
#
# search_field_tag 'search', nil, class: 'special_input'
# # => <input class="special_input" id="search" name="search" type="search" />
#
# search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" />
- 9
def search_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :search))
end
# Creates a text field of type "tel".
#
# ==== Options
# * Accepts the same options as text_field_tag.
#
# ==== Examples
# telephone_field_tag 'name'
# # => <input id="name" name="name" type="tel" />
#
# telephone_field_tag 'tel', '0123456789'
# # => <input id="tel" name="tel" type="tel" value="0123456789" />
#
# telephone_field_tag 'tel', nil, class: 'special_input'
# # => <input class="special_input" id="tel" name="tel" type="tel" />
#
# telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" />
- 9
def telephone_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :tel))
end
- 9
alias phone_field_tag telephone_field_tag
# Creates a text field of type "date".
#
# ==== Options
# * Accepts the same options as text_field_tag.
#
# ==== Examples
# date_field_tag 'name'
# # => <input id="name" name="name" type="date" />
#
# date_field_tag 'date', '01/01/2014'
# # => <input id="date" name="date" type="date" value="01/01/2014" />
#
# date_field_tag 'date', nil, class: 'special_input'
# # => <input class="special_input" id="date" name="date" type="date" />
#
# date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" />
- 9
def date_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :date))
end
# Creates a text field of type "time".
#
# === Options
# * <tt>:min</tt> - The minimum acceptable value.
# * <tt>:max</tt> - The maximum acceptable value.
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
- 9
def time_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :time))
end
# Creates a text field of type "datetime-local".
#
# === Options
# * <tt>:min</tt> - The minimum acceptable value.
# * <tt>:max</tt> - The maximum acceptable value.
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
- 9
def datetime_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: "datetime-local"))
end
- 9
alias datetime_local_field_tag datetime_field_tag
# Creates a text field of type "month".
#
# === Options
# * <tt>:min</tt> - The minimum acceptable value.
# * <tt>:max</tt> - The maximum acceptable value.
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
- 9
def month_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :month))
end
# Creates a text field of type "week".
#
# === Options
# * <tt>:min</tt> - The minimum acceptable value.
# * <tt>:max</tt> - The maximum acceptable value.
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
- 9
def week_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :week))
end
# Creates a text field of type "url".
#
# ==== Options
# * Accepts the same options as text_field_tag.
#
# ==== Examples
# url_field_tag 'name'
# # => <input id="name" name="name" type="url" />
#
# url_field_tag 'url', 'http://rubyonrails.org'
# # => <input id="url" name="url" type="url" value="http://rubyonrails.org" />
#
# url_field_tag 'url', nil, class: 'special_input'
# # => <input class="special_input" id="url" name="url" type="url" />
#
# url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" />
- 9
def url_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :url))
end
# Creates a text field of type "email".
#
# ==== Options
# * Accepts the same options as text_field_tag.
#
# ==== Examples
# email_field_tag 'name'
# # => <input id="name" name="name" type="email" />
#
# email_field_tag 'email', 'email@example.com'
# # => <input id="email" name="email" type="email" value="email@example.com" />
#
# email_field_tag 'email', nil, class: 'special_input'
# # => <input class="special_input" id="email" name="email" type="email" />
#
# email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" />
- 9
def email_field_tag(name, value = nil, options = {})
text_field_tag(name, value, options.merge(type: :email))
end
# Creates a number field.
#
# ==== Options
# * <tt>:min</tt> - The minimum acceptable value.
# * <tt>:max</tt> - The maximum acceptable value.
# * <tt>:in</tt> - A range specifying the <tt>:min</tt> and
# <tt>:max</tt> values.
# * <tt>:within</tt> - Same as <tt>:in</tt>.
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
#
# ==== Examples
# number_field_tag 'quantity'
# # => <input id="quantity" name="quantity" type="number" />
#
# number_field_tag 'quantity', '1'
# # => <input id="quantity" name="quantity" type="number" value="1" />
#
# number_field_tag 'quantity', nil, class: 'special_input'
# # => <input class="special_input" id="quantity" name="quantity" type="number" />
#
# number_field_tag 'quantity', nil, min: 1
# # => <input id="quantity" name="quantity" min="1" type="number" />
#
# number_field_tag 'quantity', nil, max: 9
# # => <input id="quantity" name="quantity" max="9" type="number" />
#
# number_field_tag 'quantity', nil, in: 1...10
# # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
#
# number_field_tag 'quantity', nil, within: 1...10
# # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
#
# number_field_tag 'quantity', nil, min: 1, max: 10
# # => <input id="quantity" name="quantity" min="1" max="10" type="number" />
#
# number_field_tag 'quantity', nil, min: 1, max: 10, step: 2
# # => <input id="quantity" name="quantity" min="1" max="10" step="2" type="number" />
#
# number_field_tag 'quantity', '1', class: 'special_input', disabled: true
# # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" />
- 9
def number_field_tag(name, value = nil, options = {})
options = options.stringify_keys
options["type"] ||= "number"
if range = options.delete("in") || options.delete("within")
options.update("min" => range.min, "max" => range.max)
end
text_field_tag(name, value, options)
end
# Creates a range form element.
#
# ==== Options
# * Accepts the same options as number_field_tag.
- 9
def range_field_tag(name, value = nil, options = {})
number_field_tag(name, value, options.merge(type: :range))
end
# Creates the hidden UTF8 enforcer tag. Override this method in a helper
# to customize the tag.
- 9
def utf8_enforcer_tag
# Use raw HTML to ensure the value is written as an HTML entity; it
# needs to be the right character regardless of which encoding the
# browser infers.
'<input name="utf8" type="hidden" value="✓" />'.html_safe
end
- 9
private
- 9
def html_options_for_form(url_for_options, options)
options.stringify_keys.tap do |html_options|
html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
# The following URL is unescaped, this is just a hash of options, and it is the
# responsibility of the caller to escape all the values.
html_options["action"] = url_for(url_for_options)
html_options["accept-charset"] = "UTF-8"
html_options["data-remote"] = true if html_options.delete("remote")
if html_options["data-remote"] &&
!embed_authenticity_token_in_remote_forms &&
html_options["authenticity_token"].blank?
# The authenticity token is taken from the meta tag in this case
html_options["authenticity_token"] = false
elsif html_options["authenticity_token"] == true
# Include the default authenticity_token, which is only generated when its set to nil,
# but we needed the true value to override the default of no authenticity_token on data-remote.
html_options["authenticity_token"] = nil
end
end
end
- 9
def extra_tags_for_form(html_options)
authenticity_token = html_options.delete("authenticity_token")
method = html_options.delete("method").to_s.downcase
method_tag = \
case method
when "get"
html_options["method"] = "get"
""
when "post", ""
html_options["method"] = "post"
token_tag(authenticity_token, form_options: {
action: html_options["action"],
method: "post"
})
else
html_options["method"] = "post"
method_tag(method) + token_tag(authenticity_token, form_options: {
action: html_options["action"],
method: method
})
end
if html_options.delete("enforce_utf8") { default_enforce_utf8 }
utf8_enforcer_tag + method_tag
else
method_tag
end
end
- 9
def form_tag_html(html_options)
extra_tags = extra_tags_for_form(html_options)
tag(:form, html_options, true) + extra_tags
end
- 9
def form_tag_with_body(html_options, content)
output = form_tag_html(html_options)
output << content
output.safe_concat("</form>")
end
# see http://www.w3.org/TR/html4/types.html#type-name
- 9
def sanitize_to_id(name)
name.to_s.delete("]").tr("^-a-zA-Z0-9:.", "_")
end
- 9
def set_default_disable_with(value, tag_options)
return unless ActionView::Base.automatically_disable_submit_tag
data = tag_options["data"]
unless tag_options["data-disable-with"] == false || (data && data["disable_with"] == false)
disable_with_text = tag_options["data-disable-with"]
disable_with_text ||= data["disable_with"] if data
disable_with_text ||= value.to_s.clone
tag_options.deep_merge!("data" => { "disable_with" => disable_with_text })
else
data.delete("disable_with") if data
end
tag_options.delete("data-disable-with")
end
- 9
def convert_direct_upload_option_to_url(options)
if options.delete(:direct_upload) && respond_to?(:rails_direct_uploads_url)
options["data-direct-upload-url"] = rails_direct_uploads_url
end
options
end
end
end
end
# frozen_string_literal: true
- 9
require "action_view/helpers/tag_helper"
- 9
module ActionView
- 9
module Helpers #:nodoc:
- 9
module JavaScriptHelper
- 9
JS_ESCAPE_MAP = {
'\\' => '\\\\',
"</" => '<\/',
"\r\n" => '\n',
"\n" => '\n',
"\r" => '\n',
'"' => '\\"',
"'" => "\\'",
"`" => "\\`",
"$" => "\\$"
}
- 9
JS_ESCAPE_MAP[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "
"
- 9
JS_ESCAPE_MAP[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "
"
# Escapes carriage returns and single and double quotes for JavaScript segments.
#
# Also available through the alias j(). This is particularly helpful in JavaScript
# responses, like:
#
# $('some_element').replaceWith('<%= j render 'some/element_template' %>');
- 9
def escape_javascript(javascript)
javascript = javascript.to_s
if javascript.empty?
result = ""
else
result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"']|[`]|[$])/u, JS_ESCAPE_MAP)
end
javascript.html_safe? ? result.html_safe : result
end
- 9
alias_method :j, :escape_javascript
# Returns a JavaScript tag with the +content+ inside. Example:
# javascript_tag "alert('All is good')"
#
# Returns:
# <script>
# //<![CDATA[
# alert('All is good')
# //]]>
# </script>
#
# +html_options+ may be a hash of attributes for the <tt>\<script></tt>
# tag.
#
# javascript_tag "alert('All is good')", type: 'application/javascript'
#
# Returns:
# <script type="application/javascript">
# //<![CDATA[
# alert('All is good')
# //]]>
# </script>
#
# Instead of passing the content as an argument, you can also use a block
# in which case, you pass your +html_options+ as the first parameter.
#
# <%= javascript_tag type: 'application/javascript' do -%>
# alert('All is good')
# <% end -%>
#
# If you have a content security policy enabled then you can add an automatic
# nonce value by passing <tt>nonce: true</tt> as part of +html_options+. Example:
#
# <%= javascript_tag nonce: true do -%>
# alert('All is good')
# <% end -%>
- 9
def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
content =
if block_given?
html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
capture(&block)
else
content_or_options_with_block
end
if html_options[:nonce] == true
html_options[:nonce] = content_security_policy_nonce
end
content_tag("script", javascript_cdata_section(content), html_options)
end
- 9
def javascript_cdata_section(content) #:nodoc:
"\n//#{cdata_section("\n#{content}\n//")}\n".html_safe
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/hash/keys"
- 9
require "active_support/core_ext/string/output_safety"
- 9
require "active_support/number_helper"
- 9
module ActionView
# = Action View Number Helpers
- 9
module Helpers #:nodoc:
# Provides methods for converting numbers into formatted strings.
# Methods are provided for phone numbers, currency, percentage,
# precision, positional notation, file size and pretty printing.
#
# Most methods expect a +number+ argument, and will return it
# unchanged if can't be converted into a valid number.
- 9
module NumberHelper
# Raised when argument +number+ param given to the helpers is invalid and
# the option :raise is set to +true+.
- 9
class InvalidNumberError < StandardError
- 9
attr_accessor :number
- 9
def initialize(number)
@number = number
end
end
# Formats a +number+ into a phone number (US by default e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
#
# ==== Options
#
# * <tt>:area_code</tt> - Adds parentheses around the area code.
# * <tt>:delimiter</tt> - Specifies the delimiter to use
# (defaults to "-").
# * <tt>:extension</tt> - Specifies an extension to add to the
# end of the generated number.
# * <tt>:country_code</tt> - Sets the country code for the phone
# number.
# * <tt>:pattern</tt> - Specifies how the number is divided into three
# groups with the custom regexp to override the default format.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
# ==== Examples
#
# number_to_phone(5551234) # => 555-1234
# number_to_phone("5551234") # => 555-1234
# number_to_phone(1235551234) # => 123-555-1234
# number_to_phone(1235551234, area_code: true) # => (123) 555-1234
# number_to_phone(1235551234, delimiter: " ") # => 123 555 1234
# number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555
# number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234
# number_to_phone("123a456") # => 123a456
# number_to_phone("1234a567", raise: true) # => InvalidNumberError
#
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".")
# # => +1.123.555.1234 x 1343
#
# number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
# # => "(755) 6123-4567"
# number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/)
# # => "133-1234-5678"
- 9
def number_to_phone(number, options = {})
return unless number
options = options.symbolize_keys
parse_float(number, true) if options.delete(:raise)
ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options))
end
# Formats a +number+ into a currency string (e.g., $13.65). You
# can customize the format in the +options+ hash.
#
# The currency unit and number formatting of the current locale will be used
# unless otherwise specified in the provided options. No currency conversion
# is performed. If the user is given a way to change their locale, they will
# also be able to change the relative value of the currency displayed with
# this helper. If your application will ever support multiple locales, you
# may want to specify a constant <tt>:locale</tt> option or consider
# using a library capable of currency conversion.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the level of precision (defaults
# to 2).
# * <tt>:unit</tt> - Sets the denomination of the currency
# (defaults to "$").
# * <tt>:separator</tt> - Sets the separator between the units
# (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to ",").
# * <tt>:format</tt> - Sets the format for non-negative numbers
# (defaults to "%u%n"). Fields are <tt>%u</tt> for the
# currency, and <tt>%n</tt> for the number.
# * <tt>:negative_format</tt> - Sets the format for negative
# numbers (defaults to prepending a hyphen to the formatted
# number given by <tt>:format</tt>). Accepts the same fields
# than <tt>:format</tt>, except <tt>%n</tt> is here the
# absolute value of the number.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +false+).
#
# ==== Examples
#
# number_to_currency(1234567890.50) # => $1,234,567,890.50
# number_to_currency(1234567890.506) # => $1,234,567,890.51
# number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506
# number_to_currency(1234567890.506, locale: :fr) # => 1 234 567 890,51 €
# number_to_currency("123a456") # => $123a456
#
# number_to_currency("123a456", raise: true) # => InvalidNumberError
#
# number_to_currency(-0.456789, precision: 0)
# # => "$0"
# number_to_currency(-1234567890.50, negative_format: "(%u%n)")
# # => ($1,234,567,890.50)
# number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "")
# # => R$1234567890,50
# number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u")
# # => 1234567890,50 R$
# number_to_currency(1234567890.50, strip_insignificant_zeros: true)
# # => "$1,234,567,890.5"
- 9
def number_to_currency(number, options = {})
delegate_number_helper_method(:number_to_currency, number, options)
end
# Formats a +number+ as a percentage string (e.g., 65%). You can
# customize the format in the +options+ hash.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +false+).
# * <tt>:format</tt> - Specifies the format of the percentage
# string The number field is <tt>%n</tt> (defaults to "%n%").
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
# ==== Examples
#
# number_to_percentage(100) # => 100.000%
# number_to_percentage("98") # => 98.000%
# number_to_percentage(100, precision: 0) # => 100%
# number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
# number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
# number_to_percentage(1000, locale: :fr) # => 1 000,000%
# number_to_percentage("98a") # => 98a%
# number_to_percentage(100, format: "%n %") # => 100.000 %
#
# number_to_percentage("98a", raise: true) # => InvalidNumberError
- 9
def number_to_percentage(number, options = {})
delegate_number_helper_method(:number_to_percentage, number, options)
end
# Formats a +number+ with grouped thousands using +delimiter+
# (e.g., 12,324). You can customize the format in the +options+
# hash.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to ",").
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for
# deriving the placement of delimiter. Helpful when using currency formats
# like INR.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
# ==== Examples
#
# number_with_delimiter(12345678) # => 12,345,678
# number_with_delimiter("123456") # => 123,456
# number_with_delimiter(12345678.05) # => 12,345,678.05
# number_with_delimiter(12345678, delimiter: ".") # => 12.345.678
# number_with_delimiter(12345678, delimiter: ",") # => 12,345,678
# number_with_delimiter(12345678.05, separator: " ") # => 12,345,678 05
# number_with_delimiter(12345678.05, locale: :fr) # => 12 345 678,05
# number_with_delimiter("112a") # => 112a
# number_with_delimiter(98765432.98, delimiter: " ", separator: ",")
# # => 98 765 432,98
#
# number_with_delimiter("123456.78",
# delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) # => "1,23,456.78"
#
# number_with_delimiter("112a", raise: true) # => raise InvalidNumberError
- 9
def number_with_delimiter(number, options = {})
delegate_number_helper_method(:number_to_delimited, number, options)
end
# Formats a +number+ with the specified level of
# <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if
# +:significant+ is +false+, and 5 if +:significant+ is +true+).
# You can customize the format in the +options+ hash.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +false+).
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +false+).
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
# ==== Examples
#
# number_with_precision(111.2345) # => 111.235
# number_with_precision(111.2345, precision: 2) # => 111.23
# number_with_precision(13, precision: 5) # => 13.00000
# number_with_precision(389.32314, precision: 0) # => 389
# number_with_precision(111.2345, significant: true) # => 111
# number_with_precision(111.2345, precision: 1, significant: true) # => 100
# number_with_precision(13, precision: 5, significant: true) # => 13.000
# number_with_precision(111.234, locale: :fr) # => 111,234
#
# number_with_precision(13, precision: 5, significant: true, strip_insignificant_zeros: true)
# # => 13
#
# number_with_precision(389.32314, precision: 4, significant: true) # => 389.3
# number_with_precision(1111.2345, precision: 2, separator: ',', delimiter: '.')
# # => 1.111,23
- 9
def number_with_precision(number, options = {})
delegate_number_helper_method(:number_to_rounded, number, options)
end
# Formats the bytes in +number+ into a more understandable
# representation (e.g., giving it 1500 yields 1.46 KB). This
# method is useful for reporting file sizes to users. You can
# customize the format in the +options+ hash.
#
# See <tt>number_to_human</tt> if you want to pretty-print a
# generic number.
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +true+)
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
# ==== Examples
#
# number_to_human_size(123) # => 123 Bytes
# number_to_human_size(1234) # => 1.21 KB
# number_to_human_size(12345) # => 12.1 KB
# number_to_human_size(1234567) # => 1.18 MB
# number_to_human_size(1234567890) # => 1.15 GB
# number_to_human_size(1234567890123) # => 1.12 TB
# number_to_human_size(1234567890123456) # => 1.1 PB
# number_to_human_size(1234567890123456789) # => 1.07 EB
# number_to_human_size(1234567, precision: 2) # => 1.2 MB
# number_to_human_size(483989, precision: 2) # => 470 KB
# number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
# number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
# number_to_human_size(524288000, precision: 5) # => "500 MB"
- 9
def number_to_human_size(number, options = {})
delegate_number_helper_method(:number_to_human_size, number, options)
end
# Pretty prints (formats and approximates) a number in a way it
# is more readable by humans (e.g.: 1200000000 becomes "1.2
# Billion"). This is useful for numbers that can get very large
# (and too hard to read).
#
# See <tt>number_to_human_size</tt> if you want to print a file
# size.
#
# You can also define your own unit-quantifier names if you want
# to use other decimal units (e.g.: 1500 becomes "1.5
# kilometers", 0.150 becomes "150 milliliters", etc). You may
# define a wide range of unit quantifiers, even fractional ones
# (centi, deci, mili, etc).
#
# ==== Options
#
# * <tt>:locale</tt> - Sets the locale to be used for formatting
# (defaults to current locale).
# * <tt>:precision</tt> - Sets the precision of the number
# (defaults to 3).
# * <tt>:significant</tt> - If +true+, precision will be the number
# of significant_digits. If +false+, the number of fractional
# digits (defaults to +true+)
# * <tt>:separator</tt> - Sets the separator between the
# fractional and integer digits (defaults to ".").
# * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
# to "").
# * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
# insignificant zeros after the decimal separator (defaults to
# +true+)
# * <tt>:units</tt> - A Hash of unit quantifier names. Or a
# string containing an i18n scope where to find this hash. It
# might have the following keys:
# * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
# <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
# <tt>:billion</tt>, <tt>:trillion</tt>,
# <tt>:quadrillion</tt>
# * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
# <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
# <tt>:pico</tt>, <tt>:femto</tt>
# * <tt>:format</tt> - Sets the format of the output string
# (defaults to "%n %u"). The field types are:
# * %u - The quantifier (ex.: 'thousand')
# * %n - The number
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
# ==== Examples
#
# number_to_human(123) # => "123"
# number_to_human(1234) # => "1.23 Thousand"
# number_to_human(12345) # => "12.3 Thousand"
# number_to_human(1234567) # => "1.23 Million"
# number_to_human(1234567890) # => "1.23 Billion"
# number_to_human(1234567890123) # => "1.23 Trillion"
# number_to_human(1234567890123456) # => "1.23 Quadrillion"
# number_to_human(1234567890123456789) # => "1230 Quadrillion"
# number_to_human(489939, precision: 2) # => "490 Thousand"
# number_to_human(489939, precision: 4) # => "489.9 Thousand"
# number_to_human(1234567, precision: 4,
# significant: false) # => "1.2346 Million"
# number_to_human(1234567, precision: 1,
# separator: ',',
# significant: false) # => "1,2 Million"
#
# number_to_human(500000000, precision: 5) # => "500 Million"
# number_to_human(12345012345, significant: false) # => "12.345 Billion"
#
# Non-significant zeros after the decimal separator are stripped
# out by default (set <tt>:strip_insignificant_zeros</tt> to
# +false+ to change that):
#
# number_to_human(12.00001) # => "12"
# number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
#
# ==== Custom Unit Quantifiers
#
# You can also use your own custom unit quantifiers:
# number_to_human(500000, units: {unit: "ml", thousand: "lt"}) # => "500 lt"
#
# If in your I18n locale you have:
# distance:
# centi:
# one: "centimeter"
# other: "centimeters"
# unit:
# one: "meter"
# other: "meters"
# thousand:
# one: "kilometer"
# other: "kilometers"
# billion: "gazillion-distance"
#
# Then you could do:
#
# number_to_human(543934, units: :distance) # => "544 kilometers"
# number_to_human(54393498, units: :distance) # => "54400 kilometers"
# number_to_human(54393498000, units: :distance) # => "54.4 gazillion-distance"
# number_to_human(343, units: :distance, precision: 1) # => "300 meters"
# number_to_human(1, units: :distance) # => "1 meter"
# number_to_human(0.34, units: :distance) # => "34 centimeters"
#
- 9
def number_to_human(number, options = {})
delegate_number_helper_method(:number_to_human, number, options)
end
- 9
private
- 9
def delegate_number_helper_method(method, number, options)
return unless number
options = escape_unsafe_options(options.symbolize_keys)
wrap_with_output_safety_handling(number, options.delete(:raise)) {
ActiveSupport::NumberHelper.public_send(method, number, options)
}
end
- 9
def escape_unsafe_options(options)
options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
options
end
- 9
def escape_units(units)
units.transform_values do |v|
ERB::Util.html_escape(v)
end
end
- 9
def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
valid_float = valid_float?(number)
raise InvalidNumberError, number if raise_on_invalid && !valid_float
formatted_number = yield
if valid_float || number.html_safe?
formatted_number.html_safe
else
formatted_number
end
end
- 9
def valid_float?(number)
!parse_float(number, false).nil?
end
- 9
def parse_float(number, raise_error)
Float(number)
rescue ArgumentError, TypeError
raise InvalidNumberError, number if raise_error
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/string/output_safety"
- 9
module ActionView #:nodoc:
# = Action View Raw Output Helper
- 9
module Helpers #:nodoc:
- 9
module OutputSafetyHelper
# This method outputs without escaping a string. Since escaping tags is
# now default, this can be used when you don't want Rails to automatically
# escape tags. This is not recommended if the data is coming from the user's
# input.
#
# For example:
#
# raw @user.name
# # => 'Jimmy <alert>Tables</alert>'
- 9
def raw(stringish)
stringish.to_s.html_safe
end
# This method returns an HTML safe string similar to what <tt>Array#join</tt>
# would return. The array is flattened, and all items, including
# the supplied separator, are HTML escaped unless they are HTML
# safe, and the returned string is marked as HTML safe.
#
# safe_join([raw("<p>foo</p>"), "<p>bar</p>"], "<br />")
# # => "<p>foo</p><br /><p>bar</p>"
#
# safe_join([raw("<p>foo</p>"), raw("<p>bar</p>")], raw("<br />"))
# # => "<p>foo</p><br /><p>bar</p>"
#
- 9
def safe_join(array, sep = $,)
sep = ERB::Util.unwrapped_html_escape(sep)
array.flatten.map! { |i| ERB::Util.unwrapped_html_escape(i) }.join(sep).html_safe
end
# Converts the array to a comma-separated sentence where the last element is
# joined by the connector word. This is the html_safe-aware version of
# ActiveSupport's {Array#to_sentence}[https://api.rubyonrails.org/classes/Array.html#method-i-to_sentence].
#
- 9
def to_sentence(array, options = {})
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
default_connectors = {
words_connector: ", ",
two_words_connector: " and ",
last_word_connector: ", and "
}
if defined?(I18n)
i18n_connectors = I18n.translate(:'support.array', locale: options[:locale], default: {})
default_connectors.merge!(i18n_connectors)
end
options = default_connectors.merge!(options)
case array.length
when 0
"".html_safe
when 1
ERB::Util.html_escape(array[0])
when 2
safe_join([array[0], array[1]], options[:two_words_connector])
else
safe_join([safe_join(array[0...-1], options[:words_connector]), options[:last_word_connector], array[-1]], nil)
end
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
- 9
module Helpers #:nodoc:
# = Action View Rendering
#
# Implements methods that allow rendering from a view context.
# In order to use this module, all you need is to implement
# view_renderer that returns an ActionView::Renderer object.
- 9
module RenderingHelper
# Returns the result of a render that's dictated by the options hash. The primary options are:
#
# * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>.
# * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
# * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
# * <tt>:plain</tt> - Renders the text passed in out. Setting the content
# type as <tt>text/plain</tt>.
# * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise
# performs HTML escape on the string first. Setting the content type as
# <tt>text/html</tt>.
# * <tt>:body</tt> - Renders the text passed in, and inherits the content
# type of <tt>text/plain</tt> from <tt>ActionDispatch::Response</tt>
# object.
#
# If no <tt>options</tt> hash is passed or if <tt>:update</tt> is specified, then:
#
# If an object responding to `render_in` is passed, `render_in` is called on the object,
# passing in the current view context.
#
# Otherwise, a partial is rendered using the second parameter as the locals hash.
- 9
def render(options = {}, locals = {}, &block)
case options
when Hash
in_rendering_context(options) do |renderer|
if block_given?
view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
else
view_renderer.render(self, options)
end
end
else
if options.respond_to?(:render_in)
options.render_in(self, &block)
else
view_renderer.render_partial(self, partial: options, locals: locals, &block)
end
end
end
# Overwrites _layout_for in the context object so it supports the case a block is
# passed to a partial. Returns the contents that are yielded to a layout, given a
# name or a block.
#
# You can think of a layout as a method that is called with a block. If the user calls
# <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>.
# If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>.
#
# The user can override this default by passing a block to the layout:
#
# # The template
# <%= render layout: "my_layout" do %>
# Content
# <% end %>
#
# # The layout
# <html>
# <%= yield %>
# </html>
#
# In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>,
# this method returns the block that was passed in to <tt>render :layout</tt>, and the response
# would be
#
# <html>
# Content
# </html>
#
# Finally, the block can take block arguments, which can be passed in by +yield+:
#
# # The template
# <%= render layout: "my_layout" do |customer| %>
# Hello <%= customer.name %>
# <% end %>
#
# # The layout
# <html>
# <%= yield Struct.new(:name).new("David") %>
# </html>
#
# In this case, the layout would receive the block passed into <tt>render :layout</tt>,
# and the struct specified would be passed into the block as an argument. The result
# would be
#
# <html>
# Hello David
# </html>
#
- 9
def _layout_for(*args, &block)
name = args.first
if block && !name.is_a?(Symbol)
capture(*args, &block)
else
super
end
end
end
end
end
# frozen_string_literal: true
- 9
require "rails-html-sanitizer"
- 9
module ActionView
# = Action View Sanitize Helpers
- 9
module Helpers #:nodoc:
# The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
# These helper methods extend Action View making them callable within your template files.
- 9
module SanitizeHelper
- 9
extend ActiveSupport::Concern
# Sanitizes HTML input, stripping all but known-safe tags and attributes.
#
# It also strips href/src attributes with unsafe protocols like
# <tt>javascript:</tt>, while also protecting against attempts to use Unicode,
# ASCII, and hex character references to work around these protocol filters.
# All special characters will be escaped.
#
# The default sanitizer is Rails::Html::SafeListSanitizer. See {Rails HTML
# Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information.
#
# Custom sanitization rules can also be provided.
#
# Please note that sanitizing user-provided text does not guarantee that the
# resulting markup is valid or even well-formed.
#
# ==== Options
#
# * <tt>:tags</tt> - An array of allowed tags.
# * <tt>:attributes</tt> - An array of allowed attributes.
# * <tt>:scrubber</tt> - A {Rails::Html scrubber}[https://github.com/rails/rails-html-sanitizer]
# or {Loofah::Scrubber}[https://github.com/flavorjones/loofah] object that
# defines custom sanitization rules. A custom scrubber takes precedence over
# custom tags and attributes.
#
# ==== Examples
#
# Normal use:
#
# <%= sanitize @comment.body %>
#
# Providing custom lists of permitted tags and attributes:
#
# <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %>
#
# Providing a custom Rails::Html scrubber:
#
# class CommentScrubber < Rails::Html::PermitScrubber
# def initialize
# super
# self.tags = %w( form script comment blockquote )
# self.attributes = %w( style )
# end
#
# def skip_node?(node)
# node.text?
# end
# end
#
# <%= sanitize @comment.body, scrubber: CommentScrubber.new %>
#
# See {Rails HTML Sanitizer}[https://github.com/rails/rails-html-sanitizer] for
# documentation about Rails::Html scrubbers.
#
# Providing a custom Loofah::Scrubber:
#
# scrubber = Loofah::Scrubber.new do |node|
# node.remove if node.name == 'script'
# end
#
# <%= sanitize @comment.body, scrubber: scrubber %>
#
# See {Loofah's documentation}[https://github.com/flavorjones/loofah] for more
# information about defining custom Loofah::Scrubber objects.
#
# To set the default allowed tags or attributes across your application:
#
# # In config/application.rb
# config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
# config.action_view.sanitized_allowed_attributes = ['href', 'title']
- 9
def sanitize(html, options = {})
self.class.safe_list_sanitizer.sanitize(html, options)&.html_safe
end
# Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
- 9
def sanitize_css(style)
self.class.safe_list_sanitizer.sanitize_css(style)
end
# Strips all HTML tags from +html+, including comments and special characters.
#
# strip_tags("Strip <i>these</i> tags!")
# # => Strip these tags!
#
# strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
# # => Bold no more! See more here...
#
# strip_tags("<div id='top-bar'>Welcome to my website!</div>")
# # => Welcome to my website!
#
# strip_tags("> A quote from Smith & Wesson")
# # => > A quote from Smith & Wesson
- 9
def strip_tags(html)
self.class.full_sanitizer.sanitize(html)
end
# Strips all link tags from +html+ leaving just the link text.
#
# strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
# # => Ruby on Rails
#
# strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.')
# # => Please e-mail me at me@email.com.
#
# strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
# # => Blog: Visit.
#
# strip_links('<<a href="https://example.org">malformed & link</a>')
# # => <malformed & link
- 9
def strip_links(html)
self.class.link_sanitizer.sanitize(html)
end
- 9
module ClassMethods #:nodoc:
- 9
attr_writer :full_sanitizer, :link_sanitizer, :safe_list_sanitizer
- 9
def sanitizer_vendor
Rails::Html::Sanitizer
end
- 9
def sanitized_allowed_tags
safe_list_sanitizer.allowed_tags
end
- 9
def sanitized_allowed_attributes
safe_list_sanitizer.allowed_attributes
end
# Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
# any object that responds to +sanitize+.
#
# class Application < Rails::Application
# config.action_view.full_sanitizer = MySpecialSanitizer.new
# end
- 9
def full_sanitizer
@full_sanitizer ||= sanitizer_vendor.full_sanitizer.new
end
# Gets the Rails::Html::LinkSanitizer instance used by +strip_links+.
# Replace with any object that responds to +sanitize+.
#
# class Application < Rails::Application
# config.action_view.link_sanitizer = MySpecialSanitizer.new
# end
- 9
def link_sanitizer
@link_sanitizer ||= sanitizer_vendor.link_sanitizer.new
end
# Gets the Rails::Html::SafeListSanitizer instance used by sanitize and +sanitize_css+.
# Replace with any object that responds to +sanitize+.
#
# class Application < Rails::Application
# config.action_view.safe_list_sanitizer = MySpecialSanitizer.new
# end
- 9
def safe_list_sanitizer
@safe_list_sanitizer ||= sanitizer_vendor.safe_list_sanitizer.new
end
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/string/output_safety"
- 9
require "set"
- 9
module ActionView
# = Action View Tag Helpers
- 9
module Helpers #:nodoc:
# Provides methods to generate HTML tags programmatically both as a modern
# HTML5 compliant builder style and legacy XHTML compliant tags.
- 9
module TagHelper
- 9
extend ActiveSupport::Concern
- 9
include CaptureHelper
- 9
include OutputSafetyHelper
- 9
BOOLEAN_ATTRIBUTES = %w(allowfullscreen allowpaymentrequest async autofocus
autoplay checked compact controls declare default
defaultchecked defaultmuted defaultselected defer
disabled enabled formnovalidate hidden indeterminate
inert ismap itemscope loop multiple muted nohref
nomodule noresize noshade novalidate nowrap open
pauseonexit playsinline readonly required reversed
scoped seamless selected sortable truespeed
typemustmatch visible).to_set
- 9
BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym))
- 9
BOOLEAN_ATTRIBUTES.freeze
- 9
TAG_PREFIXES = ["aria", "data", :aria, :data].to_set.freeze
- 9
TAG_TYPES = {}
- 9
TAG_TYPES.merge! BOOLEAN_ATTRIBUTES.index_with(:boolean)
- 9
TAG_TYPES.merge! TAG_PREFIXES.index_with(:prefix)
- 9
TAG_TYPES.freeze
- 9
PRE_CONTENT_STRINGS = Hash.new { "" }
- 9
PRE_CONTENT_STRINGS[:textarea] = "\n"
- 9
PRE_CONTENT_STRINGS["textarea"] = "\n"
- 9
class TagBuilder #:nodoc:
- 9
include CaptureHelper
- 9
include OutputSafetyHelper
- 9
VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set
- 9
def initialize(view_context)
@view_context = view_context
end
- 9
def tag_string(name, content = nil, escape_attributes: true, **options, &block)
content = @view_context.capture(self, &block) if block_given?
if VOID_ELEMENTS.include?(name) && content.nil?
"<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe
else
content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes)
end
end
- 9
def content_tag_string(name, content, options, escape = true)
tag_options = tag_options(options, escape) if options
content = ERB::Util.unwrapped_html_escape(content) if escape
"<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
end
- 9
def tag_options(options, escape = true)
return if options.blank?
output = +""
sep = " "
options.each_pair do |key, value|
type = TAG_TYPES[key]
if type == :prefix && value.is_a?(Hash)
value.each_pair do |k, v|
next if v.nil?
output << sep
output << prefix_tag_option(key, k, v, escape)
end
elsif type == :boolean
if value
output << sep
output << boolean_tag_option(key)
end
elsif !value.nil?
output << sep
output << tag_option(key, value, escape)
end
end
output unless output.empty?
end
- 9
def boolean_tag_option(key)
%(#{key}="#{key}")
end
- 9
def tag_option(key, value, escape)
case value
when Array, Hash
value = TagHelper.build_tag_values(value) if key.to_s == "class"
value = escape ? safe_join(value, " ") : value.join(" ")
else
value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
end
value = value.gsub('"', """) if value.include?('"')
%(#{key}="#{value}")
end
- 9
private
- 9
def prefix_tag_option(prefix, key, value, escape)
key = "#{prefix}-#{key.to_s.dasherize}"
unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
value = value.to_json
end
tag_option(key, value, escape)
end
- 9
def respond_to_missing?(*args)
true
end
- 9
def method_missing(called, *args, **options, &block)
tag_string(called, *args, **options, &block)
end
end
# Returns an HTML tag.
#
# === Building HTML tags
#
# Builds HTML5 compliant tags with a tag proxy. Every tag can be built with:
#
# tag.<tag name>(optional content, options)
#
# where tag name can be e.g. br, div, section, article, or any tag really.
#
# ==== Passing content
#
# Tags can pass content to embed within it:
#
# tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1>
#
# tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div>
#
# Content can also be captured with a block, which is useful in templates:
#
# <%= tag.p do %>
# The next great American novel starts here.
# <% end %>
# # => <p>The next great American novel starts here.</p>
#
# ==== Options
#
# Use symbol keyed options to add attributes to the generated tag.
#
# tag.section class: %w( kitties puppies )
# # => <section class="kitties puppies"></section>
#
# tag.section id: dom_id(@post)
# # => <section id="<generated dom id>"></section>
#
# Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+.
#
# tag.input type: 'text', disabled: true
# # => <input type="text" disabled="disabled">
#
# HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
# pointing to a hash of sub-attributes.
#
# To play nicely with JavaScript conventions, sub-attributes are dasherized.
#
# tag.article data: { user_id: 123 }
# # => <article data-user-id="123"></article>
#
# Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>.
#
# Data attribute values are encoded to JSON, with the exception of strings, symbols and
# BigDecimals.
# This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
# from 1.4.3.
#
# tag.div data: { city_state: %w( Chicago IL ) }
# # => <div data-city-state="["Chicago","IL"]"></div>
#
# The generated attributes are escaped by default. This can be disabled using
# +escape_attributes+.
#
# tag.img src: 'open & shut.png'
# # => <img src="open & shut.png">
#
# tag.img src: 'open & shut.png', escape_attributes: false
# # => <img src="open & shut.png">
#
# The tag builder respects
# {HTML5 void elements}[https://www.w3.org/TR/html5/syntax.html#void-elements]
# if no content is passed, and omits closing tags for those elements.
#
# # A standard element:
# tag.div # => <div></div>
#
# # A void element:
# tag.br # => <br>
#
# === Legacy syntax
#
# The following format is for legacy syntax support. It will be deprecated in future versions of Rails.
#
# tag(name, options = nil, open = false, escape = true)
#
# It returns an empty HTML tag of type +name+ which by default is XHTML
# compliant. Set +open+ to true to create an open tag compatible
# with HTML 4.0 and below. Add HTML attributes by passing an attributes
# hash to +options+. Set +escape+ to false to disable attribute value
# escaping.
#
# ==== Options
#
# You can use symbols or strings for the attribute names.
#
# Use +true+ with boolean attributes that can render with no value, like
# +disabled+ and +readonly+.
#
# HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
# pointing to a hash of sub-attributes.
#
# ==== Examples
#
# tag("br")
# # => <br />
#
# tag("br", nil, true)
# # => <br>
#
# tag("input", type: 'text', disabled: true)
# # => <input type="text" disabled="disabled" />
#
# tag("input", type: 'text', class: ["strong", "highlight"])
# # => <input class="strong highlight" type="text" />
#
# tag("img", src: "open & shut.png")
# # => <img src="open & shut.png" />
#
# tag("img", { src: "open & shut.png" }, false, false)
# # => <img src="open & shut.png" />
#
# tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) })
# # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" />
#
# tag("div", class: { highlight: current_user.admin? })
# # => <div class="highlight" />
- 9
def tag(name = nil, options = nil, open = false, escape = true)
if name.nil?
tag_builder
else
"<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
end
end
# Returns an HTML block tag of type +name+ surrounding the +content+. Add
# HTML attributes by passing an attributes hash to +options+.
# Instead of passing the content as an argument, you can also use a block
# in which case, you pass your +options+ as the second parameter.
# Set escape to false to disable attribute value escaping.
# Note: this is legacy syntax, see +tag+ method description for details.
#
# ==== Options
# The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and
# <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
# symbols or strings for the attribute names.
#
# ==== Examples
# content_tag(:p, "Hello world!")
# # => <p>Hello world!</p>
# content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
# # => <div class="strong"><p>Hello world!</p></div>
# content_tag(:div, "Hello world!", class: ["strong", "highlight"])
# # => <div class="strong highlight">Hello world!</div>
# content_tag(:div, "Hello world!", class: ["strong", { highlight: current_user.admin? }])
# # => <div class="strong highlight">Hello world!</div>
# content_tag("select", options, multiple: true)
# # => <select multiple="multiple">...options...</select>
#
# <%= content_tag :div, class: "strong" do -%>
# Hello world!
# <% end -%>
# # => <div class="strong">Hello world!</div>
- 9
def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
if block_given?
options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
tag_builder.content_tag_string(name, capture(&block), options, escape)
else
tag_builder.content_tag_string(name, content_or_options_with_block, options, escape)
end
end
# Returns a string of class names built from +args+.
#
# ==== Examples
# class_names("foo", "bar")
# # => "foo bar"
# class_names({ foo: true, bar: false })
# # => "foo"
# class_names(nil, false, 123, "", "foo", { bar: true })
# # => "123 foo bar"
- 9
def class_names(*args)
safe_join(build_tag_values(*args), " ")
end
# Returns a CDATA section with the given +content+. CDATA sections
# are used to escape blocks of text containing characters which would
# otherwise be recognized as markup. CDATA sections begin with the string
# <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
#
# cdata_section("<hello world>")
# # => <![CDATA[<hello world>]]>
#
# cdata_section(File.read("hello_world.txt"))
# # => <![CDATA[<hello from a text file]]>
#
# cdata_section("hello]]>world")
# # => <![CDATA[hello]]]]><![CDATA[>world]]>
- 9
def cdata_section(content)
splitted = content.to_s.gsub(/\]\]\>/, "]]]]><![CDATA[>")
"<![CDATA[#{splitted}]]>".html_safe
end
# Returns an escaped version of +html+ without affecting existing escaped entities.
#
# escape_once("1 < 2 & 3")
# # => "1 < 2 & 3"
#
# escape_once("<< Accept & Checkout")
# # => "<< Accept & Checkout"
- 9
def escape_once(html)
ERB::Util.html_escape_once(html)
end
- 9
private
- 9
def build_tag_values(*args)
tag_values = []
args.each do |tag_value|
case tag_value
when Hash
tag_value.each do |key, val|
tag_values << key.to_s if val && key.present?
end
when Array
tag_values.concat build_tag_values(*tag_value)
else
tag_values << tag_value.to_s if tag_value.present?
end
end
tag_values
end
- 9
module_function :build_tag_values
- 9
def tag_builder
@tag_builder ||= TagBuilder.new(self)
end
end
end
end
# frozen_string_literal: true
- 3
module ActionView
- 3
module Helpers #:nodoc:
- 3
module Tags #:nodoc:
- 3
extend ActiveSupport::Autoload
- 3
eager_autoload do
- 3
autoload :Base
- 3
autoload :Translator
- 3
autoload :CheckBox
- 3
autoload :CollectionCheckBoxes
- 3
autoload :CollectionRadioButtons
- 3
autoload :CollectionSelect
- 3
autoload :ColorField
- 3
autoload :DateField
- 3
autoload :DateSelect
- 3
autoload :DatetimeField
- 3
autoload :DatetimeLocalField
- 3
autoload :DatetimeSelect
- 3
autoload :EmailField
- 3
autoload :FileField
- 3
autoload :GroupedCollectionSelect
- 3
autoload :HiddenField
- 3
autoload :Label
- 3
autoload :MonthField
- 3
autoload :NumberField
- 3
autoload :PasswordField
- 3
autoload :RadioButton
- 3
autoload :RangeField
- 3
autoload :SearchField
- 3
autoload :Select
- 3
autoload :TelField
- 3
autoload :TextArea
- 3
autoload :TextField
- 3
autoload :TimeField
- 3
autoload :TimeSelect
- 3
autoload :TimeZoneSelect
- 3
autoload :UrlField
- 3
autoload :WeekField
end
end
end
end
# frozen_string_literal: true
- 3
module ActionView
- 3
module Helpers
- 3
module Tags # :nodoc:
- 3
class Base # :nodoc:
- 3
include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
- 3
include FormOptionsHelper
- 3
attr_reader :object
- 3
def initialize(object_name, method_name, template_object, options = {})
@object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
@template_object = template_object
@object_name.sub!(/\[\]$/, "") || @object_name.sub!(/\[\]\]$/, "]")
@object = retrieve_object(options.delete(:object))
@skip_default_ids = options.delete(:skip_default_ids)
@allow_method_names_outside_object = options.delete(:allow_method_names_outside_object)
@options = options
if Regexp.last_match
@generate_indexed_names = true
@auto_index = retrieve_autoindex(Regexp.last_match.pre_match)
else
@generate_indexed_names = false
@auto_index = nil
end
end
# This is what child classes implement.
- 3
def render
raise NotImplementedError, "Subclasses must implement a render method"
end
- 3
private
- 3
def value
if @allow_method_names_outside_object
object.public_send @method_name if object && object.respond_to?(@method_name)
else
object.public_send @method_name if object
end
end
- 3
def value_before_type_cast
unless object.nil?
method_before_type_cast = @method_name + "_before_type_cast"
if value_came_from_user? && object.respond_to?(method_before_type_cast)
object.public_send(method_before_type_cast)
else
value
end
end
end
- 3
def value_came_from_user?
method_name = "#{@method_name}_came_from_user?"
!object.respond_to?(method_name) || object.public_send(method_name)
end
- 3
def retrieve_object(object)
if object
object
elsif @template_object.instance_variable_defined?("@#{@object_name}")
@template_object.instance_variable_get("@#{@object_name}")
end
rescue NameError
# As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
nil
end
- 3
def retrieve_autoindex(pre_match)
object = self.object || @template_object.instance_variable_get("@#{pre_match}")
if object && object.respond_to?(:to_param)
object.to_param
else
raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
end
end
- 3
def add_default_name_and_id_for_value(tag_value, options)
if tag_value.nil?
add_default_name_and_id(options)
else
specified_id = options["id"]
add_default_name_and_id(options)
if specified_id.blank? && options["id"].present?
options["id"] += "_#{sanitized_value(tag_value)}"
end
end
end
- 3
def add_default_name_and_id(options)
index = name_and_id_index(options)
options["name"] = options.fetch("name") { tag_name(options["multiple"], index) }
if generate_ids?
options["id"] = options.fetch("id") { tag_id(index) }
if namespace = options.delete("namespace")
options["id"] = options["id"] ? "#{namespace}_#{options['id']}" : namespace
end
end
end
- 3
def tag_name(multiple = false, index = nil)
# a little duplication to construct fewer strings
case
when @object_name.empty?
"#{sanitized_method_name}#{multiple ? "[]" : ""}"
when index
"#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}"
else
"#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}"
end
end
- 3
def tag_id(index = nil)
# a little duplication to construct fewer strings
case
when @object_name.empty?
sanitized_method_name.dup
when index
"#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
else
"#{sanitized_object_name}_#{sanitized_method_name}"
end
end
- 3
def sanitized_object_name
@sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").delete_suffix("_")
end
- 3
def sanitized_method_name
@sanitized_method_name ||= @method_name.delete_suffix("?")
end
- 3
def sanitized_value(value)
value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").downcase
end
- 3
def select_content_tag(option_tags, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
if placeholder_required?(html_options)
raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
options[:include_blank] ||= true unless options[:prompt]
end
value = options.fetch(:selected) { value() }
select = content_tag("select", add_options(option_tags, options, value), html_options)
if html_options["multiple"] && options.fetch(:include_hidden, true)
tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "") + select
else
select
end
end
- 3
def placeholder_required?(html_options)
# See https://html.spec.whatwg.org/multipage/forms.html#attr-select-required
html_options["required"] && !html_options["multiple"] && html_options.fetch("size", 1).to_i == 1
end
- 3
def add_options(option_tags, options, value = nil)
if options[:include_blank]
content = (options[:include_blank] if options[:include_blank].is_a?(String))
label = (" " unless content)
option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
end
if value.blank? && options[:prompt]
tag_options = { value: "" }.tap do |prompt_opts|
prompt_opts[:disabled] = true if options[:disabled] == ""
prompt_opts[:selected] = true if options[:selected] == ""
end
option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
end
option_tags
end
- 3
def name_and_id_index(options)
if options.key?("index")
options.delete("index") || ""
elsif @generate_indexed_names
@auto_index || ""
end
end
- 3
def generate_ids?
!@skip_default_ids
end
end
end
end
end
# frozen_string_literal: true
require "action_view/helpers/tags/checkable"
module ActionView
module Helpers
module Tags # :nodoc:
class CheckBox < Base #:nodoc:
include Checkable
def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options)
@checked_value = checked_value
@unchecked_value = unchecked_value
super(object_name, method_name, template_object, options)
end
def render
options = @options.stringify_keys
options["type"] = "checkbox"
options["value"] = @checked_value
options["checked"] = "checked" if input_checked?(options)
if options["multiple"]
add_default_name_and_id_for_value(@checked_value, options)
options.delete("multiple")
else
add_default_name_and_id(options)
end
include_hidden = options.delete("include_hidden") { true }
checkbox = tag("input", options)
if include_hidden
hidden = hidden_field_for_checkbox(options)
hidden + checkbox
else
checkbox
end
end
private
def checked?(value)
case value
when TrueClass, FalseClass
value == !!@checked_value
when NilClass
false
when String
value == @checked_value
else
if value.respond_to?(:include?)
value.include?(@checked_value)
else
value.to_i == @checked_value.to_i
end
end
end
def hidden_field_for_checkbox(options)
@unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
module Checkable # :nodoc:
def input_checked?(options)
if options.has_key?("checked")
checked = options.delete "checked"
checked == true || checked == "checked"
else
checked?(value)
end
end
end
end
end
end
# frozen_string_literal: true
require "action_view/helpers/tags/collection_helpers"
module ActionView
module Helpers
module Tags # :nodoc:
class CollectionCheckBoxes < Base # :nodoc:
include CollectionHelpers
class CheckBoxBuilder < Builder # :nodoc:
def check_box(extra_html_options = {})
html_options = extra_html_options.merge(@input_html_options)
html_options[:multiple] = true
html_options[:skip_default_ids] = false
@template_object.check_box(@object_name, @method_name, html_options, @value, nil)
end
end
def render(&block)
render_collection_for(CheckBoxBuilder, &block)
end
private
def render_component(builder)
builder.check_box + builder.label
end
def hidden_field_name
"#{super}[]"
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
module CollectionHelpers # :nodoc:
class Builder # :nodoc:
attr_reader :object, :text, :value
def initialize(template_object, object_name, method_name, object,
sanitized_attribute_name, text, value, input_html_options)
@template_object = template_object
@object_name = object_name
@method_name = method_name
@object = object
@sanitized_attribute_name = sanitized_attribute_name
@text = text
@value = value
@input_html_options = input_html_options
end
def label(label_html_options = {}, &block)
html_options = @input_html_options.slice(:index, :namespace).merge(label_html_options)
html_options[:for] ||= @input_html_options[:id] if @input_html_options[:id]
@template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block)
end
end
def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
@collection = collection
@value_method = value_method
@text_method = text_method
@html_options = html_options
super(object_name, method_name, template_object, options)
end
private
def instantiate_builder(builder_class, item, value, text, html_options)
builder_class.new(@template_object, @object_name, @method_name, item,
sanitize_attribute_name(value), text, value, html_options)
end
# Generate default options for collection helpers, such as :checked and
# :disabled.
def default_html_options_for_collection(item, value)
html_options = @html_options.dup
[:checked, :selected, :disabled, :readonly].each do |option|
current_value = @options[option]
next if current_value.nil?
accept = if current_value.respond_to?(:call)
current_value.call(item)
else
Array(current_value).map(&:to_s).include?(value.to_s)
end
if accept
html_options[option] = true
elsif option == :checked
html_options[option] = false
end
end
html_options[:object] = @object
html_options
end
def sanitize_attribute_name(value)
"#{sanitized_method_name}_#{sanitized_value(value)}"
end
def render_collection
@collection.map do |item|
value = value_for_collection(item, @value_method)
text = value_for_collection(item, @text_method)
default_html_options = default_html_options_for_collection(item, value)
additional_html_options = option_html_attributes(item)
yield item, value, text, default_html_options.merge(additional_html_options)
end.join.html_safe
end
def render_collection_for(builder_class, &block)
options = @options.stringify_keys
rendered_collection = render_collection do |item, value, text, default_html_options|
builder = instantiate_builder(builder_class, item, value, text, default_html_options)
if block_given?
@template_object.capture(builder, &block)
else
render_component(builder)
end
end
# Prepend a hidden field to make sure something will be sent back to the
# server if all radio buttons are unchecked.
if options.fetch("include_hidden", true)
hidden_field + rendered_collection
else
rendered_collection
end
end
def hidden_field
hidden_name = @html_options[:name] || hidden_field_name
@template_object.hidden_field_tag(hidden_name, "", id: nil)
end
def hidden_field_name
"#{tag_name(false, @options[:index])}"
end
end
end
end
end
# frozen_string_literal: true
require "action_view/helpers/tags/collection_helpers"
module ActionView
module Helpers
module Tags # :nodoc:
class CollectionRadioButtons < Base # :nodoc:
include CollectionHelpers
class RadioButtonBuilder < Builder # :nodoc:
def radio_button(extra_html_options = {})
html_options = extra_html_options.merge(@input_html_options)
html_options[:skip_default_ids] = false
@template_object.radio_button(@object_name, @method_name, @value, html_options)
end
end
def render(&block)
render_collection_for(RadioButtonBuilder, &block)
end
private
def render_component(builder)
builder.radio_button + builder.label
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class CollectionSelect < Base #:nodoc:
def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
@collection = collection
@value_method = value_method
@text_method = text_method
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
option_tags_options = {
selected: @options.fetch(:selected) { value },
disabled: @options[:disabled]
}
select_content_tag(
options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options),
@options, @html_options
)
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class ColorField < TextField # :nodoc:
def render
options = @options.stringify_keys
options["value"] ||= validate_color_string(value)
@options = options
super
end
private
def validate_color_string(string)
regex = /#[0-9a-fA-F]{6}/
if regex.match?(string)
string.downcase
else
"#000000"
end
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class DateField < DatetimeField # :nodoc:
private
def format_date(value)
value&.strftime("%Y-%m-%d")
end
end
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/time/calculations"
module ActionView
module Helpers
module Tags # :nodoc:
class DateSelect < Base # :nodoc:
def initialize(object_name, method_name, template_object, options, html_options)
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe)
end
class << self
def select_type
@select_type ||= name.split("::").last.sub("Select", "").downcase
end
end
private
def select_type
self.class.select_type
end
def datetime_selector(options, html_options)
datetime = options.fetch(:selected) { value || default_datetime(options) }
@auto_index ||= nil
options = options.dup
options[:field_name] = @method_name
options[:include_position] = true
options[:prefix] ||= @object_name
options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
DateTimeSelector.new(datetime, options, html_options)
end
def default_datetime(options)
return if options[:include_blank] || options[:prompt]
case options[:default]
when nil
Time.current
when Date, Time
options[:default]
else
default = options[:default].dup
# Rename :minute and :second to :min and :sec
default[:min] ||= default[:minute]
default[:sec] ||= default[:second]
time = Time.current
[:year, :month, :day, :hour, :min, :sec].each do |key|
default[key] ||= time.send(key)
end
Time.utc(
default[:year], default[:month], default[:day],
default[:hour], default[:min], default[:sec]
)
end
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class DatetimeField < TextField # :nodoc:
def render
options = @options.stringify_keys
options["value"] ||= format_date(value)
options["min"] = format_date(datetime_value(options["min"]))
options["max"] = format_date(datetime_value(options["max"]))
@options = options
super
end
private
def format_date(value)
raise NotImplementedError
end
def datetime_value(value)
if value.is_a? String
DateTime.parse(value) rescue nil
else
value
end
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class DatetimeLocalField < DatetimeField # :nodoc:
class << self
def field_type
@field_type ||= "datetime-local"
end
end
private
def format_date(value)
value&.strftime("%Y-%m-%dT%T")
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class DatetimeSelect < DateSelect # :nodoc:
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class EmailField < TextField # :nodoc:
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class FileField < TextField # :nodoc:
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class GroupedCollectionSelect < Base # :nodoc:
def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
@collection = collection
@group_method = group_method
@group_label_method = group_label_method
@option_key_method = option_key_method
@option_value_method = option_value_method
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
option_tags_options = {
selected: @options.fetch(:selected) { value },
disabled: @options[:disabled]
}
select_content_tag(
option_groups_from_collection_for_select(@collection, @group_method, @group_label_method, @option_key_method, @option_value_method, option_tags_options), @options, @html_options
)
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class HiddenField < TextField # :nodoc:
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class Label < Base # :nodoc:
class LabelBuilder # :nodoc:
attr_reader :object
def initialize(template_object, object_name, method_name, object, tag_value)
@template_object = template_object
@object_name = object_name
@method_name = method_name
@object = object
@tag_value = tag_value
end
def translation
method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name
content ||= Translator
.new(object, @object_name, method_and_value, scope: "helpers.label")
.translate
content ||= @method_name.humanize
content
end
end
def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
options ||= {}
content_is_options = content_or_options.is_a?(Hash)
if content_is_options
options.merge! content_or_options
@content = nil
else
@content = content_or_options
end
super(object_name, method_name, template_object, options)
end
def render(&block)
options = @options.stringify_keys
tag_value = options.delete("value")
name_and_id = options.dup
if name_and_id["for"]
name_and_id["id"] = name_and_id["for"]
else
name_and_id.delete("id")
end
add_default_name_and_id_for_value(tag_value, name_and_id)
options.delete("index")
options.delete("namespace")
options["for"] = name_and_id["id"] unless options.key?("for")
builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)
content = if block_given?
@template_object.capture(builder, &block)
elsif @content.present?
@content.to_s
else
render_component(builder)
end
label_tag(name_and_id["id"], content, options)
end
private
def render_component(builder)
builder.translation
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class MonthField < DatetimeField # :nodoc:
private
def format_date(value)
value&.strftime("%Y-%m")
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class NumberField < TextField # :nodoc:
def render
options = @options.stringify_keys
if range = options.delete("in") || options.delete("within")
options.update("min" => range.min, "max" => range.max)
end
@options = options
super
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class PasswordField < TextField # :nodoc:
def render
@options = { value: nil }.merge!(@options)
super
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
module Placeholderable # :nodoc:
def initialize(*)
super
if tag_value = @options[:placeholder]
placeholder = tag_value if tag_value.is_a?(String)
method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
placeholder ||= Tags::Translator
.new(object, @object_name, method_and_value, scope: "helpers.placeholder")
.translate
placeholder ||= @method_name.humanize
@options[:placeholder] = placeholder
end
end
end
end
end
end
# frozen_string_literal: true
require "action_view/helpers/tags/checkable"
module ActionView
module Helpers
module Tags # :nodoc:
class RadioButton < Base # :nodoc:
include Checkable
def initialize(object_name, method_name, template_object, tag_value, options)
@tag_value = tag_value
super(object_name, method_name, template_object, options)
end
def render
options = @options.stringify_keys
options["type"] = "radio"
options["value"] = @tag_value
options["checked"] = "checked" if input_checked?(options)
add_default_name_and_id_for_value(@tag_value, options)
tag("input", options)
end
private
def checked?(value)
value.to_s == @tag_value.to_s
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class RangeField < NumberField # :nodoc:
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class SearchField < TextField # :nodoc:
def render
options = @options.stringify_keys
if options["autosave"]
if options["autosave"] == true
options["autosave"] = request.host.split(".").reverse.join(".")
end
options["results"] ||= 10
end
if options["onsearch"]
options["incremental"] = true unless options.has_key?("incremental")
end
@options = options
super
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class Select < Base # :nodoc:
def initialize(object_name, method_name, template_object, choices, options, html_options)
@choices = block_given? ? template_object.capture { yield || "" } : choices
@choices = @choices.to_a if @choices.is_a?(Range)
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
option_tags_options = {
selected: @options.fetch(:selected) { value.to_s },
disabled: @options[:disabled]
}
option_tags = if grouped_choices?
grouped_options_for_select(@choices, option_tags_options)
else
options_for_select(@choices, option_tags_options)
end
select_content_tag(option_tags, @options, @html_options)
end
private
# Grouped choices look like this:
#
# [nil, []]
# { nil => [] }
def grouped_choices?
!@choices.blank? && @choices.first.respond_to?(:last) && Array === @choices.first.last
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class TelField < TextField # :nodoc:
end
end
end
end
# frozen_string_literal: true
require "action_view/helpers/tags/placeholderable"
module ActionView
module Helpers
module Tags # :nodoc:
class TextArea < Base # :nodoc:
include Placeholderable
def render
options = @options.stringify_keys
add_default_name_and_id(options)
if size = options.delete("size")
options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
end
content_tag("textarea", options.delete("value") { value_before_type_cast }, options)
end
end
end
end
end
# frozen_string_literal: true
require "action_view/helpers/tags/placeholderable"
module ActionView
module Helpers
module Tags # :nodoc:
class TextField < Base # :nodoc:
include Placeholderable
def render
options = @options.stringify_keys
options["size"] = options["maxlength"] unless options.key?("size")
options["type"] ||= field_type
options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
add_default_name_and_id(options)
tag("input", options)
end
class << self
def field_type
@field_type ||= name.split("::").last.sub("Field", "").downcase
end
end
private
def field_type
self.class.field_type
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class TimeField < DatetimeField # :nodoc:
private
def format_date(value)
value&.strftime("%T.%L")
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class TimeSelect < DateSelect # :nodoc:
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class TimeZoneSelect < Base # :nodoc:
def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
@priority_zones = priority_zones
@html_options = html_options
super(object_name, method_name, template_object, options)
end
def render
select_content_tag(
time_zone_options_for_select(value || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
)
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class Translator # :nodoc:
def initialize(object, object_name, method_and_value, scope:)
@object_name = object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
@method_and_value = method_and_value
@scope = scope
@model = object.respond_to?(:to_model) ? object.to_model : nil
end
def translate
translated_attribute = I18n.t("#{object_name}.#{method_and_value}", default: i18n_default, scope: scope).presence
translated_attribute || human_attribute_name
end
private
attr_reader :object_name, :method_and_value, :scope, :model
def i18n_default
if model
key = model.model_name.i18n_key
["#{key}.#{method_and_value}".to_sym, ""]
else
""
end
end
def human_attribute_name
if model && model.class.respond_to?(:human_attribute_name)
model.class.human_attribute_name(method_and_value)
end
end
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class UrlField < TextField # :nodoc:
end
end
end
end
# frozen_string_literal: true
module ActionView
module Helpers
module Tags # :nodoc:
class WeekField < DatetimeField # :nodoc:
private
def format_date(value)
value&.strftime("%Y-W%V")
end
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/string/filters"
- 9
require "active_support/core_ext/array/extract_options"
- 9
module ActionView
# = Action View Text Helpers
- 9
module Helpers #:nodoc:
# The TextHelper module provides a set of methods for filtering, formatting
# and transforming strings, which can reduce the amount of inline Ruby code in
# your views. These helper methods extend Action View making them callable
# within your template files.
#
# ==== Sanitization
#
# Most text helpers that generate HTML output sanitize the given input by default,
# but do not escape it. This means HTML tags will appear in the page but all malicious
# code will be removed. Let's look at some examples using the +simple_format+ method:
#
# simple_format('<a href="http://example.com/">Example</a>')
# # => "<p><a href=\"http://example.com/\">Example</a></p>"
#
# simple_format('<a href="javascript:alert(\'no!\')">Example</a>')
# # => "<p><a>Example</a></p>"
#
# If you want to escape all content, you should invoke the +h+ method before
# calling the text helper.
#
# simple_format h('<a href="http://example.com/">Example</a>')
# # => "<p><a href=\"http://example.com/\">Example</a></p>"
- 9
module TextHelper
- 9
extend ActiveSupport::Concern
- 9
include SanitizeHelper
- 9
include TagHelper
- 9
include OutputSafetyHelper
# The preferred method of outputting text in your views is to use the
# <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
# do not operate as expected in an eRuby code block. If you absolutely must
# output text within a non-output code block (i.e., <% %>), you can use the concat method.
#
# <%
# concat "hello"
# # is the equivalent of <%= "hello" %>
#
# if logged_in
# concat "Logged in!"
# else
# concat link_to('login', action: :login)
# end
# # will either display "Logged in!" or a login link
# %>
- 9
def concat(string)
output_buffer << string
end
- 9
def safe_concat(string)
output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
end
# Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
# (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
# for a total length not exceeding <tt>:length</tt>.
#
# Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
#
# Pass a block if you want to show extra content when the text is truncated.
#
# The result is marked as HTML-safe, but it is escaped by default, unless <tt>:escape</tt> is
# +false+. Care should be taken if +text+ contains HTML tags or entities, because truncation
# may produce invalid HTML (such as unbalanced or incomplete tags).
#
# truncate("Once upon a time in a world far far away")
# # => "Once upon a time in a world..."
#
# truncate("Once upon a time in a world far far away", length: 17)
# # => "Once upon a ti..."
#
# truncate("Once upon a time in a world far far away", length: 17, separator: ' ')
# # => "Once upon a..."
#
# truncate("And they found that many people were sleeping better.", length: 25, omission: '... (continued)')
# # => "And they f... (continued)"
#
# truncate("<p>Once upon a time in a world far far away</p>")
# # => "<p>Once upon a time in a wo..."
#
# truncate("<p>Once upon a time in a world far far away</p>", escape: false)
# # => "<p>Once upon a time in a wo..."
#
# truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
# # => "Once upon a time in a wo...<a href="#">Continue</a>"
- 9
def truncate(text, options = {}, &block)
if text
length = options.fetch(:length, 30)
content = text.truncate(length, options)
content = options[:escape] == false ? content.html_safe : ERB::Util.html_escape(content)
content << capture(&block) if block_given? && text.length > length
content
end
end
# Highlights one or more +phrases+ everywhere in +text+ by inserting it into
# a <tt>:highlighter</tt> string. The highlighter can be specialized by passing <tt>:highlighter</tt>
# as a single-quoted string with <tt>\1</tt> where the phrase is to be inserted (defaults to
# '<mark>\1</mark>') or passing a block that receives each matched term. By default +text+
# is sanitized to prevent possible XSS attacks. If the input is trustworthy, passing false
# for <tt>:sanitize</tt> will turn sanitizing off.
#
# highlight('You searched for: rails', 'rails')
# # => You searched for: <mark>rails</mark>
#
# highlight('You searched for: rails', /for|rails/)
# # => You searched <mark>for</mark>: <mark>rails</mark>
#
# highlight('You searched for: ruby, rails, dhh', 'actionpack')
# # => You searched for: ruby, rails, dhh
#
# highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>')
# # => You searched <em>for</em>: <em>rails</em>
#
# highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
# # => You searched for: <a href="search?q=rails">rails</a>
#
# highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
# # => You searched for: <a href="search?q=rails">rails</a>
#
# highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false)
# # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark>
- 9
def highlight(text, phrases, options = {})
text = sanitize(text) if options.fetch(:sanitize, true)
if text.blank? || phrases.blank?
text || ""
else
match = Array(phrases).map do |p|
Regexp === p ? p.to_s : Regexp.escape(p)
end.join("|")
if block_given?
text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
else
highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
end
end.html_safe
end
# Extracts an excerpt from +text+ that matches the first instance of +phrase+.
# The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
# defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
# then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the
# <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+
# isn't found, +nil+ is returned.
#
# excerpt('This is an example', 'an', radius: 5)
# # => ...s is an exam...
#
# excerpt('This is an example', 'is', radius: 5)
# # => This is a...
#
# excerpt('This is an example', 'is')
# # => This is an example
#
# excerpt('This next thing is an example', 'ex', radius: 2)
# # => ...next...
#
# excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
# # => <chop> is also an example
#
# excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
# # => ...a very beautiful...
- 9
def excerpt(text, phrase, options = {})
return unless text && phrase
separator = options.fetch(:separator, nil) || ""
case phrase
when Regexp
regex = phrase
else
regex = /#{Regexp.escape(phrase)}/i
end
return unless matches = text.match(regex)
phrase = matches[0]
unless separator.empty?
text.split(separator).each do |value|
if value.match?(regex)
phrase = value
break
end
end
end
first_part, second_part = text.split(phrase, 2)
prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
affix = [first_part, separator, phrase, separator, second_part].join.strip
[prefix, affix, postfix].join
end
# Attempts to pluralize the +singular+ word unless +count+ is 1. If
# +plural+ is supplied, it will use that when count is > 1, otherwise
# it will use the Inflector to determine the plural form for the given locale,
# which defaults to I18n.locale
#
# The word will be pluralized using rules defined for the locale
# (you must define your own inflection rules for languages other than English).
# See ActiveSupport::Inflector.pluralize
#
# pluralize(1, 'person')
# # => 1 person
#
# pluralize(2, 'person')
# # => 2 people
#
# pluralize(3, 'person', plural: 'users')
# # => 3 users
#
# pluralize(0, 'person')
# # => 0 people
#
# pluralize(2, 'Person', locale: :de)
# # => 2 Personen
- 9
def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
singular
else
plural || singular.pluralize(locale)
end
"#{count || 0} #{word}"
end
# Wraps the +text+ into lines no longer than +line_width+ width. This method
# breaks on the first whitespace character that does not exceed +line_width+
# (which is 80 by default).
#
# word_wrap('Once upon a time')
# # => Once upon a time
#
# word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
# # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\na successor to the throne turned out to be more trouble than anyone could have\nimagined...
#
# word_wrap('Once upon a time', line_width: 8)
# # => Once\nupon a\ntime
#
# word_wrap('Once upon a time', line_width: 1)
# # => Once\nupon\na\ntime
#
# You can also specify a custom +break_sequence+ ("\n" by default)
#
# word_wrap('Once upon a time', line_width: 1, break_sequence: "\r\n")
# # => Once\r\nupon\r\na\r\ntime
- 9
def word_wrap(text, line_width: 80, break_sequence: "\n")
text.split("\n").collect! do |line|
line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line
end * break_sequence
end
# Returns +text+ transformed into HTML using simple formatting rules.
# Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are
# considered a paragraph and wrapped in <tt><p></tt> tags. One newline
# (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a
# <tt><br /></tt> tag is appended. This method does not remove the
# newlines from the +text+.
#
# You can pass any HTML attributes into <tt>html_options</tt>. These
# will be added to all created paragraphs.
#
# ==== Options
# * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
# * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt>
#
# ==== Examples
# my_text = "Here is some basic text...\n...with a line break."
#
# simple_format(my_text)
# # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
#
# simple_format(my_text, {}, wrapper_tag: "div")
# # => "<div>Here is some basic text...\n<br />...with a line break.</div>"
#
# more_text = "We want to put a paragraph...\n\n...right there."
#
# simple_format(more_text)
# # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
#
# simple_format("Look ma! A class!", class: 'description')
# # => "<p class='description'>Look ma! A class!</p>"
#
# simple_format("<blink>Unblinkable.</blink>")
# # => "<p>Unblinkable.</p>"
#
# simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false)
# # => "<p><blink>Blinkable!</blink> It's true.</p>"
- 9
def simple_format(text, html_options = {}, options = {})
wrapper_tag = options.fetch(:wrapper_tag, :p)
text = sanitize(text) if options.fetch(:sanitize, true)
paragraphs = split_paragraphs(text)
if paragraphs.empty?
content_tag(wrapper_tag, nil, html_options)
else
paragraphs.map! { |paragraph|
content_tag(wrapper_tag, raw(paragraph), html_options)
}.join("\n\n").html_safe
end
end
# Creates a Cycle object whose _to_s_ method cycles through elements of an
# array every time it is called. This can be used for example, to alternate
# classes for table rows. You can use named cycles to allow nesting in loops.
# Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
# named cycle. The default name for a cycle without a +:name+ key is
# <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
# and passing the name of the cycle. The current cycle string can be obtained
# anytime using the current_cycle method.
#
# # Alternate CSS classes for even and odd numbers...
# @items = [1,2,3,4]
# <table>
# <% @items.each do |item| %>
# <tr class="<%= cycle("odd", "even") -%>">
# <td><%= item %></td>
# </tr>
# <% end %>
# </table>
#
#
# # Cycle CSS classes for rows, and text colors for values within each row
# @items = x = [{first: 'Robert', middle: 'Daniel', last: 'James'},
# {first: 'Emily', middle: 'Shannon', maiden: 'Pike', last: 'Hicks'},
# {first: 'June', middle: 'Dae', last: 'Jones'}]
# <% @items.each do |item| %>
# <tr class="<%= cycle("odd", "even", name: "row_class") -%>">
# <td>
# <% item.values.each do |value| %>
# <%# Create a named cycle "colors" %>
# <span style="color:<%= cycle("red", "green", "blue", name: "colors") -%>">
# <%= value %>
# </span>
# <% end %>
# <% reset_cycle("colors") %>
# </td>
# </tr>
# <% end %>
- 9
def cycle(first_value, *values)
options = values.extract_options!
name = options.fetch(:name, "default")
values.unshift(*first_value)
cycle = get_cycle(name)
unless cycle && cycle.values == values
cycle = set_cycle(name, Cycle.new(*values))
end
cycle.to_s
end
# Returns the current cycle string after a cycle has been started. Useful
# for complex table highlighting or any other design need which requires
# the current cycle string in more than one place.
#
# # Alternate background colors
# @items = [1,2,3,4]
# <% @items.each do |item| %>
# <div style="background-color:<%= cycle("red","white","blue") %>">
# <span style="background-color:<%= current_cycle %>"><%= item %></span>
# </div>
# <% end %>
- 9
def current_cycle(name = "default")
cycle = get_cycle(name)
cycle.current_value if cycle
end
# Resets a cycle so that it starts from the first element the next time
# it is called. Pass in +name+ to reset a named cycle.
#
# # Alternate CSS classes for even and odd numbers...
# @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
# <table>
# <% @items.each do |item| %>
# <tr class="<%= cycle("even", "odd") -%>">
# <% item.each do |value| %>
# <span style="color:<%= cycle("#333", "#666", "#999", name: "colors") -%>">
# <%= value %>
# </span>
# <% end %>
#
# <% reset_cycle("colors") %>
# </tr>
# <% end %>
# </table>
- 9
def reset_cycle(name = "default")
cycle = get_cycle(name)
cycle.reset if cycle
end
- 9
class Cycle #:nodoc:
- 9
attr_reader :values
- 9
def initialize(first_value, *values)
@values = values.unshift(first_value)
reset
end
- 9
def reset
@index = 0
end
- 9
def current_value
@values[previous_index].to_s
end
- 9
def to_s
value = @values[@index].to_s
@index = next_index
value
end
- 9
private
- 9
def next_index
step_index(1)
end
- 9
def previous_index
step_index(-1)
end
- 9
def step_index(n)
(@index + n) % @values.size
end
end
- 9
private
# The cycle helpers need to store the cycles in a place that is
# guaranteed to be reset every time a page is rendered, so it
# uses an instance variable of ActionView::Base.
- 9
def get_cycle(name)
@_cycles = Hash.new unless defined?(@_cycles)
@_cycles[name]
end
- 9
def set_cycle(name, cycle_object)
@_cycles = Hash.new unless defined?(@_cycles)
@_cycles[name] = cycle_object
end
- 9
def split_paragraphs(text)
return [] if text.blank?
text.to_str.gsub(/\r\n?/, "\n").split(/\n\n+/).map! do |t|
t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t
end
end
- 9
def cut_excerpt_part(part_position, part, separator, options)
return "", "" unless part
radius = options.fetch(:radius, 100)
omission = options.fetch(:omission, "...")
part = part.split(separator)
part.delete("")
affix = part.size > radius ? omission : ""
part = if part_position == :first
drop_index = [part.length - radius, 0].max
part.drop(drop_index)
else
part.first(radius)
end
return affix, part.join(separator)
end
end
end
end
# frozen_string_literal: true
- 9
require "action_view/helpers/tag_helper"
- 9
require "active_support/core_ext/string/access"
- 9
require "i18n/exceptions"
- 9
module ActionView
# = Action View Translation Helpers
- 9
module Helpers #:nodoc:
- 9
module TranslationHelper
- 9
extend ActiveSupport::Concern
- 9
include TagHelper
- 9
included do
- 12
mattr_accessor :debug_missing_translation, default: true
end
# Delegates to <tt>I18n#translate</tt> but also performs three additional
# functions.
#
# First, it will ensure that any thrown +MissingTranslation+ messages will
# be rendered as inline spans that:
#
# * Have a <tt>translation-missing</tt> class applied
# * Contain the missing key as the value of the +title+ attribute
# * Have a titleized version of the last key segment as text
#
# For example, the value returned for the missing translation key
# <tt>"blog.post.title"</tt> will be:
#
# <span
# class="translation_missing"
# title="translation missing: en.blog.post.title">Title</span>
#
# This allows for views to display rather reasonable strings while still
# giving developers a way to find missing translations.
#
# If you would prefer missing translations to raise an error, you can
# opt out of span-wrapping behavior globally by setting
# <tt>ActionView::Base.raise_on_missing_translations = true</tt> or
# individually by passing <tt>raise: true</tt> as an option to
# <tt>translate</tt>.
#
# Second, if the key starts with a period <tt>translate</tt> will scope
# the key by the current partial. Calling <tt>translate(".foo")</tt> from
# the <tt>people/index.html.erb</tt> template is equivalent to calling
# <tt>translate("people.index.foo")</tt>. This makes it less
# repetitive to translate many keys within the same partial and provides
# a convention to scope keys consistently.
#
# Third, the translation will be marked as <tt>html_safe</tt> if the key
# has the suffix "_html" or the last element of the key is "html". Calling
# <tt>translate("footer_html")</tt> or <tt>translate("footer.html")</tt>
# will return an HTML safe string that won't be escaped by other HTML
# helper methods. This naming convention helps to identify translations
# that include HTML tags so that you know what kind of output to expect
# when you call translate in a template and translators know which keys
# they can provide HTML values for.
- 9
def translate(key, **options)
unless options[:default].nil?
remaining_defaults = Array.wrap(options.delete(:default)).compact
options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol)
end
# If the user has explicitly decided to NOT raise errors, pass that option to I18n.
# Otherwise, tell I18n to raise an exception, which we rescue further in this method.
# Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default.
if options[:raise] == false
raise_error = false
i18n_raise = false
else
raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations
i18n_raise = true
end
if html_safe_translation_key?(key)
html_safe_options = options.dup
options.except(*I18n::RESERVED_KEYS).each do |name, value|
unless name == :count && value.is_a?(Numeric)
html_safe_options[name] = ERB::Util.html_escape(value.to_s)
end
end
translation = I18n.translate(scope_key_by_partial(key), **html_safe_options.merge(raise: i18n_raise))
if translation.respond_to?(:map)
translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element }
else
translation.respond_to?(:html_safe) ? translation.html_safe : translation
end
else
I18n.translate(scope_key_by_partial(key), **options.merge(raise: i18n_raise))
end
rescue I18n::MissingTranslationData => e
if remaining_defaults.present?
translate remaining_defaults.shift, **options.merge(default: remaining_defaults)
else
raise e if raise_error
keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
title = +"translation missing: #{keys.join('.')}"
interpolations = options.except(:default, :scope)
if interpolations.any?
title << ", " << interpolations.map { |k, v| "#{k}: #{ERB::Util.html_escape(v)}" }.join(", ")
end
return title unless ActionView::Base.debug_missing_translation
content_tag("span", keys.last.to_s.titleize, class: "translation_missing", title: title)
end
end
- 9
alias :t :translate
# Delegates to <tt>I18n.localize</tt> with no additional functionality.
#
# See https://www.rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
# for more information.
- 9
def localize(object, **options)
I18n.localize(object, **options)
end
- 9
alias :l :localize
- 9
private
- 9
def scope_key_by_partial(key)
stringified_key = key.to_s
if stringified_key.start_with?(".")
if @current_template&.virtual_path
@_scope_key_by_partial_cache ||= {}
@_scope_key_by_partial_cache[@current_template.virtual_path] ||= @current_template.virtual_path.gsub(%r{/_?}, ".")
"#{@_scope_key_by_partial_cache[@current_template.virtual_path]}#{stringified_key}"
else
raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
end
else
key
end
end
- 9
def html_safe_translation_key?(key)
/(?:_|\b)html\z/.match?(key.to_s)
end
end
end
end
# frozen_string_literal: true
- 9
require "action_view/helpers/javascript_helper"
- 9
require "active_support/core_ext/array/access"
- 9
require "active_support/core_ext/hash/keys"
- 9
require "active_support/core_ext/string/output_safety"
- 9
module ActionView
# = Action View URL Helpers
- 9
module Helpers #:nodoc:
# Provides a set of methods for making links and getting URLs that
# depend on the routing subsystem (see ActionDispatch::Routing).
# This allows you to use the same format for links in views
# and controllers.
- 9
module UrlHelper
# This helper may be included in any class that includes the
# URL helpers of a routes (routes.url_helpers). Some methods
# provided here will only work in the context of a request
# (link_to_unless_current, for instance), which must be provided
# as a method called #request on the context.
- 9
BUTTON_TAG_METHOD_VERBS = %w{patch put delete}
- 9
extend ActiveSupport::Concern
- 9
include TagHelper
- 9
module ClassMethods
- 9
def _url_for_modules
- 3
ActionView::RoutingUrlFor
end
end
# Basic implementation of url_for to allow use helpers without routes existence
- 9
def url_for(options = nil) # :nodoc:
case options
when String
options
when :back
_back_url
else
raise ArgumentError, "arguments passed to url_for can't be handled. Please require " \
"routes or provide your own implementation"
end
end
- 9
def _back_url # :nodoc:
_filtered_referrer || "javascript:history.back()"
end
- 9
private :_back_url
- 9
def _filtered_referrer # :nodoc:
if controller.respond_to?(:request)
referrer = controller.request.env["HTTP_REFERER"]
if referrer && URI(referrer).scheme != "javascript"
referrer
end
end
rescue URI::InvalidURIError
end
- 9
private :_filtered_referrer
# Creates an anchor element of the given +name+ using a URL created by the set of +options+.
# See the valid options in the documentation for +url_for+. It's also possible to
# pass a \String instead of an options hash, which generates an anchor element that uses the
# value of the \String as the href for the link. Using a <tt>:back</tt> \Symbol instead
# of an options hash will generate a link to the referrer (a JavaScript back link
# will be used in place of a referrer if none exists). If +nil+ is passed as the name
# the value of the link itself will become the name.
#
# ==== Signatures
#
# link_to(body, url, html_options = {})
# # url is a String; you can use URL helpers like
# # posts_path
#
# link_to(body, url_options = {}, html_options = {})
# # url_options, except :method, is passed to url_for
#
# link_to(options = {}, html_options = {}) do
# # name
# end
#
# link_to(url, html_options = {}) do
# # name
# end
#
# ==== Options
# * <tt>:data</tt> - This option can be used to add custom data attributes.
# * <tt>method: symbol of HTTP verb</tt> - This modifier will dynamically
# create an HTML form and immediately submit the form for processing using
# the HTTP verb specified. Useful for having links perform a POST operation
# in dangerous actions like deleting a record (which search bots can follow
# while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>.
# Note that if the user has JavaScript disabled, the request will fall back
# to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript
# disabled clicking the link will have no effect. If you are relying on the
# POST behavior, you should check for it in your controller's action by using
# the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>patch?</tt>, or <tt>put?</tt>.
# * <tt>remote: true</tt> - This will allow the unobtrusive JavaScript
# driver to make an Ajax request to the URL in question instead of following
# the link. The drivers each provide mechanisms for listening for the
# completion of the Ajax request and performing JavaScript operations once
# they're complete
#
# ==== Data attributes
#
# * <tt>confirm: 'question?'</tt> - This will allow the unobtrusive JavaScript
# driver to prompt with the question specified (in this case, the
# resulting text would be <tt>question?</tt>. If the user accepts, the
# link is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be used as the
# name for a disabled version of the link. This feature is provided by
# the unobtrusive JavaScript driver.
#
# ==== Examples
# Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments
# and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base
# your application on resources and use
#
# link_to "Profile", profile_path(@profile)
# # => <a href="/profiles/1">Profile</a>
#
# or the even pithier
#
# link_to "Profile", @profile
# # => <a href="/profiles/1">Profile</a>
#
# in place of the older more verbose, non-resource-oriented
#
# link_to "Profile", controller: "profiles", action: "show", id: @profile
# # => <a href="/profiles/show/1">Profile</a>
#
# Similarly,
#
# link_to "Profiles", profiles_path
# # => <a href="/profiles">Profiles</a>
#
# is better than
#
# link_to "Profiles", controller: "profiles"
# # => <a href="/profiles">Profiles</a>
#
# When name is +nil+ the href is presented instead
#
# link_to nil, "http://example.com"
# # => <a href="http://www.example.com">http://www.example.com</a>
#
# You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
#
# <%= link_to(@profile) do %>
# <strong><%= @profile.name %></strong> -- <span>Check it out!</span>
# <% end %>
# # => <a href="/profiles/1">
# <strong>David</strong> -- <span>Check it out!</span>
# </a>
#
# Classes and ids for CSS are easy to produce:
#
# link_to "Articles", articles_path, id: "news", class: "article"
# # => <a href="/articles" class="article" id="news">Articles</a>
#
# Be careful when using the older argument style, as an extra literal hash is needed:
#
# link_to "Articles", { controller: "articles" }, id: "news", class: "article"
# # => <a href="/articles" class="article" id="news">Articles</a>
#
# Leaving the hash off gives the wrong link:
#
# link_to "WRONG!", controller: "articles", id: "news", class: "article"
# # => <a href="/articles/index/news?class=article">WRONG!</a>
#
# +link_to+ can also produce links with anchors or query strings:
#
# link_to "Comment wall", profile_path(@profile, anchor: "wall")
# # => <a href="/profiles/1#wall">Comment wall</a>
#
# link_to "Ruby on Rails search", controller: "searches", query: "ruby on rails"
# # => <a href="/searches?query=ruby+on+rails">Ruby on Rails search</a>
#
# link_to "Nonsense search", searches_path(foo: "bar", baz: "quux")
# # => <a href="/searches?foo=bar&baz=quux">Nonsense search</a>
#
# The only option specific to +link_to+ (<tt>:method</tt>) is used as follows:
#
# link_to("Destroy", "http://www.example.com", method: :delete)
# # => <a href='http://www.example.com' rel="nofollow" data-method="delete">Destroy</a>
#
# You can also use custom data attributes using the <tt>:data</tt> option:
#
# link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
# # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
#
# Also you can set any link attributes such as <tt>target</tt>, <tt>rel</tt>, <tt>type</tt>:
#
# link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow"
# # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a>
- 9
def link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options, name = options, name, block if block_given?
options ||= {}
html_options = convert_options_to_data_attributes(options, html_options)
url = url_for(options)
html_options["href"] ||= url
content_tag("a", name || url, html_options, &block)
end
# Generates a form containing a single button that submits to the URL created
# by the set of +options+. This is the safest method to ensure links that
# cause changes to your data are not triggered by search bots or accelerators.
# If the HTML button does not work with your layout, you can also consider
# using the +link_to+ method with the <tt>:method</tt> modifier as described in
# the +link_to+ documentation.
#
# By default, the generated form element has a class name of <tt>button_to</tt>
# to allow styling of the form itself and its children. This can be changed
# using the <tt>:form_class</tt> modifier within +html_options+. You can control
# the form submission and input element behavior using +html_options+.
# This method accepts the <tt>:method</tt> modifier described in the +link_to+ documentation.
# If no <tt>:method</tt> modifier is given, it will default to performing a POST operation.
# You can also disable the button by passing <tt>disabled: true</tt> in +html_options+.
# If you are using RESTful routes, you can pass the <tt>:method</tt>
# to change the HTTP verb used to submit the form.
#
# ==== Options
# The +options+ hash accepts the same options as +url_for+.
#
# There are a few special +html_options+:
# * <tt>:method</tt> - \Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
# <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
# * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
# * <tt>:data</tt> - This option can be used to add custom data attributes.
# * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
# submit behavior. By default this behavior is an ajax submit.
# * <tt>:form</tt> - This hash will be form attributes
# * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
# be placed
# * <tt>:params</tt> - \Hash of parameters to be rendered as hidden fields within the form.
#
# ==== Data attributes
#
# * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to
# prompt with the question specified. If the user accepts, the link is
# processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be
# used as the value for a disabled version of the submit
# button when the form is submitted. This feature is provided
# by the unobtrusive JavaScript driver.
#
# ==== Examples
# <%= button_to "New", action: "new" %>
# # => "<form method="post" action="/controller/new" class="button_to">
# # <input value="New" type="submit" />
# # </form>"
#
# <%= button_to "New", new_article_path %>
# # => "<form method="post" action="/articles/new" class="button_to">
# # <input value="New" type="submit" />
# # </form>"
#
# <%= button_to [:make_happy, @user] do %>
# Make happy <strong><%= @user.name %></strong>
# <% end %>
# # => "<form method="post" action="/users/1/make_happy" class="button_to">
# # <button type="submit">
# # Make happy <strong><%= @user.name %></strong>
# # </button>
# # </form>"
#
# <%= button_to "New", { action: "new" }, form_class: "new-thing" %>
# # => "<form method="post" action="/controller/new" class="new-thing">
# # <input value="New" type="submit" />
# # </form>"
#
#
# <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
# # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
# # <input value="Create" type="submit" />
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>"
#
#
# <%= button_to "Delete Image", { action: "delete", id: @image.id },
# method: :delete, data: { confirm: "Are you sure?" } %>
# # => "<form method="post" action="/images/delete/1" class="button_to">
# # <input type="hidden" name="_method" value="delete" />
# # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>"
#
#
# <%= button_to('Destroy', 'http://www.example.com',
# method: :delete, remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
# # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
# # <input name='_method' value='delete' type='hidden' />
# # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
# # </form>"
# #
- 9
def button_to(name = nil, options = nil, html_options = nil, &block)
html_options, options = options, name if block_given?
options ||= {}
html_options ||= {}
html_options = html_options.stringify_keys
url = options.is_a?(String) ? options : url_for(options)
remote = html_options.delete("remote")
params = html_options.delete("params")
method = html_options.delete("method").to_s
method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe
form_method = method == "get" ? "get" : "post"
form_options = html_options.delete("form") || {}
form_options[:class] ||= html_options.delete("form_class") || "button_to"
form_options[:method] = form_method
form_options[:action] = url
form_options[:'data-remote'] = true if remote
request_token_tag = if form_method == "post"
request_method = method.empty? ? "post" : method
token_tag(nil, form_options: { action: url, method: request_method })
else
""
end
html_options = convert_options_to_data_attributes(options, html_options)
html_options["type"] = "submit"
button = if block_given?
content_tag("button", html_options, &block)
else
html_options["value"] = name || url
tag("input", html_options)
end
inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
if params
to_form_params(params).each do |param|
inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value])
end
end
content_tag("form", inner_tags, form_options)
end
# Creates a link tag of the given +name+ using a URL created by the set of
# +options+ unless the current request URI is the same as the links, in
# which case only the name is returned (or the given block is yielded, if
# one exists). You can give +link_to_unless_current+ a block which will
# specialize the default behavior (e.g., show a "Start Here" link rather
# than the link's text).
#
# ==== Examples
# Let's say you have a navigation menu...
#
# <ul id="navbar">
# <li><%= link_to_unless_current("Home", { action: "index" }) %></li>
# <li><%= link_to_unless_current("About Us", { action: "about" }) %></li>
# </ul>
#
# If in the "about" action, it will render...
#
# <ul id="navbar">
# <li><a href="/controller/index">Home</a></li>
# <li>About Us</li>
# </ul>
#
# ...but if in the "index" action, it will render:
#
# <ul id="navbar">
# <li>Home</li>
# <li><a href="/controller/about">About Us</a></li>
# </ul>
#
# The implicit block given to +link_to_unless_current+ is evaluated if the current
# action is the action given. So, if we had a comments page and wanted to render a
# "Go Back" link instead of a link to the comments page, we could do something like this...
#
# <%=
# link_to_unless_current("Comment", { controller: "comments", action: "new" }) do
# link_to("Go back", { controller: "posts", action: "index" })
# end
# %>
- 9
def link_to_unless_current(name, options = {}, html_options = {}, &block)
link_to_unless current_page?(options), name, options, html_options, &block
end
# Creates a link tag of the given +name+ using a URL created by the set of
# +options+ unless +condition+ is true, in which case only the name is
# returned. To specialize the default behavior (i.e., show a login link rather
# than just the plaintext link text), you can pass a block that
# accepts the name or the full argument list for +link_to_unless+.
#
# ==== Examples
# <%= link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) %>
# # If the user is logged in...
# # => <a href="/controller/reply/">Reply</a>
#
# <%=
# link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) do |name|
# link_to(name, { controller: "accounts", action: "signup" })
# end
# %>
# # If the user is logged in...
# # => <a href="/controller/reply/">Reply</a>
# # If not...
# # => <a href="/accounts/signup">Reply</a>
- 9
def link_to_unless(condition, name, options = {}, html_options = {}, &block)
link_to_if !condition, name, options, html_options, &block
end
# Creates a link tag of the given +name+ using a URL created by the set of
# +options+ if +condition+ is true, otherwise only the name is
# returned. To specialize the default behavior, you can pass a block that
# accepts the name or the full argument list for +link_to_if+.
#
# ==== Examples
# <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %>
# # If the user isn't logged in...
# # => <a href="/sessions/new/">Login</a>
#
# <%=
# link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) do
# link_to(@current_user.login, { controller: "accounts", action: "show", id: @current_user })
# end
# %>
# # If the user isn't logged in...
# # => <a href="/sessions/new/">Login</a>
# # If they are logged in...
# # => <a href="/accounts/show/3">my_username</a>
- 9
def link_to_if(condition, name, options = {}, html_options = {}, &block)
if condition
link_to(name, options, html_options)
else
if block_given?
block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
else
ERB::Util.html_escape(name)
end
end
end
# Creates a mailto link tag to the specified +email_address+, which is
# also used as the name of the link unless +name+ is specified. Additional
# HTML attributes for the link can be passed in +html_options+.
#
# +mail_to+ has several methods for customizing the email itself by
# passing special keys to +html_options+.
#
# ==== Options
# * <tt>:subject</tt> - Preset the subject line of the email.
# * <tt>:body</tt> - Preset the body of the email.
# * <tt>:cc</tt> - Carbon Copy additional recipients on the email.
# * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
# * <tt>:reply_to</tt> - Preset the Reply-To field of the email.
#
# ==== Obfuscation
# Prior to Rails 4.0, +mail_to+ provided options for encoding the address
# in order to hinder email harvesters. To take advantage of these options,
# install the +actionview-encoded_mail_to+ gem.
#
# ==== Examples
# mail_to "me@domain.com"
# # => <a href="mailto:me@domain.com">me@domain.com</a>
#
# mail_to "me@domain.com", "My email"
# # => <a href="mailto:me@domain.com">My email</a>
#
# mail_to "me@domain.com", "My email", cc: "ccaddress@domain.com",
# subject: "This is an example email"
# # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
#
# You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
#
# <%= mail_to "me@domain.com" do %>
# <strong>Email me:</strong> <span>me@domain.com</span>
# <% end %>
# # => <a href="mailto:me@domain.com">
# <strong>Email me:</strong> <span>me@domain.com</span>
# </a>
- 9
def mail_to(email_address, name = nil, html_options = {}, &block)
html_options, name = name, nil if block_given?
html_options = (html_options || {}).stringify_keys
extras = %w{ cc bcc body subject reply_to }.map! { |item|
option = html_options.delete(item).presence || next
"#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
extras = extras.empty? ? "" : "?" + extras.join("&")
encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
content_tag("a", name || email_address, html_options, &block)
end
# True if the current request URI was generated by the given +options+.
#
# ==== Examples
# Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
#
# current_page?(action: 'process')
# # => false
#
# current_page?(action: 'checkout')
# # => true
#
# current_page?(controller: 'library', action: 'checkout')
# # => false
#
# current_page?(controller: 'shop', action: 'checkout')
# # => true
#
# current_page?(controller: 'shop', action: 'checkout', order: 'asc')
# # => false
#
# current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1')
# # => true
#
# current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2')
# # => false
#
# current_page?('http://www.example.com/shop/checkout')
# # => true
#
# current_page?('http://www.example.com/shop/checkout', check_parameters: true)
# # => false
#
# current_page?('/shop/checkout')
# # => true
#
# current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
# # => true
#
# Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
#
# current_page?(controller: 'product', action: 'index')
# # => false
#
# We can also pass in the symbol arguments instead of strings.
#
- 9
def current_page?(options, check_parameters: false)
unless request
raise "You cannot use helpers that need to determine the current " \
"page unless your view context provides a Request object " \
"in a #request method"
end
return false unless request.get? || request.head?
check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
url_string = URI::DEFAULT_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
# We ignore any extra parameters in the request_uri if the
# submitted URL doesn't have any either. This lets the function
# work with things like ?order=asc
# the behaviour can be disabled with check_parameters: true
request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
request_uri = URI::DEFAULT_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
if url_string.start_with?("/") && url_string != "/"
url_string.chomp!("/")
request_uri.chomp!("/")
end
if %r{^\w+://}.match?(url_string)
url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
else
url_string == request_uri
end
end
# Creates an SMS anchor link tag to the specified +phone_number+, which is
# also used as the name of the link unless +name+ is specified. Additional
# HTML attributes for the link can be passed in +html_options+.
#
# When clicked, an SMS message is prepopulated with the passed phone number
# and optional +body+ value.
#
# +sms_to+ has a +body+ option for customizing the SMS message itself by
# passing special keys to +html_options+.
#
# ==== Options
# * <tt>:body</tt> - Preset the body of the message.
#
# ==== Examples
# sms_to "5155555785"
# # => <a href="sms:5155555785;">5155555785</a>
#
# sms_to "5155555785", "Text me"
# # => <a href="sms:5155555785;">Text me</a>
#
# sms_to "5155555785", "Text me",
# body: "Hello Jim I have a question about your product."
# # => <a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>
#
# You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
#
# <%= sms_to "5155555785" do %>
# <strong>Text me:</strong>
# <% end %>
# # => <a href="sms:5155555785;">
# <strong>Text me:</strong>
# </a>
- 9
def sms_to(phone_number, name = nil, html_options = {}, &block)
html_options, name = name, nil if block_given?
html_options = (html_options || {}).stringify_keys
extras = %w{ body }.map! { |item|
option = html_options.delete(item).presence || next
"#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
extras = extras.empty? ? "" : "?&" + extras.join("&")
encoded_phone_number = ERB::Util.url_encode(phone_number)
html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
content_tag("a", name || phone_number, html_options, &block)
end
# Creates a TEL anchor link tag to the specified +phone_number+, which is
# also used as the name of the link unless +name+ is specified. Additional
# HTML attributes for the link can be passed in +html_options+.
#
# When clicked, the default app to make calls is opened, and it
# is prepopulated with the passed phone number and optional
# +country_code+ value.
#
# +phone_to+ has an optional +country_code+ option which automatically adds the country
# code as well as the + sign in the phone numer that gets prepopulated,
# for example if +country_code: "01"+ +\+01+ will be prepended to the
# phone numer, by passing special keys to +html_options+.
#
# ==== Options
# * <tt>:country_code</tt> - Prepends the country code to the number
#
# ==== Examples
# phone_to "1234567890"
# # => <a href="tel:1234567890">1234567890</a>
#
# phone_to "1234567890", "Phone me"
# # => <a href="tel:134567890">Phone me</a>
#
# phone_to "1234567890", "Phone me", country_code: "01"
# # => <a href="tel:+015155555785">Phone me</a>
#
# You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
#
# <%= phone_to "1234567890" do %>
# <strong>Phone me:</strong>
# <% end %>
# # => <a href="tel:1234567890">
# <strong>Phone me:</strong>
# </a>
- 9
def phone_to(phone_number, name = nil, html_options = {}, &block)
html_options, name = name, nil if block_given?
html_options = (html_options || {}).stringify_keys
country_code = html_options.delete("country_code").presence
country_code = country_code.nil? ? "" : "+#{ERB::Util.url_encode(country_code)}"
encoded_phone_number = ERB::Util.url_encode(phone_number)
html_options["href"] = "tel:#{country_code}#{encoded_phone_number}"
content_tag("a", name || phone_number, html_options, &block)
end
- 9
private
- 9
def convert_options_to_data_attributes(options, html_options)
if html_options
html_options = html_options.stringify_keys
html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options)
method = html_options.delete("method")
add_method_to_attributes!(html_options, method) if method
html_options
else
link_to_remote_options?(options) ? { "data-remote" => "true" } : {}
end
end
- 9
def link_to_remote_options?(options)
if options.is_a?(Hash)
options.delete("remote") || options.delete(:remote)
end
end
- 9
def add_method_to_attributes!(html_options, method)
if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
if html_options["rel"].blank?
html_options["rel"] = "nofollow"
else
html_options["rel"] = "#{html_options["rel"]} nofollow"
end
end
html_options["data-method"] = method
end
- 9
STRINGIFIED_COMMON_METHODS = {
get: "get",
delete: "delete",
patch: "patch",
post: "post",
put: "put",
}.freeze
- 9
def method_not_get_method?(method)
return false unless method
(STRINGIFIED_COMMON_METHODS[method] || method.to_s.downcase) != "get"
end
- 9
def token_tag(token = nil, form_options: {})
if token != false && defined?(protect_against_forgery?) && protect_against_forgery?
token ||= form_authenticity_token(form_options: form_options)
tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
else
""
end
end
- 9
def method_tag(method)
tag("input", type: "hidden", name: "_method", value: method.to_s)
end
# Returns an array of hashes each containing :name and :value keys
# suitable for use as the names and values of form input fields:
#
# to_form_params(name: 'David', nationality: 'Danish')
# # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}]
#
# to_form_params(country: { name: 'Denmark' })
# # => [{name: 'country[name]', value: 'Denmark'}]
#
# to_form_params(countries: ['Denmark', 'Sweden']})
# # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}]
#
# An optional namespace can be passed to enclose key names:
#
# to_form_params({ name: 'Denmark' }, 'country')
# # => [{name: 'country[name]', value: 'Denmark'}]
- 9
def to_form_params(attribute, namespace = nil)
attribute = if attribute.respond_to?(:permitted?)
attribute.to_h
else
attribute
end
params = []
case attribute
when Hash
attribute.each do |key, value|
prefix = namespace ? "#{namespace}[#{key}]" : key
params.push(*to_form_params(value, prefix))
end
when Array
array_prefix = "#{namespace}[]"
attribute.each do |value|
params.push(*to_form_params(value, array_prefix))
end
else
params << { name: namespace.to_s, value: attribute.to_param }
end
params.sort_by { |pair| pair[:name] }
end
end
end
end
# frozen_string_literal: true
- 9
require "action_view/rendering"
- 9
require "active_support/core_ext/module/redefine_method"
- 9
module ActionView
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
# repeated setups. The inclusion pattern has pages that look like this:
#
# <%= render "shared/header" %>
# Hello World
# <%= render "shared/footer" %>
#
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
#
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
# that the header and footer are only mentioned in one place, like this:
#
# // The header part of this layout
# <%= yield %>
# // The footer part of this layout
#
# And then you have content pages that look like this:
#
# hello world
#
# At rendering time, the content page is computed and then inserted in the layout, like this:
#
# // The header part of this layout
# hello world
# // The footer part of this layout
#
# == Accessing shared variables
#
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
# references that won't materialize before rendering time:
#
# <h1><%= @page_title %></h1>
# <%= yield %>
#
# ...and content pages that fulfill these references _at_ rendering time:
#
# <% @page_title = "Welcome" %>
# Off-world colonies offers you a chance to start a new life
#
# The result after rendering is:
#
# <h1>Welcome</h1>
# Off-world colonies offers you a chance to start a new life
#
# == Layout assignment
#
# You can either specify a layout declaratively (using the #layout class method) or give
# it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
# If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
#
# For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
# that template will be used for all actions in PostsController and controllers inheriting
# from PostsController.
#
# If you use a module, for instance Weblog::PostsController, you will need a template named
# <tt>app/views/layouts/weblog/posts.html.erb</tt>.
#
# Since all your controllers inherit from ApplicationController, they will use
# <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
# or provided.
#
# == Inheritance Examples
#
# class BankController < ActionController::Base
# # bank.html.erb exists
#
# class ExchangeController < BankController
# # exchange.html.erb exists
#
# class CurrencyController < BankController
#
# class InformationController < BankController
# layout "information"
#
# class TellerController < InformationController
# # teller.html.erb exists
#
# class EmployeeController < InformationController
# # employee.html.erb exists
# layout nil
#
# class VaultController < BankController
# layout :access_level_layout
#
# class TillController < BankController
# layout false
#
# In these examples, we have three implicit lookup scenarios:
# * The +BankController+ uses the "bank" layout.
# * The +ExchangeController+ uses the "exchange" layout.
# * The +CurrencyController+ inherits the layout from BankController.
#
# However, when a layout is explicitly set, the explicitly set layout wins:
# * The +InformationController+ uses the "information" layout, explicitly set.
# * The +TellerController+ also uses the "information" layout, because the parent explicitly set it.
# * The +EmployeeController+ uses the "employee" layout, because it set the layout to +nil+, resetting the parent configuration.
# * The +VaultController+ chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
# * The +TillController+ does not use a layout at all.
#
# == Types of layouts
#
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
#
# The method reference is the preferred approach to variable layouts and is used like this:
#
# class WeblogController < ActionController::Base
# layout :writers_and_readers
#
# def index
# # fetching posts
# end
#
# private
# def writers_and_readers
# logged_in? ? "writer_layout" : "reader_layout"
# end
# end
#
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
# is logged in or not.
#
# If you want to use an inline method, such as a proc, do something like this:
#
# class WeblogController < ActionController::Base
# layout proc { |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
# end
#
# If an argument isn't given to the proc, it's evaluated in the context of
# the current controller anyway.
#
# class WeblogController < ActionController::Base
# layout proc { logged_in? ? "writer_layout" : "reader_layout" }
# end
#
# Of course, the most common way of specifying a layout is still just as a plain template name:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard"
# end
#
# The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
# <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
#
# Setting the layout to +nil+ forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
# Setting it to +nil+ is useful to re-enable template lookup overriding a previous configuration set in the parent:
#
# class ApplicationController < ActionController::Base
# layout "application"
# end
#
# class PostsController < ApplicationController
# # Will use "application" layout
# end
#
# class CommentsController < ApplicationController
# # Will search for "comments" layout and fallback "application" layout
# layout nil
# end
#
# == Conditional layouts
#
# If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
# a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
# <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard", except: :rss
#
# # ...
#
# end
#
# This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will
# be rendered directly, without wrapping a layout around the rendered view.
#
# Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
# #<tt>except: [ :rss, :text_only ]</tt> is valid, as is <tt>except: :rss</tt>.
#
# == Using a different layout in the action render call
#
# If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
# Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
# You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
#
# class WeblogController < ActionController::Base
# layout "weblog_standard"
#
# def help
# render action: "help", layout: "help"
# end
# end
#
# This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead.
- 9
module Layouts
- 9
extend ActiveSupport::Concern
- 9
include ActionView::Rendering
- 9
included do
- 15
class_attribute :_layout, instance_accessor: false
- 15
class_attribute :_layout_conditions, instance_accessor: false, default: {}
- 15
_write_layout_method
end
- 9
delegate :_layout_conditions, to: :class
- 9
module ClassMethods
- 9
def inherited(klass) # :nodoc:
- 213
super
- 213
klass._write_layout_method
end
# This module is mixed in if layout conditions are provided. This means
# that if no layout conditions are used, this method is not used
- 9
module LayoutConditions # :nodoc:
- 9
private
# Determines whether the current action has a layout definition by
# checking the action name against the :only and :except conditions
# set by the <tt>layout</tt> method.
#
# ==== Returns
# * <tt>Boolean</tt> - True if the action has a layout definition, false otherwise.
- 9
def _conditional_layout?
return unless super
conditions = _layout_conditions
if only = conditions[:only]
only.include?(action_name)
elsif except = conditions[:except]
!except.include?(action_name)
else
true
end
end
end
# Specify the layout to use for this class.
#
# If the specified layout is a:
# String:: the String is the template name
# Symbol:: call the method specified by the symbol
# Proc:: call the passed Proc
# false:: There is no layout
# true:: raise an ArgumentError
# nil:: Force default layout behavior with inheritance
#
# Return value of +Proc+ and +Symbol+ arguments should be +String+, +false+, +true+ or +nil+
# with the same meaning as described above.
# ==== Parameters
# * <tt>layout</tt> - The layout to use.
#
# ==== Options (conditions)
# * :only - A list of actions to apply this layout to.
# * :except - Apply this layout to all actions but this one.
- 9
def layout(layout, conditions = {})
- 108
include LayoutConditions unless conditions.empty?
- 135
conditions.each { |k, v| conditions[k] = Array(v).map(&:to_s) }
- 108
self._layout_conditions = conditions
- 108
self._layout = layout
- 108
_write_layout_method
end
# Creates a _layout method to be called by _default_layout .
#
# If a layout is not explicitly mentioned then look for a layout with the controller's name.
# if nothing is found then try same procedure to find super class's layout.
- 9
def _write_layout_method # :nodoc:
- 336
silence_redefinition_of_method(:_layout)
- 336
prefixes = /\blayouts/.match?(_implied_layout_name) ? [] : ["layouts"]
- 336
default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super"
- 336
name_clause = if name
- 336
default_behavior
else
<<-RUBY
super
RUBY
end
- 336
layout_definition = \
case _layout
when String
- 87
_layout.inspect
when Symbol
- 18
<<-RUBY
#{_layout}.tap do |layout|
return #{default_behavior} if layout.nil?
unless layout.is_a?(String) || !layout
raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \
"should have returned a String, false, or nil"
end
end
RUBY
when Proc
- 18
define_method :_layout_from_proc, &_layout
- 18
private :_layout_from_proc
- 18
<<-RUBY
result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'})
return #{default_behavior} if result.nil?
result
RUBY
when false
- 3
nil
when true
raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil"
when nil
- 210
name_clause
end
- 336
class_eval <<-RUBY, __FILE__, __LINE__ + 1
# frozen_string_literal: true
def _layout(lookup_context, formats)
if _conditional_layout?
#{layout_definition}
else
#{name_clause}
end
end
private :_layout
RUBY
end
- 9
private
# If no layout is supplied, look for a template named the return
# value of this method.
#
# ==== Returns
# * <tt>String</tt> - A template name
- 9
def _implied_layout_name
- 510
controller_path
end
end
- 9
def _normalize_options(options) # :nodoc:
super
if _include_layout?(options)
layout = options.delete(:layout) { :default }
options[:layout] = _layout_for_option(layout)
end
end
- 9
attr_internal_writer :action_has_layout
- 9
def initialize(*) # :nodoc:
@_action_has_layout = true
super
end
# Controls whether an action should be rendered using a layout.
# If you want to disable any <tt>layout</tt> settings for the
# current action so that it is rendered without a layout then
# either override this method in your controller to return false
# for that action or set the <tt>action_has_layout</tt> attribute
# to false before rendering.
- 9
def action_has_layout?
@_action_has_layout
end
- 9
private
- 9
def _conditional_layout?
true
end
# This will be overwritten by _write_layout_method
- 9
def _layout(*); end
# Determine the layout for a given name, taking into account the name type.
#
# ==== Parameters
# * <tt>name</tt> - The name of the template
- 9
def _layout_for_option(name)
case name
when String then _normalize_layout(name)
when Proc then name
when true then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, true) }
when :default then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, false) }
when false, nil then nil
else
raise ArgumentError,
"String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}"
end
end
- 9
def _normalize_layout(value)
value.is_a?(String) && !value.match?(/\blayouts/) ? "layouts/#{value}" : value
end
# Returns the default layout for this controller.
# Optionally raises an exception if the layout could not be found.
#
# ==== Parameters
# * <tt>formats</tt> - The formats accepted to this layout
# * <tt>require_layout</tt> - If set to +true+ and layout is not found,
# an +ArgumentError+ exception is raised (defaults to +false+)
#
# ==== Returns
# * <tt>template</tt> - The template object for the default layout (or +nil+)
- 9
def _default_layout(lookup_context, formats, require_layout = false)
begin
value = _layout(lookup_context, formats) if action_has_layout?
rescue NameError => e
raise e, "Could not render layout: #{e.message}"
end
if require_layout && action_has_layout? && !value
raise ArgumentError,
"There was no default layout for #{self.class} in #{view_paths.inspect}"
end
_normalize_layout(value)
end
- 9
def _include_layout?(options)
(options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
end
end
end
# frozen_string_literal: true
- 3
require "active_support/log_subscriber"
- 3
module ActionView
# = Action View Log Subscriber
#
# Provides functionality so that Rails can output logs from Action View.
- 3
class LogSubscriber < ActiveSupport::LogSubscriber
- 3
VIEWS_PATTERN = /^app\/views\//
- 3
def initialize
- 3
@root = nil
- 3
super
end
- 3
def render_template(event)
info do
message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
end
end
- 3
def render_partial(event)
debug do
message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil?
message
end
end
- 3
def render_layout(event)
info do
message = +" Rendered layout #{from_rails_root(event.payload[:identifier])}"
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
end
end
- 3
def render_collection(event)
identifier = event.payload[:identifier] || "templates"
debug do
message = +" Rendered collection of #{from_rails_root(identifier)}"
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
message
end
end
- 3
def start(name, id, payload)
log_rendering_start(payload, name)
super
end
- 3
def logger
ActionView::Base.logger
end
- 3
private
- 3
EMPTY = ""
- 3
def from_rails_root(string) # :doc:
string = string.sub(rails_root, EMPTY)
string.sub!(VIEWS_PATTERN, EMPTY)
string
end
- 3
def rails_root # :doc:
@root ||= "#{Rails.root}/"
end
- 3
def render_count(payload) # :doc:
if payload[:cache_hits]
"[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
else
"[#{payload[:count]} times]"
end
end
- 3
def cache_message(payload) # :doc:
case payload[:cache_hit]
when :hit
"[cache hit]"
when :miss
"[cache miss]"
end
end
- 3
def log_rendering_start(payload, name)
debug do
qualifier =
if name == "render_template.action_view"
""
elsif name == "render_layout.action_view"
"layout "
end
return unless qualifier
message = +" Rendering #{qualifier}#{from_rails_root(payload[:identifier])}"
message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
message
end
end
end
end
- 3
ActionView::LogSubscriber.attach_to :action_view
# frozen_string_literal: true
- 3
require "concurrent/map"
- 3
require "active_support/core_ext/module/attribute_accessors"
- 3
require "action_view/template/resolver"
- 3
module ActionView
# = Action View Lookup Context
#
# <tt>LookupContext</tt> is the object responsible for holding all information
# required for looking up templates, i.e. view paths and details.
# <tt>LookupContext</tt> is also responsible for generating a key, given to
# view paths, used in the resolver cache lookup. Since this key is generated
# only once during the request, it speeds up all cache accesses.
- 3
class LookupContext #:nodoc:
- 3
attr_accessor :prefixes, :rendered_format
- 3
deprecate :rendered_format
- 3
deprecate :rendered_format=
- 3
mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances
- 3
mattr_accessor :registered_details, default: []
- 3
def self.register_detail(name, &block)
- 12
registered_details << name
- 12
Accessors::DEFAULT_PROCS[name] = block
- 12
Accessors.define_method(:"default_#{name}", &block)
- 12
Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{name}
@details[:#{name}] || []
end
def #{name}=(value)
value = value.present? ? Array(value) : default_#{name}
_set_detail(:#{name}, value) if value != @details[:#{name}]
end
METHOD
end
# Holds accessors for the registered details.
- 3
module Accessors #:nodoc:
- 3
DEFAULT_PROCS = {}
end
- 3
register_detail(:locale) do
locales = [I18n.locale]
locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks
locales << I18n.default_locale
locales.uniq!
locales
end
- 3
register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
- 3
register_detail(:variants) { [] }
- 3
register_detail(:handlers) { Template::Handlers.extensions }
- 3
class DetailsKey #:nodoc:
- 3
alias :eql? :equal?
- 3
@details_keys = Concurrent::Map.new
- 3
@digest_cache = Concurrent::Map.new
- 3
@view_context_mutex = Mutex.new
- 3
def self.digest_cache(details)
@digest_cache[details_cache_key(details)] ||= Concurrent::Map.new
end
- 3
def self.details_cache_key(details)
if details[:formats]
details = details.dup
details[:formats] &= Template::Types.symbols
end
@details_keys[details] ||= Object.new
end
- 3
def self.clear
- 3
ActionView::ViewPaths.all_view_paths.each do |path_set|
- 3
path_set.each(&:clear_cache)
end
- 3
ActionView::LookupContext.fallbacks.each(&:clear_cache)
- 3
@view_context_class = nil
- 3
@details_keys.clear
- 3
@digest_cache.clear
end
- 3
def self.digest_caches
@digest_cache.values
end
- 3
def self.view_context_class(klass)
@view_context_mutex.synchronize do
@view_context_class ||= klass.with_empty_template_cache
end
end
end
# Add caching behavior on top of Details.
- 3
module DetailsCache
- 3
attr_accessor :cache
# Calculate the details key. Remove the handlers from calculation to improve performance
# since the user cannot modify it explicitly.
- 3
def details_key #:nodoc:
@details_key ||= DetailsKey.details_cache_key(@details) if @cache
end
# Temporary skip passing the details_key forward.
- 3
def disable_cache
old_value, @cache = @cache, false
yield
ensure
@cache = old_value
end
- 3
private
- 3
def _set_detail(key, value) # :doc:
@details = @details.dup if @digest_cache || @details_key
@digest_cache = nil
@details_key = nil
@details[key] = value
end
end
# Helpers related to template lookup using the lookup context information.
- 3
module ViewPaths
- 3
attr_reader :view_paths, :html_fallback_for_js
- 3
def find(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
end
- 3
alias :find_template :find
- 3
alias :find_file :find
- 3
deprecate :find_file
- 3
def find_all(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
end
- 3
def exists?(name, prefixes = [], partial = false, keys = [], **options)
@view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
end
- 3
alias :template_exists? :exists?
- 3
def any?(name, prefixes = [], partial = false)
@view_paths.exists?(*args_for_any(name, prefixes, partial))
end
- 3
alias :any_templates? :any?
# Adds fallbacks to the view paths. Useful in cases when you are rendering
# a :file.
- 3
def with_fallbacks
view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq)
if block_given?
ActiveSupport::Deprecation.warn <<~eowarn.squish
Calling `with_fallbacks` with a block is deprecated. Call methods on
the lookup context returned by `with_fallbacks` instead.
eowarn
begin
_view_paths = @view_paths
@view_paths = view_paths
yield
ensure
@view_paths = _view_paths
end
else
ActionView::LookupContext.new(view_paths, @details, @prefixes)
end
end
- 3
private
# Whenever setting view paths, makes a copy so that we can manipulate them in
# instance objects as we wish.
- 3
def build_view_paths(paths)
ActionView::PathSet.new(Array(paths))
end
- 3
def args_for_lookup(name, prefixes, partial, keys, details_options)
name, prefixes = normalize_name(name, prefixes)
details, details_key = detail_args_for(details_options)
[name, prefixes, partial || false, details, details_key, keys]
end
# Compute details hash and key according to user options (e.g. passed from #render).
- 3
def detail_args_for(options) # :doc:
return @details, details_key if options.empty? # most common path.
user_details = @details.merge(options)
if @cache
details_key = DetailsKey.details_cache_key(user_details)
else
details_key = nil
end
[user_details, details_key]
end
- 3
def args_for_any(name, prefixes, partial)
name, prefixes = normalize_name(name, prefixes)
details, details_key = detail_args_for_any
[name, prefixes, partial || false, details, details_key]
end
- 3
def detail_args_for_any
@detail_args_for_any ||= begin
details = {}
registered_details.each do |k|
if k == :variants
details[k] = :any
else
details[k] = Accessors::DEFAULT_PROCS[k].call
end
end
if @cache
[details, DetailsKey.details_cache_key(details)]
else
[details, nil]
end
end
end
# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
- 3
def normalize_name(name, prefixes)
prefixes = prefixes.presence
parts = name.to_s.split("/")
parts.shift if parts.first.empty?
name = parts.pop
return name, prefixes || [""] if parts.empty?
parts = parts.join("/")
prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
return name, prefixes
end
end
- 3
include Accessors
- 3
include DetailsCache
- 3
include ViewPaths
- 3
def initialize(view_paths, details = {}, prefixes = [])
@details_key = nil
@digest_cache = nil
@cache = true
@prefixes = prefixes
@details = initialize_details({}, details)
@view_paths = build_view_paths(view_paths)
end
- 3
def digest_cache
@digest_cache ||= DetailsKey.digest_cache(@details)
end
- 3
def with_prepended_formats(formats)
details = @details.dup
details[:formats] = formats
self.class.new(@view_paths, details, @prefixes)
end
- 3
def initialize_details(target, details)
registered_details.each do |k|
target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call
end
target
end
- 3
private :initialize_details
# Override formats= to expand ["*/*"] values and automatically
# add :html as fallback to :js.
- 3
def formats=(values)
if values
values = values.dup
values.concat(default_formats) if values.delete "*/*"
values.uniq!
invalid_values = (values - Template::Types.symbols)
unless invalid_values.empty?
raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
end
if values == [:js]
values << :html
@html_fallback_for_js = true
end
end
super(values)
end
# Override locale to return a symbol instead of array.
- 3
def locale
@details[:locale].first
end
# Overload locale= to also set the I18n.locale. If the current I18n.config object responds
# to original_config, it means that it has a copy of the original I18n configuration and it's
# acting as proxy, which we need to skip.
- 3
def locale=(value)
if value
config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config
config.locale = value
end
super(default_locale)
end
end
end
# frozen_string_literal: true
- 9
module ActionView
- 9
module ModelNaming #:nodoc:
# Converts the given object to an ActiveModel compliant one.
- 9
def convert_to_model(object)
object.respond_to?(:to_model) ? object.to_model : object
end
- 9
def model_name_from_record_or_class(record_or_class)
convert_to_model(record_or_class).model_name
end
end
end
# frozen_string_literal: true
- 9
module ActionView #:nodoc:
# = Action View PathSet
#
# This class is used to store and access paths in Action View. A number of
# operations are defined so that you can search among the paths in this
# set and also perform operations on other +PathSet+ objects.
#
# A +LookupContext+ will use a +PathSet+ to store the paths in its context.
- 9
class PathSet #:nodoc:
- 9
include Enumerable
- 9
attr_reader :paths
- 9
delegate :[], :include?, :pop, :size, :each, to: :paths
- 9
def initialize(paths = [])
- 60
@paths = typecast paths
end
- 9
def initialize_copy(other)
- 3
@paths = other.paths.dup
- 3
self
end
- 9
def to_ary
- 6
paths.dup
end
- 9
def compact
PathSet.new paths.compact
end
- 9
def +(array)
- 6
PathSet.new(paths + array)
end
- 9
%w(<< concat push insert unshift).each do |method|
- 45
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{method}(*args)
paths.#{method}(*typecast(args))
end
METHOD
end
- 9
def find(*args)
find_all(*args).first || raise(MissingTemplate.new(self, *args))
end
- 9
alias :find_file :find
- 9
deprecate :find_file
- 9
def find_all(path, prefixes = [], *args)
_find_all path, prefixes, args
end
- 9
def exists?(path, prefixes, *args)
find_all(path, prefixes, *args).any?
end
- 9
def find_all_with_query(query) # :nodoc:
paths.each do |resolver|
templates = resolver.find_all_with_query(query)
return templates unless templates.empty?
end
[]
end
- 9
private
- 9
def _find_all(path, prefixes, args)
prefixes = [prefixes] if String === prefixes
prefixes.each do |prefix|
paths.each do |resolver|
templates = resolver.find_all(path, prefix, *args)
return templates unless templates.empty?
end
end
[]
end
- 9
def typecast(paths)
- 60
paths.map do |path|
- 33
case path
when Pathname, String
- 21
OptimizedFileSystemResolver.new path.to_s
else
- 12
path
end
end
end
end
end
# frozen_string_literal: true
require "action_view"
require "rails"
module ActionView
# = Action View Railtie
class Railtie < Rails::Engine # :nodoc:
NULL_OPTION = Object.new
config.action_view = ActiveSupport::OrderedOptions.new
config.action_view.embed_authenticity_token_in_remote_forms = nil
config.action_view.debug_missing_translation = true
config.action_view.default_enforce_utf8 = nil
config.action_view.finalize_compiled_template_methods = NULL_OPTION
config.eager_load_namespaces << ActionView
config.after_initialize do |app|
ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
end
config.after_initialize do |app|
form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
end
config.after_initialize do |app|
form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
unless form_with_generates_ids.nil?
ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
end
end
config.after_initialize do |app|
default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
unless default_enforce_utf8.nil?
ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
end
end
config.after_initialize do |app|
ActiveSupport.on_load(:action_view) do
app.config.action_view.each do |k, v|
if k == :raise_on_missing_translations
ActiveSupport::Deprecation.warn \
"action_view.raise_on_missing_translations is deprecated and will be removed in Rails 6.2. " \
"Set i18n.raise_on_missing_translations instead. " \
"Note that this new setting also affects how missing translations are handled in controllers."
end
send "#{k}=", v
end
end
end
initializer "action_view.finalize_compiled_template_methods" do |app|
ActiveSupport.on_load(:action_view) do
option = app.config.action_view.delete(:finalize_compiled_template_methods)
if option != NULL_OPTION
ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect"
end
end
end
initializer "action_view.logger" do
ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
end
initializer "action_view.caching" do |app|
ActiveSupport.on_load(:action_view) do
if app.config.action_view.cache_template_loading.nil?
ActionView::Resolver.caching = app.config.cache_classes
end
end
end
initializer "action_view.setup_action_pack" do |app|
ActiveSupport.on_load(:action_controller) do
ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
end
end
initializer "action_view.collection_caching", after: "action_controller.set_configs" do |app|
PartialRenderer.collection_cache = app.config.action_controller.cache_store
end
config.after_initialize do |app|
enable_caching = if app.config.action_view.cache_template_loading.nil?
app.config.cache_classes
else
app.config.action_view.cache_template_loading
end
unless enable_caching
app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
end
end
rake_tasks do |app|
unless app.config.api_only
load "action_view/tasks/cache_digests.rake"
end
end
end
end
# frozen_string_literal: true
- 9
require "active_support/core_ext/module"
- 9
require "action_view/model_naming"
- 9
module ActionView
# RecordIdentifier encapsulates methods used by various ActionView helpers
# to associate records with DOM elements.
#
# Consider for example the following code that form of post:
#
# <%= form_for(post) do |f| %>
# <%= f.text_field :body %>
# <% end %>
#
# When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
# is:
#
# <form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post">
# <input type="text" name="post[body]" id="post_body" />
# </form>
#
# When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML
# is:
#
# <form class="edit_post" id="edit_post_42" action="/posts/42" accept-charset="UTF-8" method="post">
# <input type="text" value="What a wonderful world!" name="post[body]" id="post_body" />
# </form>
#
# In both cases, the +id+ and +class+ of the wrapping DOM element are
# automatically generated, following naming conventions encapsulated by the
# RecordIdentifier methods #dom_id and #dom_class:
#
# dom_id(Post.new) # => "new_post"
# dom_class(Post.new) # => "post"
# dom_id(Post.find 42) # => "post_42"
# dom_class(Post.find 42) # => "post"
#
# Note that these methods do not strictly require +Post+ to be a subclass of
# ActiveRecord::Base.
# Any +Post+ class will work as long as its instances respond to +to_key+
# and +model_name+, given that +model_name+ responds to +param_key+.
# For instance:
#
# class Post
# attr_accessor :to_key
#
# def model_name
# OpenStruct.new param_key: 'post'
# end
#
# def self.find(id)
# new.tap { |post| post.to_key = [id] }
# end
# end
- 9
module RecordIdentifier
- 9
extend self
- 9
extend ModelNaming
- 9
include ModelNaming
- 9
JOIN = "_"
- 9
NEW = "new"
# The DOM class convention is to use the singular form of an object or class.
#
# dom_class(post) # => "post"
# dom_class(Person) # => "person"
#
# If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
#
# dom_class(post, :edit) # => "edit_post"
# dom_class(Person, :edit) # => "edit_person"
- 9
def dom_class(record_or_class, prefix = nil)
singular = model_name_from_record_or_class(record_or_class).param_key
prefix ? "#{prefix}#{JOIN}#{singular}" : singular
end
# The DOM id convention is to use the singular form of an object or class with the id following an underscore.
# If no id is found, prefix with "new_" instead.
#
# dom_id(Post.find(45)) # => "post_45"
# dom_id(Post.new) # => "new_post"
#
# If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
#
# dom_id(Post.find(45), :edit) # => "edit_post_45"
# dom_id(Post.new, :custom) # => "custom_post"
- 9
def dom_id(record, prefix = nil)
if record_id = record_key_for_dom_id(record)
"#{dom_class(record, prefix)}#{JOIN}#{record_id}"
else
dom_class(record, prefix || NEW)
end
end
- 9
private
# Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
# This can be overwritten to customize the default generated string representation if desired.
# If you need to read back a key from a dom_id in order to query for the underlying database record,
# you should write a helper like 'person_record_from_dom_id' that will extract the key either based
# on the default implementation (which just joins all key attributes with '_') or on your own
# overwritten version of the method. By default, this implementation passes the key string through a
# method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
# make sure yourself that your dom ids are valid, in case you overwrite this method.
- 9
def record_key_for_dom_id(record) # :doc:
key = convert_to_model(record).to_key
key ? key.join(JOIN) : key
end
end
end
# frozen_string_literal: true
- 3
require "concurrent/map"
- 3
module ActionView
# This class defines the interface for a renderer. Each class that
# subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
# render a specific type of object.
#
# The base +Renderer+ class uses its +render+ method to delegate to the
# renderers. These currently consist of
#
# PartialRenderer - Used for rendering partials
# TemplateRenderer - Used for rendering other types of templates
# StreamingTemplateRenderer - Used for streaming
#
# Whenever the +render+ method is called on the base +Renderer+ class, a new
# renderer object of the correct type is created, and the +render+ method on
# that new object is called in turn. This abstracts the set up and rendering
# into a separate classes for partials and templates.
- 3
class AbstractRenderer #:nodoc:
- 3
delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
- 3
def initialize(lookup_context)
@lookup_context = lookup_context
end
- 3
def render
raise NotImplementedError
end
- 3
module ObjectRendering # :nodoc:
- 3
PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
h[k] = Concurrent::Map.new
end
- 3
def initialize(lookup_context, options)
super
@context_prefix = lookup_context.prefixes.first
end
- 3
private
- 3
def local_variable(path)
if as = @options[:as]
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
as.to_sym
else
begin
base = path.end_with?("/") ? "" : File.basename(path)
raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
$1.to_sym
end
end
end
- 3
IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
"make sure your partial name starts with underscore."
- 3
OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
"make sure it starts with lowercase letter, " \
"and is followed by any combination of letters, numbers and underscores."
- 3
def raise_invalid_identifier(path)
raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
end
- 3
def raise_invalid_option_as(as)
raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
end
# Obtains the path to where the object's partial is located. If the object
# responds to +to_partial_path+, then +to_partial_path+ will be called and
# will provide the path. If the object does not respond to +to_partial_path+,
# then an +ArgumentError+ is raised.
#
# If +prefix_partial_path_with_controller_namespace+ is true, then this
# method will prefix the partial paths with a namespace.
- 3
def partial_path(object, view)
object = object.to_model if object.respond_to?(:to_model)
path = if object.respond_to?(:to_partial_path)
object.to_partial_path
else
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
end
if view.prefix_partial_path_with_controller_namespace
PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
else
path
end
end
- 3
def merge_prefix_into_object_path(prefix, object_path)
if prefix.include?(?/) && object_path.include?(?/)
prefixes = []
prefix_array = File.dirname(prefix).split("/")
object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
prefix_array.each_with_index do |dir, index|
break if dir == object_path_array[index]
prefixes << dir
end
(prefixes << object_path).join("/")
else
object_path
end
end
end
- 3
class RenderedCollection # :nodoc:
- 3
def self.empty(format)
EmptyCollection.new format
end
- 3
attr_reader :rendered_templates
- 3
def initialize(rendered_templates, spacer)
@rendered_templates = rendered_templates
@spacer = spacer
end
- 3
def body
@rendered_templates.map(&:body).join(@spacer.body).html_safe
end
- 3
def format
rendered_templates.first.format
end
- 3
class EmptyCollection
- 3
attr_reader :format
- 3
def initialize(format)
@format = format
end
- 3
def body; nil; end
end
end
- 3
class RenderedTemplate # :nodoc:
- 3
attr_reader :body, :template
- 3
def initialize(body, template)
@body = body
@template = template
end
- 3
def format
template.format
end
- 3
EMPTY_SPACER = Struct.new(:body).new
end
- 3
private
- 3
NO_DETAILS = {}.freeze
- 3
def extract_details(options) # :doc:
details = nil
@lookup_context.registered_details.each do |key|
value = options[key]
if value
(details ||= {})[key] = Array(value)
end
end
details || NO_DETAILS
end
- 3
def prepend_formats(formats) # :doc:
formats = Array(formats)
return if formats.empty? || @lookup_context.html_fallback_for_js
@lookup_context.formats = formats | @lookup_context.formats
end
- 3
def build_rendered_template(content, template)
RenderedTemplate.new content, template
end
- 3
def build_rendered_collection(templates, spacer)
RenderedCollection.new templates, spacer
end
end
end
# frozen_string_literal: true
- 3
require "action_view/renderer/partial_renderer"
- 3
module ActionView
- 3
class PartialIteration
# The number of iterations that will be done by the partial.
- 3
attr_reader :size
# The current iteration of the partial.
- 3
attr_reader :index
- 3
def initialize(size)
@size = size
@index = 0
end
# Check if this is the first iteration of the partial.
- 3
def first?
index == 0
end
# Check if this is the last iteration of the partial.
- 3
def last?
index == size - 1
end
- 3
def iterate! # :nodoc:
@index += 1
end
end
- 3
class CollectionRenderer < PartialRenderer # :nodoc:
- 3
include ObjectRendering
- 3
class CollectionIterator # :nodoc:
- 3
include Enumerable
- 3
def initialize(collection)
@collection = collection
end
- 3
def each(&blk)
@collection.each(&blk)
end
- 3
def size
@collection.size
end
end
- 3
class SameCollectionIterator < CollectionIterator # :nodoc:
- 3
def initialize(collection, path, variables)
super(collection)
@path = path
@variables = variables
end
- 3
def from_collection(collection)
self.class.new(collection, @path, @variables)
end
- 3
def each_with_info
return enum_for(:each_with_info) unless block_given?
variables = [@path] + @variables
@collection.each { |o| yield(o, variables) }
end
end
- 3
class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
- 3
def initialize(collection, path, variables, relation)
super(collection, path, variables)
relation.skip_preloading! unless relation.loaded?
@relation = relation
end
- 3
def from_collection(collection)
self.class.new(collection, @path, @variables, @relation)
end
- 3
def each_with_info
return super unless block_given?
@relation.preload_associations(@collection)
super
end
end
- 3
class MixedCollectionIterator < CollectionIterator # :nodoc:
- 3
def initialize(collection, paths)
super(collection)
@paths = paths
end
- 3
def each_with_info
return enum_for(:each_with_info) unless block_given?
@collection.each_with_index { |o, i| yield(o, @paths[i]) }
end
end
- 3
def render_collection_with_partial(collection, partial, context, block)
iter_vars = retrieve_variable(partial)
collection = if collection.respond_to?(:preload_associations)
PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
else
SameCollectionIterator.new(collection, partial, iter_vars)
end
template = find_template(partial, @locals.keys + iter_vars)
layout = if !block && (layout = @options[:layout])
find_template(layout.to_s, @locals.keys + iter_vars)
end
render_collection(collection, context, partial, template, layout, block)
end
- 3
def render_collection_derive_partial(collection, context, block)
paths = collection.map { |o| partial_path(o, context) }
if paths.uniq.length == 1
# Homogeneous
render_collection_with_partial(collection, paths.first, context, block)
else
if @options[:cached]
raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
end
paths.map! { |path| retrieve_variable(path).unshift(path) }
collection = MixedCollectionIterator.new(collection, paths)
render_collection(collection, context, nil, nil, nil, block)
end
end
- 3
private
- 3
def retrieve_variable(path)
variable = local_variable(path)
[variable, :"#{variable}_counter", :"#{variable}_iteration"]
end
- 3
def render_collection(collection, view, path, template, layout, block)
identifier = (template && template.identifier) || path
ActiveSupport::Notifications.instrument(
"render_collection.action_view",
identifier: identifier,
layout: layout && layout.virtual_path,
count: collection.size
) do |payload|
spacer = if @options.key?(:spacer_template)
spacer_template = find_template(@options[:spacer_template], @locals.keys)
build_rendered_template(spacer_template.render(view, @locals), spacer_template)
else
RenderedTemplate::EMPTY_SPACER
end
collection_body = if template
cache_collection_render(payload, view, template, collection) do |filtered_collection|
collection_with_template(view, template, layout, filtered_collection)
end
else
collection_with_template(view, nil, layout, collection)
end
return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
build_rendered_collection(collection_body, spacer)
end
end
- 3
def collection_with_template(view, template, layout, collection)
locals = @locals
cache = {}
partial_iteration = PartialIteration.new(collection.size)
collection.each_with_info.map do |object, (path, as, counter, iteration)|
index = partial_iteration.index
locals[as] = object
locals[counter] = index
locals[iteration] = partial_iteration
_template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
content = _template.render(view, locals)
content = layout.render(view, locals) { content } if layout
partial_iteration.iterate!
build_rendered_template(content, _template)
end
end
end
end
# frozen_string_literal: true
module ActionView
class ObjectRenderer < PartialRenderer # :nodoc:
include ObjectRendering
def initialize(lookup_context, options)
super
@object = nil
@local_name = nil
end
def render_object_with_partial(object, partial, context, block)
@object = object
@local_name = local_variable(partial)
render(partial, context, block)
end
def render_object_derive_partial(object, context, block)
path = partial_path(object, context)
render_object_with_partial(object, path, context, block)
end
private
def template_keys(path)
super + [@local_name]
end
def render_partial_template(view, locals, template, layout, block)
locals[@local_name || template.variable] = @object
super(view, locals, template, layout, block)
end
end
end
# frozen_string_literal: true
- 3
require "action_view/renderer/partial_renderer/collection_caching"
- 3
module ActionView
# = Action View Partials
#
# There's also a convenience method for rendering sub templates within the current controller that depends on a
# single object (we call this kind of sub templates for partials). It relies on the fact that partials should
# follow the naming convention of being prefixed with an underscore -- as to separate them from regular
# templates that could be rendered on their own.
#
# In a template for Advertiser#account:
#
# <%= render partial: "account" %>
#
# This would render "advertiser/_account.html.erb".
#
# In another template for Advertiser#buy, we could have:
#
# <%= render partial: "account", locals: { account: @buyer } %>
#
# <% @advertisements.each do |ad| %>
# <%= render partial: "ad", locals: { ad: ad } %>
# <% end %>
#
# This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
# render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
#
# == The :as and :object options
#
# By default ActionView::PartialRenderer doesn't have any local variables.
# The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
#
# <%= render partial: "account", object: @buyer %>
#
# would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
# equivalent to:
#
# <%= render partial: "account", locals: { account: @buyer } %>
#
# With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
# wanted it to be +user+ instead of +account+ we'd do:
#
# <%= render partial: "account", object: @buyer, as: 'user' %>
#
# This is equivalent to
#
# <%= render partial: "account", locals: { user: @buyer } %>
#
# == \Rendering a collection of partials
#
# The example of partial use describes a familiar pattern where a template needs to iterate over an array and
# render a sub template for each of the elements. This pattern has been implemented as a single method that
# accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
# example in "Using partials" can be rewritten with a single line:
#
# <%= render partial: "ad", collection: @advertisements %>
#
# This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
# iteration object will automatically be made available to the template with a name of the form
# +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
# the collection and the total size of the collection. The iteration object also has two convenience methods,
# +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
# For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
# +index+ method.
#
# The <tt>:as</tt> option may be used when rendering partials.
#
# You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
# The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
#
# <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
#
# If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you
# to specify a text which will be displayed instead by using this form:
#
# <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
#
# == \Rendering shared partials
#
# Two controllers can share a set of partials and render them like this:
#
# <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
#
# This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
#
# == \Rendering objects that respond to +to_partial_path+
#
# Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
# and pick the proper path by checking +to_partial_path+ method.
#
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
# # <%= render partial: "accounts/account", locals: { account: @account} %>
# <%= render partial: @account %>
#
# # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
# # that's why we can replace:
# # <%= render partial: "posts/post", collection: @posts %>
# <%= render partial: @posts %>
#
# == \Rendering the default case
#
# If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
# defaults of render to render partials. Examples:
#
# # Instead of <%= render partial: "account" %>
# <%= render "account" %>
#
# # Instead of <%= render partial: "account", locals: { account: @buyer } %>
# <%= render "account", account: @buyer %>
#
# # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
# # <%= render partial: "accounts/account", locals: { account: @account} %>
# <%= render @account %>
#
# # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
# # that's why we can replace:
# # <%= render partial: "posts/post", collection: @posts %>
# <%= render @posts %>
#
# == \Rendering partials with layouts
#
# Partials can have their own layouts applied to them. These layouts are different than the ones that are
# specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
# of users:
#
# <%# app/views/users/index.html.erb %>
# Here's the administrator:
# <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
#
# Here's the editor:
# <%= render partial: "user", layout: "editor", locals: { user: editor } %>
#
# <%# app/views/users/_user.html.erb %>
# Name: <%= user.name %>
#
# <%# app/views/users/_administrator.html.erb %>
# <div id="administrator">
# Budget: $<%= user.budget %>
# <%= yield %>
# </div>
#
# <%# app/views/users/_editor.html.erb %>
# <div id="editor">
# Deadline: <%= user.deadline %>
# <%= yield %>
# </div>
#
# ...this will return:
#
# Here's the administrator:
# <div id="administrator">
# Budget: $<%= user.budget %>
# Name: <%= user.name %>
# </div>
#
# Here's the editor:
# <div id="editor">
# Deadline: <%= user.deadline %>
# Name: <%= user.name %>
# </div>
#
# If a collection is given, the layout will be rendered once for each item in
# the collection. For example, these two snippets have the same output:
#
# <%# app/views/users/_user.html.erb %>
# Name: <%= user.name %>
#
# <%# app/views/users/index.html.erb %>
# <%# This does not use layouts %>
# <ul>
# <% users.each do |user| -%>
# <li>
# <%= render partial: "user", locals: { user: user } %>
# </li>
# <% end -%>
# </ul>
#
# <%# app/views/users/_li_layout.html.erb %>
# <li>
# <%= yield %>
# </li>
#
# <%# app/views/users/index.html.erb %>
# <ul>
# <%= render partial: "user", layout: "li_layout", collection: users %>
# </ul>
#
# Given two users whose names are Alice and Bob, these snippets return:
#
# <ul>
# <li>
# Name: Alice
# </li>
# <li>
# Name: Bob
# </li>
# </ul>
#
# The current object being rendered, as well as the object_counter, will be
# available as local variables inside the layout template under the same names
# as available in the partial.
#
# You can also apply a layout to a block within any template:
#
# <%# app/views/users/_chief.html.erb %>
# <%= render(layout: "administrator", locals: { user: chief }) do %>
# Title: <%= chief.title %>
# <% end %>
#
# ...this will return:
#
# <div id="administrator">
# Budget: $<%= user.budget %>
# Title: <%= chief.name %>
# </div>
#
# As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
#
# If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
# an array to layout and treat it as an enumerable.
#
# <%# app/views/users/_user.html.erb %>
# <div class="user">
# Budget: $<%= user.budget %>
# <%= yield user %>
# </div>
#
# <%# app/views/users/index.html.erb %>
# <%= render layout: @users do |user| %>
# Title: <%= user.title %>
# <% end %>
#
# This will render the layout for each user and yield to the block, passing the user, each time.
#
# You can also yield multiple times in one layout and use block arguments to differentiate the sections.
#
# <%# app/views/users/_user.html.erb %>
# <div class="user">
# <%= yield user, :header %>
# Budget: $<%= user.budget %>
# <%= yield user, :footer %>
# </div>
#
# <%# app/views/users/index.html.erb %>
# <%= render layout: @users do |user, section| %>
# <%- case section when :header -%>
# Title: <%= user.title %>
# <%- when :footer -%>
# Deadline: <%= user.deadline %>
# <%- end -%>
# <% end %>
- 3
class PartialRenderer < AbstractRenderer
- 3
include CollectionCaching
- 3
def initialize(lookup_context, options)
super(lookup_context)
@options = options
@locals = @options[:locals] || {}
@details = extract_details(@options)
end
- 3
def render(partial, context, block)
template = find_template(partial, template_keys(partial))
if !block && (layout = @options[:layout])
layout = find_template(layout.to_s, template_keys(partial))
end
render_partial_template(context, @locals, template, layout, block)
end
- 3
private
- 3
def template_keys(_)
@locals.keys
end
- 3
def render_partial_template(view, locals, template, layout, block)
ActiveSupport::Notifications.instrument(
"render_partial.action_view",
identifier: template.identifier,
layout: layout && layout.virtual_path
) do |payload|
content = template.render(view, locals, add_to_stack: !block) do |*name|
view._layout_for(*name, &block)
end
content = layout.render(view, locals) { content } if layout
payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
build_rendered_template(content, template)
end
end
- 3
def find_template(path, locals)
prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
@lookup_context.find_template(path, prefixes, true, locals, @details)
end
end
end
# frozen_string_literal: true
- 3
require "active_support/core_ext/enumerable"
- 3
module ActionView
- 3
module CollectionCaching # :nodoc:
- 3
extend ActiveSupport::Concern
- 3
included do
# Fallback cache store if Action View is used without Rails.
# Otherwise overridden in Railtie to use Rails.cache.
- 3
mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new
end
- 3
private
- 3
def will_cache?(options, view)
options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
end
- 3
def cache_collection_render(instrumentation_payload, view, template, collection)
return yield(collection) unless will_cache?(@options, view)
collection_iterator = collection
# Result is a hash with the key represents the
# key used for cache lookup and the value is the item
# on which the partial is being rendered
keyed_collection, ordered_keys = collection_by_cache_keys(view, template, collection)
# Pull all partials from cache
# Result is a hash, key matches the entry in
# `keyed_collection` where the cache was retrieved and the
# value is the value that was present in the cache
cached_partials = collection_cache.read_multi(*keyed_collection.keys)
instrumentation_payload[:cache_hits] = cached_partials.size
# Extract the items for the keys that are not found
collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
index = 0
keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
# This block is called once
# for every cache miss while preserving order.
rendered_partials[index].tap { index += 1 }
end
ordered_keys.map do |key|
keyed_partials[key]
end
end
- 3
def callable_cache_key?
@options[:cached].respond_to?(:call)
end
- 3
def collection_by_cache_keys(view, template, collection)
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
digest_path = view.digest_path_from_template(template)
collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
key = expanded_cache_key(seed.call(item), view, template, digest_path)
ordered_keys << key
hash[key] = item
end
end
- 3
def expanded_cache_key(key, view, template, digest_path)
key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path))
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
end
# `order_by` is an enumerable object containing keys of the cache,
# all keys are passed in whether found already or not.
#
# `cached_partials` is a hash. If the value exists
# it represents the rendered partial from the cache
# otherwise `Hash#fetch` will take the value of its block.
#
# This method expects a block that will return the rendered
# partial. An example is to render all results
# for each element that was not found in the cache and store it as an array.
# Order it so that the first empty cache element in `cached_partials`
# corresponds to the first element in `rendered_partials`.
#
# If the partial is not already cached it will also be
# written back to the underlying cache store.
- 3
def fetch_or_cache_partial(cached_partials, template, order_by:)
order_by.index_with do |cache_key|
if content = cached_partials[cache_key]
build_rendered_template(content, template)
else
yield.tap do |rendered_partial|
collection_cache.write(cache_key, rendered_partial.body)
end
end
end
end
end
end
# frozen_string_literal: true
module ActionView
# This is the main entry point for rendering. It basically delegates
# to other objects like TemplateRenderer and PartialRenderer which
# actually renders the template.
#
# The Renderer will parse the options from the +render+ or +render_body+
# method and render a partial or a template based on the options. The
# +TemplateRenderer+ and +PartialRenderer+ objects are wrappers which do all
# the setup and logic necessary to render a view and a new object is created
# each time +render+ is called.
class Renderer
attr_accessor :lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
end
# Main render entry point shared by Action View and Action Controller.
def render(context, options)
render_to_object(context, options).body
end
def render_to_object(context, options) # :nodoc:
if options.key?(:partial)
render_partial_to_object(context, options)
elsif options.key?(:object)
object = options[:object]
AbstractRenderer::RenderedTemplate.new(object.render_in(context), object)
else
render_template_to_object(context, options)
end
end
# Render but returns a valid Rack body. If fibers are defined, we return
# a streaming body that renders the template piece by piece.
#
# Note that partials are not supported to be rendered with streaming,
# so in such cases, we just wrap them in an array.
def render_body(context, options)
if options.key?(:partial)
[render_partial(context, options)]
else
StreamingTemplateRenderer.new(@lookup_context).render(context, options)
end
end
# Direct access to template rendering.
def render_template(context, options) #:nodoc:
render_template_to_object(context, options).body
end
# Direct access to partial rendering.
def render_partial(context, options, &block) #:nodoc:
render_partial_to_object(context, options, &block).body
end
def cache_hits # :nodoc:
@cache_hits ||= {}
end
def render_template_to_object(context, options) #:nodoc:
TemplateRenderer.new(@lookup_context).render(context, options)
end
def render_partial_to_object(context, options, &block) #:nodoc:
partial = options[:partial]
if String === partial
collection = collection_from_options(options)
if collection
# Collection + Partial
renderer = CollectionRenderer.new(@lookup_context, options)
renderer.render_collection_with_partial(collection, partial, context, block)
else
if options.key?(:object)
# Object + Partial
renderer = ObjectRenderer.new(@lookup_context, options)
renderer.render_object_with_partial(options[:object], partial, context, block)
else
# Partial
renderer = PartialRenderer.new(@lookup_context, options)
renderer.render(partial, context, block)
end
end
else
collection = collection_from_object(partial) || collection_from_options(options)
if collection
# Collection + Derived Partial
renderer = CollectionRenderer.new(@lookup_context, options)
renderer.render_collection_derive_partial(collection, context, block)
else
# Object + Derived Partial
renderer = ObjectRenderer.new(@lookup_context, options)
renderer.render_object_derive_partial(partial, context, block)
end
end
end
private
def collection_from_options(options)
if options.key?(:collection)
collection = options[:collection]
collection || []
end
end
def collection_from_object(object)
object if object.respond_to?(:to_ary)
end
end
end
# frozen_string_literal: true
require "fiber"
module ActionView
# == TODO
#
# * Support streaming from child templates, partials and so on.
# * Rack::Cache needs to support streaming bodies
class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
# A valid Rack::Body (i.e. it responds to each).
# It is initialized with a block that, when called, starts
# rendering the template.
class Body #:nodoc:
def initialize(&start)
@start = start
end
def each(&block)
begin
@start.call(block)
rescue Exception => exception
log_error(exception)
block.call ActionView::Base.streaming_completion_on_exception
end
self
end
private
# This is the same logging logic as in ShowExceptions middleware.
def log_error(exception)
logger = ActionView::Base.logger
return unless logger
message = +"\n#{exception.class} (#{exception.message}):\n"
message << exception.annotated_source_code.to_s if exception.respond_to?(:annotated_source_code)
message << " " << exception.backtrace.join("\n ")
logger.fatal("#{message}\n\n")
end
end
# For streaming, instead of rendering a given a template, we return a Body
# object that responds to each. This object is initialized with a block
# that knows how to render the template.
def render_template(view, template, layout_name = nil, locals = {}) #:nodoc:
return [super.body] unless layout_name && template.supports_streaming?
locals ||= {}
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
Body.new do |buffer|
delayed_render(buffer, template, layout, view, locals)
end
end
private
def delayed_render(buffer, template, layout, view, locals)
# Wrap the given buffer in the StreamingBuffer and pass it to the
# underlying template handler. Now, every time something is concatenated
# to the buffer, it is not appended to an array, but streamed straight
# to the client.
output = ActionView::StreamingBuffer.new(buffer)
yielder = lambda { |*name| view._layout_for(*name) }
ActiveSupport::Notifications.instrument(
"render_template.action_view",
identifier: template.identifier,
layout: layout && layout.virtual_path
) do
outer_config = I18n.config
fiber = Fiber.new do
I18n.config = outer_config
if layout
layout.render(view, locals, output, &yielder)
else
# If you don't have a layout, just render the thing
# and concatenate the final result. This is the same
# as a layout with just <%= yield %>
output.safe_concat view._layout_for
end
end
# Set the view flow to support streaming. It will be aware
# when to stop rendering the layout because it needs to search
# something in the template and vice-versa.
view.view_flow = StreamingFlow.new(view, fiber)
# Yo! Start the fiber!
fiber.resume
# If the fiber is still alive, it means we need something
# from the template, so start rendering it. If not, it means
# the layout exited without requiring anything from the template.
if fiber.alive?
content = template.render(view, locals, &yielder)
# Once rendering the template is done, sets its content in the :layout key.
view.view_flow.set(:layout, content)
# In case the layout continues yielding, we need to resume
# the fiber until all yields are handled.
fiber.resume while fiber.alive?
end
end
end
end
end
# frozen_string_literal: true
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
@details = extract_details(options)
template = determine_template(options)
prepend_formats(template.format)
render_template(context, template, options[:layout], options[:locals] || {})
end
private
# Determine the template to be rendered using the given options.
def determine_template(options)
keys = options.has_key?(:locals) ? options[:locals].keys : []
if options.key?(:body)
Template::Text.new(options[:body])
elsif options.key?(:plain)
Template::Text.new(options[:plain])
elsif options.key?(:html)
Template::HTML.new(options[:html], formats.first)
elsif options.key?(:file)
if File.exist?(options[:file])
Template::RawFile.new(options[:file])
else
ActiveSupport::Deprecation.warn "render file: should be given the absolute path to a file"
@lookup_context.with_fallbacks.find_template(options[:file], nil, false, keys, @details)
end
elsif options.key?(:inline)
handler = Template.handler_for_extension(options[:type] || "erb")
format = if handler.respond_to?(:default_format)
handler.default_format
else
@lookup_context.formats.first
end
Template::Inline.new(options[:inline], "inline template", handler, locals: keys, format: format)
elsif options.key?(:template)
if options[:template].respond_to?(:render)
options[:template]
else
@lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
end
else
raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
end
end
# Renders the given template. A string representing the layout can be
# supplied as well.
def render_template(view, template, layout_name, locals)
render_with_layout(view, template, layout_name, locals) do |layout|
ActiveSupport::Notifications.instrument(
"render_template.action_view",
identifier: template.identifier,
layout: layout && layout.virtual_path
) do
template.render(view, locals) { |*name| view._layout_for(*name) }
end
end
end
def render_with_layout(view, template, path, locals)
layout = path && find_layout(path, locals.keys, [formats.first])
body = if layout
ActiveSupport::Notifications.instrument("render_layout.action_view", identifier: layout.identifier) do
view.view_flow.set(:layout, yield(layout))
layout.render(view, locals) { |*name| view._layout_for(*name) }
end
else
yield
end
build_rendered_template(body, template)
end
# This is the method which actually finds the layout using details in the lookup
# context object. If no layout is found, it checks if at least a layout with
# the given name exists across all details before raising the error.
def find_layout(layout, keys, formats)
resolve_layout(layout, keys, formats)
end
def resolve_layout(layout, keys, formats)
details = @details.dup
details[:formats] = formats
case layout
when String
begin
if layout.start_with?("/")
ActiveSupport::Deprecation.warn "Rendering layouts from an absolute path is deprecated."
@lookup_context.with_fallbacks.find_template(layout, nil, false, [], details)
else
@lookup_context.find_template(layout, nil, false, [], details)
end
rescue ActionView::MissingTemplate
all_details = @details.merge(formats: @lookup_context.default_formats)
raise unless template_exists?(layout, nil, false, [], **all_details)
end
when Proc
resolve_layout(layout.call(@lookup_context, formats), keys, formats)
else
layout
end
end
end
end
# frozen_string_literal: true
- 9
require "action_view/view_paths"
- 9
module ActionView
# This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
# it will trigger the lookup_context and consequently expire the cache.
- 9
class I18nProxy < ::I18n::Config #:nodoc:
- 9
attr_reader :original_config, :lookup_context
- 9
def initialize(original_config, lookup_context)
original_config = original_config.original_config if original_config.respond_to?(:original_config)
@original_config, @lookup_context = original_config, lookup_context
end
- 9
def locale
@original_config.locale
end
- 9
def locale=(value)
@lookup_context.locale = value
end
end
- 9
module Rendering
- 9
extend ActiveSupport::Concern
- 9
include ActionView::ViewPaths
- 9
attr_reader :rendered_format
- 9
def initialize
@rendered_format = nil
super
end
# Overwrite process to set up I18n proxy.
- 9
def process(*) #:nodoc:
old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
super
ensure
I18n.config = old_config
end
- 9
module ClassMethods
- 9
def _routes
end
- 9
def _helpers
end
- 9
def build_view_context_class(klass, supports_path, routes, helpers)
Class.new(klass) do
if routes
include routes.url_helpers(supports_path)
include routes.mounted_helpers
end
if helpers
include helpers
end
end
end
- 9
def view_context_class
klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
@view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
if klass.changed?(@view_context_class)
@view_context_class = build_view_context_class(klass, supports_path?, _routes, _helpers)
end
@view_context_class
end
end
- 9
def view_context_class
self.class.view_context_class
end
# An instance of a view class. The default view class is ActionView::Base.
#
# The view class must have the following methods:
#
# * <tt>View.new(lookup_context, assigns, controller)</tt> — Create a new
# ActionView instance for a controller and we can also pass the arguments.
#
# * <tt>View#render(option)</tt> — Returns String with the rendered template.
#
# Override this method in a module to change the default behavior.
- 9
def view_context
view_context_class.new(lookup_context, view_assigns, self)
end
# Returns an object that is able to render templates.
- 9
def view_renderer # :nodoc:
# Lifespan: Per controller
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
- 9
def render_to_body(options = {})
_process_options(options)
_render_template(options)
end
- 9
private
# Find and render a template based on the options given.
- 9
def _render_template(options)
variant = options.delete(:variant)
assigns = options.delete(:assigns)
context = view_context
context.assign assigns if assigns
lookup_context.variants = variant if variant
rendered_template = context.in_rendering_context(options) do |renderer|
renderer.render_to_object(context, options)
end
rendered_format = rendered_template.format || lookup_context.formats.first
@rendered_format = Template::Types[rendered_format]
rendered_template.body
end
# Assign the rendered format to look up context.
- 9
def _process_format(format)
super
lookup_context.formats = [format.to_sym] if format.to_sym
end
# Normalize args by converting render "foo" to render :action => "foo" and
# render "foo/bar" to render :template => "foo/bar".
- 9
def _normalize_args(action = nil, options = {})
options = super(action, options)
case action
when NilClass
when Hash
options = action
when String, Symbol
action = action.to_s
key = action.include?(?/) ? :template : :action
options[key] = action
else
if action.respond_to?(:permitted?) && action.permitted?
options = action
elsif action.respond_to?(:render_in)
options[:object] = action
else
options[:partial] = action
end
end
options
end
# Normalize options.
- 9
def _normalize_options(options)
options = super(options)
if options[:partial] == true
options[:partial] = action_name
end
if (options.keys & [:partial, :file, :template]).empty?
options[:prefixes] ||= _prefixes
end
options[:template] ||= (options[:action] || action_name).to_s
options
end
end
end
# frozen_string_literal: true
- 9
require "action_dispatch/routing/polymorphic_routes"
- 9
module ActionView
- 9
module RoutingUrlFor
# Returns the URL for the set of +options+ provided. This takes the
# same options as +url_for+ in Action Controller (see the
# documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
# <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
# instead of the fully qualified URL like "http://example.com/controller/action".
#
# ==== Options
# * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
# * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
# is currently not recommended since it breaks caching.
# * <tt>:host</tt> - Overrides the default (current) host if provided.
# * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
# * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
# * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
#
# ==== Relying on named routes
#
# Passing a record (like an Active Record) instead of a hash as the options parameter will
# trigger the named route for that record. The lookup will happen on the name of the class. So passing a
# Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
# +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
#
# ==== Implicit Controller Namespacing
#
# Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one.
#
# ==== Examples
# <%= url_for(action: 'index') %>
# # => /blogs/
#
# <%= url_for(action: 'find', controller: 'books') %>
# # => /books/find
#
# <%= url_for(action: 'login', controller: 'members', only_path: false, protocol: 'https') %>
# # => https://www.example.com/members/login/
#
# <%= url_for(action: 'play', anchor: 'player') %>
# # => /messages/play/#player
#
# <%= url_for(action: 'jump', anchor: 'tax&ship') %>
# # => /testing/jump/#tax&ship
#
# <%= url_for(Workshop.new) %>
# # relies on Workshop answering a persisted? call (and in this case returning false)
# # => /workshops
#
# <%= url_for(@workshop) %>
# # calls @workshop.to_param which by default returns the id
# # => /workshops/5
#
# # to_param can be re-defined in a model to provide different URL names:
# # => /workshops/1-workshop-name
#
# <%= url_for("http://www.example.com") %>
# # => http://www.example.com
#
# <%= url_for(:back) %>
# # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
# # => http://www.example.com
#
# <%= url_for(:back) %>
# # if request.env["HTTP_REFERER"] is not set or is blank
# # => javascript:history.back()
#
# <%= url_for(action: 'index', controller: 'users') %>
# # Assuming an "admin" namespace
# # => /admin/users
#
# <%= url_for(action: 'index', controller: '/users') %>
# # Specify absolute path with beginning slash
# # => /users
- 9
def url_for(options = nil)
case options
when String
options
when nil
super(only_path: _generate_paths_by_default)
when Hash
options = options.symbolize_keys
ensure_only_path_option(options)
super(options)
when ActionController::Parameters
ensure_only_path_option(options)
super(options)
when :back
_back_url
when Array
components = options.dup
options = components.extract_options!
ensure_only_path_option(options)
if options[:only_path]
polymorphic_path(components, options)
else
polymorphic_url(components, options)
end
else
method = _generate_paths_by_default ? :path : :url
builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method)
case options
when Symbol
builder.handle_string_call(self, options)
when Class
builder.handle_class_call(self, options)
else
builder.handle_model_call(self, options)
end
end
end
- 9
def url_options #:nodoc:
return super unless controller.respond_to?(:url_options)
controller.url_options
end
- 9
private
- 9
def _routes_context
controller
end
- 9
def optimize_routes_generation?
controller.respond_to?(:optimize_routes_generation?, true) ?
controller.optimize_routes_generation? : super
end
- 9
def _generate_paths_by_default
true
end
- 9
def ensure_only_path_option(options)
unless options.key?(:only_path)
options[:only_path] = _generate_paths_by_default unless options[:host]
end
end
end
end
# frozen_string_literal: true
- 9
require "thread"
- 9
require "delegate"
- 9
module ActionView
# = Action View Template
- 9
class Template
- 9
extend ActiveSupport::Autoload
- 9
def self.finalize_compiled_template_methods
ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods is deprecated and has no effect"
end
- 9
def self.finalize_compiled_template_methods=(_)
ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods= is deprecated and has no effect"
end
# === Encodings in ActionView::Template
#
# ActionView::Template is one of a few sources of potential
# encoding issues in Rails. This is because the source for
# templates are usually read from disk, and Ruby (like most
# encoding-aware programming languages) assumes that the
# String retrieved through File IO is encoded in the
# <tt>default_external</tt> encoding. In Rails, the default
# <tt>default_external</tt> encoding is UTF-8.
#
# As a result, if a user saves their template as ISO-8859-1
# (for instance, using a non-Unicode-aware text editor),
# and uses characters outside of the ASCII range, their
# users will see diamonds with question marks in them in
# the browser.
#
# For the rest of this documentation, when we say "UTF-8",
# we mean "UTF-8 or whatever the default_internal encoding
# is set to". By default, it will be UTF-8.
#
# To mitigate this problem, we use a few strategies:
# 1. If the source is not valid UTF-8, we raise an exception
# when the template is compiled to alert the user
# to the problem.
# 2. The user can specify the encoding using Ruby-style
# encoding comments in any template engine. If such
# a comment is supplied, Rails will apply that encoding
# to the resulting compiled source returned by the
# template handler.
# 3. In all cases, we transcode the resulting String to
# the UTF-8.
#
# This means that other parts of Rails can always assume
# that templates are encoded in UTF-8, even if the original
# source of the template was not UTF-8.
#
# From a user's perspective, the easiest thing to do is
# to save your templates as UTF-8. If you do this, you
# do not need to do anything else for things to "just work".
#
# === Instructions for template handlers
#
# The easiest thing for you to do is to simply ignore
# encodings. Rails will hand you the template source
# as the default_internal (generally UTF-8), raising
# an exception for the user before sending the template
# to you if it could not determine the original encoding.
#
# For the greatest simplicity, you can support only
# UTF-8 as the <tt>default_internal</tt>. This means
# that from the perspective of your handler, the
# entire pipeline is just UTF-8.
#
# === Advanced: Handlers with alternate metadata sources
#
# If you want to provide an alternate mechanism for
# specifying encodings (like ERB does via <%# encoding: ... %>),
# you may indicate that you will handle encodings yourself
# by implementing <tt>handles_encoding?</tt> on your handler.
#
# If you do, Rails will not try to encode the String
# into the default_internal, passing you the unaltered
# bytes tagged with the assumed encoding (from
# default_external).
#
# In this case, make sure you return a String from
# your handler encoded in the default_internal. Since
# you are handling out-of-band metadata, you are
# also responsible for alerting the user to any
# problems with converting the user's data to
# the <tt>default_internal</tt>.
#
# To do so, simply raise +WrongEncodingError+ as follows:
#
# raise WrongEncodingError.new(
# problematic_string,
# expected_encoding
# )
##
# :method: local_assigns
#
# Returns a hash with the defined local variables.
#
# Given this sub template rendering:
#
# <%= render "shared/header", { headline: "Welcome", person: person } %>
#
# You can use +local_assigns+ in the sub templates to access the local variables:
#
# local_assigns[:headline] # => "Welcome"
- 9
eager_autoload do
- 9
autoload :Error
- 9
autoload :RawFile
- 9
autoload :Handlers
- 9
autoload :HTML
- 9
autoload :Inline
- 9
autoload :Sources
- 9
autoload :Text
- 9
autoload :Types
end
- 9
extend Template::Handlers
- 9
attr_reader :identifier, :handler, :original_encoding, :updated_at
- 9
attr_reader :variable, :format, :variant, :locals, :virtual_path
- 9
def initialize(source, identifier, handler, format: nil, variant: nil, locals: nil, virtual_path: nil, updated_at: nil)
unless locals
ActiveSupport::Deprecation.warn "ActionView::Template#initialize requires a locals parameter"
locals = []
end
@source = source
@identifier = identifier
@handler = handler
@compiled = false
@locals = locals
@virtual_path = virtual_path
@variable = if @virtual_path
base = @virtual_path.end_with?("/") ? "" : ::File.basename(@virtual_path)
base =~ /\A_?(.*?)(?:\.\w+)*\z/
$1.to_sym
end
if updated_at
ActiveSupport::Deprecation.warn "ActionView::Template#updated_at is deprecated"
@updated_at = updated_at
else
@updated_at = Time.now
end
@format = format
@variant = variant
@compile_mutex = Mutex.new
end
- 9
deprecate :original_encoding
- 9
deprecate :updated_at
- 9
deprecate def virtual_path=(_); end
- 9
deprecate def locals=(_); end
- 9
deprecate def formats=(_); end
- 9
deprecate def formats; Array(format); end
- 9
deprecate def variants=(_); end
- 9
deprecate def variants; [variant]; end
- 9
deprecate def refresh(_); self; end
# Returns whether the underlying handler supports streaming. If so,
# a streaming buffer *may* be passed when it starts rendering.
- 9
def supports_streaming?
handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
end
# Render a template. If the template was not compiled yet, it is done
# exactly before rendering.
#
# This method is instrumented as "!render_template.action_view". Notice that
# we use a bang in this instrumentation because you don't want to
# consume this in production. This is only slow if it's being listened to.
- 9
def render(view, locals, buffer = ActionView::OutputBuffer.new, add_to_stack: true, &block)
instrument_render_template do
compile!(view)
view._run(method_name, self, locals, buffer, add_to_stack: add_to_stack, &block)
end
rescue => e
handle_render_error(view, e)
end
- 9
def type
@type ||= Types[format]
end
- 9
def short_identifier
@short_identifier ||= defined?(Rails.root) ? identifier.delete_prefix("#{Rails.root}/") : identifier
end
- 9
def inspect
"#<#{self.class.name} #{short_identifier} locals=#{@locals.inspect}>"
end
- 9
def source
@source.to_s
end
# This method is responsible for properly setting the encoding of the
# source. Until this point, we assume that the source is BINARY data.
# If no additional information is supplied, we assume the encoding is
# the same as <tt>Encoding.default_external</tt>.
#
# The user can also specify the encoding via a comment on the first
# line of the template (# encoding: NAME-OF-ENCODING). This will work
# with any template engine, as we process out the encoding comment
# before passing the source on to the template engine, leaving a
# blank line in its stead.
- 9
def encode!
source = self.source
return source unless source.encoding == Encoding::BINARY
# Look for # encoding: *. If we find one, we'll encode the
# String in that encoding, otherwise, we'll use the
# default external encoding.
if source.sub!(/\A#{ENCODING_FLAG}/, "")
encoding = magic_encoding = $1
else
encoding = Encoding.default_external
end
# Tag the source with the default external encoding
# or the encoding specified in the file
source.force_encoding(encoding)
# If the user didn't specify an encoding, and the handler
# handles encodings, we simply pass the String as is to
# the handler (with the default_external tag)
if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding?
source
# Otherwise, if the String is valid in the encoding,
# encode immediately to default_internal. This means
# that if a handler doesn't handle encodings, it will
# always get Strings in the default_internal
elsif source.valid_encoding?
source.encode!
# Otherwise, since the String is invalid in the encoding
# specified, raise an exception
else
raise WrongEncodingError.new(source, encoding)
end
end
# Exceptions are marshalled when using the parallel test runner with DRb, so we need
# to ensure that references to the template object can be marshalled as well. This means forgoing
# the marshalling of the compiler mutex and instantiating that again on unmarshalling.
- 9
def marshal_dump # :nodoc:
[ @source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant ]
end
- 9
def marshal_load(array) # :nodoc:
@source, @identifier, @handler, @compiled, @locals, @virtual_path, @updated_at, @format, @variant = *array
@compile_mutex = Mutex.new
end
- 9
private
# Compile a template. This method ensures a template is compiled
# just once and removes the source after it is compiled.
- 9
def compile!(view)
return if @compiled
# Templates can be used concurrently in threaded environments
# so compilation and any instance variable modification must
# be synchronized
@compile_mutex.synchronize do
# Any thread holding this lock will be compiling the template needed
# by the threads waiting. So re-check the @compiled flag to avoid
# re-compilation
return if @compiled
mod = view.compiled_method_container
instrument("!compile_template") do
compile(mod)
end
@compiled = true
end
end
- 9
class LegacyTemplate < DelegateClass(Template) # :nodoc:
- 9
attr_reader :source
- 9
def initialize(template, source)
super(template)
@source = source
end
end
# Among other things, this method is responsible for properly setting
# the encoding of the compiled template.
#
# If the template engine handles encodings, we send the encoded
# String to the engine without further processing. This allows
# the template engine to support additional mechanisms for
# specifying the encoding. For instance, ERB supports <%# encoding: %>
#
# Otherwise, after we figure out the correct encoding, we then
# encode the source into <tt>Encoding.default_internal</tt>.
# In general, this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
- 9
def compile(mod)
source = encode!
code = @handler.call(self, source)
# Make sure that the resulting String to be eval'd is in the
# encoding of the code
original_source = source
source = +<<-end_src
def #{method_name}(local_assigns, output_buffer)
@virtual_path = #{@virtual_path.inspect};#{locals_code};#{code}
end
end_src
# Make sure the source is in the encoding of the returned code
source.force_encoding(code.encoding)
# In case we get back a String from a handler that is not in
# BINARY or the default_internal, encode it to the default_internal
source.encode!
# Now, validate that the source we got back from the template
# handler is valid in the default_internal. This is for handlers
# that handle encoding but screw up
unless source.valid_encoding?
raise WrongEncodingError.new(source, Encoding.default_internal)
end
start_line = @handler.respond_to?(:start_line) ? @handler.start_line(self) : 0
begin
mod.module_eval(source, identifier, start_line)
rescue SyntaxError
# Account for when code in the template is not syntactically valid; e.g. if we're using
# ERB and the user writes <%= foo( %>, attempting to call a helper `foo` and interpolate
# the result into the template, but missing an end parenthesis.
raise SyntaxErrorInTemplate.new(self, original_source)
end
end
- 9
def handle_render_error(view, e)
if e.is_a?(Template::Error)
e.sub_template_of(self)
raise e
else
raise Template::Error.new(self)
end
end
- 9
def locals_code
# Only locals with valid variable names get set directly. Others will
# still be available in local_assigns.
locals = @locals - Module::RUBY_RESERVED_KEYWORDS
locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/)
# Assign for the same variable is to suppress unused variable warning
locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" }
end
- 9
def method_name
@method_name ||= begin
m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
m.tr!("-", "_")
m
end
end
- 9
def identifier_method_name
short_identifier.tr("^a-z_", "_")
end
- 9
def instrument(action, &block) # :doc:
ActiveSupport::Notifications.instrument("#{action}.action_view", instrument_payload, &block)
end
- 9
def instrument_render_template(&block)
ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block)
end
- 9
def instrument_payload
{ virtual_path: @virtual_path, identifier: @identifier }
end
end
end
# frozen_string_literal: true
require "active_support/core_ext/enumerable"
module ActionView
# = Action View Errors
class ActionViewError < StandardError #:nodoc:
end
class EncodingError < StandardError #:nodoc:
end
class WrongEncodingError < EncodingError #:nodoc:
def initialize(string, encoding)
@string, @encoding = string, encoding
end
def message
@string.force_encoding(Encoding::ASCII_8BIT)
"Your template was not saved as valid #{@encoding}. Please " \
"either specify #{@encoding} as the encoding for your template " \
"in your text editor, or mark the template with its " \
"encoding by inserting the following as the first line " \
"of the template:\n\n# encoding: <name of correct encoding>.\n\n" \
"The source of your template was:\n\n#{@string}"
end
end
class MissingTemplate < ActionViewError #:nodoc:
attr_reader :path
def initialize(paths, path, prefixes, partial, details, *)
@path = path
prefixes = Array(prefixes)
template_type = if partial
"partial"
elsif /layouts/i.match?(path)
"layout"
else
"template"
end
if partial && path.present?
path = path.sub(%r{([^/]+)$}, "_\\1")
end
searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join
super out
end
end
class Template
# The Template::Error exception is raised when the compilation or rendering of the template
# fails. This exception then gathers a bunch of intimate details and uses it to report a
# precise exception message.
class Error < ActionViewError #:nodoc:
SOURCE_CODE_RADIUS = 3
# Override to prevent #cause resetting during re-raise.
attr_reader :cause
def initialize(template)
super($!.message)
set_backtrace($!.backtrace)
@cause = $!
@template, @sub_templates = template, nil
end
def file_name
@template.identifier
end
def sub_template_message
if @sub_templates
"Trace of template inclusion: " +
@sub_templates.collect(&:inspect).join(", ")
else
""
end
end
def source_extract(indentation = 0)
return [] unless num = line_number
num = num.to_i
source_code = @template.encode!.split("\n")
start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
indent = end_on_line.to_s.size + indentation
return [] unless source_code = source_code[start_on_line..end_on_line]
formatted_code_for(source_code, start_on_line, indent)
end
def sub_template_of(template_path)
@sub_templates ||= []
@sub_templates << template_path
end
def line_number
@line_number ||=
if file_name
regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
$1 if message =~ regexp || backtrace.find { |line| line =~ regexp }
end
end
def annotated_source_code
source_extract(4)
end
private
def source_location
if line_number
"on line ##{line_number} of "
else
"in "
end + file_name
end
def formatted_code_for(source_code, line_counter, indent)
indent_template = "%#{indent}s: %s"
source_code.map do |line|
line_counter += 1
indent_template % [line_counter, line]
end
end
end
end
TemplateError = Template::Error
class SyntaxErrorInTemplate < TemplateError #:nodoc
def initialize(template, offending_code_string)
@offending_code_string = offending_code_string
super(template)
end
def message
<<~MESSAGE
Encountered a syntax error while rendering template: check #{@offending_code_string}
MESSAGE
end
def annotated_source_code
@offending_code_string.split("\n").map.with_index(1) { |line, index|
indentation = " " * 4
"#{index}:#{indentation}#{line}"
}
end
end
end
# frozen_string_literal: true
- 9
module ActionView #:nodoc:
# = Action View Template Handlers
- 9
class Template #:nodoc:
- 9
module Handlers #:nodoc:
- 9
autoload :Raw, "action_view/template/handlers/raw"
- 9
autoload :ERB, "action_view/template/handlers/erb"
- 9
autoload :Html, "action_view/template/handlers/html"
- 9
autoload :Builder, "action_view/template/handlers/builder"
- 9
def self.extended(base)
- 9
base.register_default_template_handler :raw, Raw.new
- 9
base.register_template_handler :erb, ERB.new
- 9
base.register_template_handler :html, Html.new
- 9
base.register_template_handler :builder, Builder.new
- 9
base.register_template_handler :ruby, lambda { |_, source| source }
end
- 9
@@template_handlers = {}
- 9
@@default_template_handlers = nil
- 9
def self.extensions
@@template_extensions ||= @@template_handlers.keys
end
- 9
class LegacyHandlerWrapper < SimpleDelegator # :nodoc:
- 9
def call(view, source)
__getobj__.call(ActionView::Template::LegacyTemplate.new(view, source))
end
end
# Register an object that knows how to handle template files with the given
# extensions. This can be used to implement new template types.
# The handler must respond to +:call+, which will be passed the template
# and should return the rendered template as a String.
- 9
def register_template_handler(*extensions, handler)
- 45
params = if handler.is_a?(Proc)
- 9
handler.parameters
else
- 36
handler.method(:call).parameters
end
- 135
unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
ActiveSupport::Deprecation.warn <<~eowarn
Single arity template handlers are deprecated. Template handlers must
now accept two parameters, the view object and the source for the view object.
Change:
>> #{handler}.call(#{params.map(&:last).join(", ")})
To:
>> #{handler}.call(#{params.map(&:last).join(", ")}, source)
eowarn
handler = LegacyHandlerWrapper.new(handler)
end
- 45
raise(ArgumentError, "Extension is required") if extensions.empty?
- 45
extensions.each do |extension|
- 45
@@template_handlers[extension.to_sym] = handler
end
- 45
@@template_extensions = nil
end
# Opposite to register_template_handler.
- 9
def unregister_template_handler(*extensions)
extensions.each do |extension|
handler = @@template_handlers.delete extension.to_sym
@@default_template_handlers = nil if @@default_template_handlers == handler
end
@@template_extensions = nil
end
- 9
def template_handler_extensions
@@template_handlers.keys.map(&:to_s).sort
end
- 9
def registered_template_handler(extension)
- 3
extension && @@template_handlers[extension.to_sym]
end
- 9
def register_default_template_handler(extension, klass)
- 9
register_template_handler(extension, klass)
- 9
@@default_template_handlers = klass
end
- 9
def handler_for_extension(extension)
- 3
registered_template_handler(extension) || @@default_template_handlers
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
- 9
module Template::Handlers
- 9
class Builder
- 9
class_attribute :default_format, default: :xml
- 9
def call(template, source)
require_engine
"xml = ::Builder::XmlMarkup.new(:indent => 2);" \
"self.output_buffer = xml.target!;" +
source +
";xml.target!;"
end
- 9
private
- 9
def require_engine # :doc:
@required ||= begin
require "builder"
true
end
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
- 9
class Template
- 9
module Handlers
- 9
class ERB
- 9
autoload :Erubi, "action_view/template/handlers/erb/erubi"
# Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERB documentation for suitable values.
- 9
class_attribute :erb_trim_mode, default: "-"
# Default implementation used.
- 9
class_attribute :erb_implementation, default: Erubi
# Do not escape templates of these mime types.
- 9
class_attribute :escape_ignore_list, default: ["text/plain"]
- 9
[self, singleton_class].each do |base|
- 18
base.alias_method :escape_whitelist, :escape_ignore_list
- 18
base.alias_method :escape_whitelist=, :escape_ignore_list=
- 18
base.deprecate(
escape_whitelist: "use #escape_ignore_list instead",
:escape_whitelist= => "use #escape_ignore_list= instead"
)
end
- 9
ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
- 9
def self.call(template, source)
new.call(template, source)
end
- 9
def supports_streaming?
true
end
- 9
def handles_encoding?
true
end
# Line number to pass to #module_eval
#
# If we're annotating the template, we need to offset the starting
# line number passed to #module_eval so that errors in the template
# will be raised on the correct line.
- 9
def start_line(template)
annotate?(template) ? -1 : 0
end
- 9
def call(template, source)
# First, convert to BINARY, so in case the encoding is
# wrong, we can still find an encoding tag
# (<%# encoding %>) inside the String using a regular
# expression
template_source = source.b
erb = template_source.gsub(ENCODING_TAG, "")
encoding = $2
erb.force_encoding valid_encoding(source.dup, encoding)
# Always make sure we return a String in the default_internal
erb.encode!
options = {
escape: (self.class.escape_ignore_list.include? template.type),
trim: (self.class.erb_trim_mode == "-")
}
if annotate?(template)
options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier} -->\n';"
options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->\n';@output_buffer.to_s"
end
self.class.erb_implementation.new(erb, options).src
end
- 9
private
- 9
def annotate?(template)
ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
end
- 9
def valid_encoding(string, encoding)
# If a magic encoding comment was found, tag the
# String with this encoding. This is for a case
# where the original String was assumed to be,
# for instance, UTF-8, but a magic comment
# proved otherwise
string.force_encoding(encoding) if encoding
# If the String is valid, return the encoding we found
return string.encoding if string.valid_encoding?
# Otherwise, raise an exception
raise WrongEncodingError.new(string, string.encoding)
end
end
end
end
end
# frozen_string_literal: true
- 9
require "erubi"
- 9
module ActionView
- 9
class Template
- 9
module Handlers
- 9
class ERB
- 9
class Erubi < ::Erubi::Engine
# :nodoc: all
- 9
def initialize(input, properties = {})
@newline_pending = 0
# Dup properties so that we don't modify argument
properties = Hash[properties]
properties[:bufvar] ||= "@output_buffer"
properties[:preamble] ||= ""
properties[:postamble] ||= "#{properties[:bufvar]}.to_s"
properties[:escapefunc] = ""
super
end
- 9
def evaluate(action_view_erb_handler_context)
src = @src
view = Class.new(ActionView::Base) {
include action_view_erb_handler_context._routes.url_helpers
class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
}.empty
view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
end
- 9
private
- 9
def add_text(text)
return if text.empty?
if text == "\n"
@newline_pending += 1
else
src << bufvar << ".safe_append='"
src << "\n" * @newline_pending if @newline_pending > 0
src << text.gsub(/['\\]/, '\\\\\&')
src << "'.freeze;"
@newline_pending = 0
end
end
- 9
BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
- 9
def add_expression(indicator, code)
flush_newline_if_pending(src)
if (indicator == "==") || @escape
src << bufvar << ".safe_expr_append="
else
src << bufvar << ".append="
end
if BLOCK_EXPR.match?(code)
src << " " << code
else
src << "(" << code << ");"
end
end
- 9
def add_code(code)
flush_newline_if_pending(src)
super
end
- 9
def add_postamble(_)
flush_newline_if_pending(src)
super
end
- 9
def flush_newline_if_pending(src)
if @newline_pending > 0
src << bufvar << ".safe_append='#{"\n" * @newline_pending}'.freeze;"
@newline_pending = 0
end
end
end
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
- 9
module Template::Handlers
- 9
class Html < Raw
- 9
def call(template, source)
"ActionView::OutputBuffer.new #{super}"
end
end
end
end
# frozen_string_literal: true
- 9
module ActionView
- 9
module Template::Handlers
- 9
class Raw
- 9
def call(template, source)
"#{source.inspect}.html_safe;"
end
end
end
end
# frozen_string_literal: true
module ActionView #:nodoc:
# = Action View HTML Template
class Template #:nodoc:
class HTML #:nodoc:
attr_reader :type
def initialize(string, type = nil)
unless type
ActiveSupport::Deprecation.warn "ActionView::Template::HTML#initialize requires a type parameter"
type = :html
end
@string = string.to_s
@type = type
end
def identifier
"html template"
end
alias_method :inspect, :identifier
def to_str
ERB::Util.h(@string)
end
def render(*args)
to_str
end
def format
@type
end
def formats; Array(format); end
deprecate :formats
end
end
end
# frozen_string_literal: true
module ActionView #:nodoc:
class Template #:nodoc:
class Inline < Template #:nodoc:
# This finalizer is needed (and exactly with a proc inside another proc)
# otherwise templates leak in development.
Finalizer = proc do |method_name, mod| # :nodoc:
proc do
mod.module_eval do
remove_possible_method method_name
end
end
end
def compile(mod)
super
ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
end
end
end
end
# frozen_string_literal: true
module ActionView #:nodoc:
# = Action View RawFile Template
class Template #:nodoc:
class RawFile #:nodoc:
attr_accessor :type, :format
def initialize(filename)
@filename = filename.to_s
extname = ::File.extname(filename).delete(".")
@type = Template::Types[extname] || Template::Types[:text]
@format = @type.symbol
end
def identifier
@filename
end
def render(*args)
::File.read(@filename)
end
def formats; Array(format); end
deprecate :formats
end
end
end
# frozen_string_literal: true
- 9
require "pathname"
- 9
require "active_support/core_ext/class"
- 9
require "active_support/core_ext/module/attribute_accessors"
- 9
require "action_view/template"
- 9
require "thread"
- 9
require "concurrent/map"
- 9
module ActionView
# = Action View Resolver
- 9
class Resolver
# Keeps all information about view path and builds virtual path.
- 9
class Path
- 9
attr_reader :name, :prefix, :partial, :virtual
- 9
alias_method :partial?, :partial
- 9
def self.build(name, prefix, partial)
virtual = +""
virtual << "#{prefix}/" unless prefix.empty?
virtual << (partial ? "_#{name}" : name)
new name, prefix, partial, virtual
end
- 9
def initialize(name, prefix, partial, virtual)
@name = name
@prefix = prefix
@partial = partial
@virtual = virtual
end
- 9
def to_str
@virtual
end
- 9
alias :to_s :to_str
end
- 9
class PathParser # :nodoc:
- 9
def build_path_regex
handlers = Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
formats = Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|")
locales = "[a-z]{2}(?:-[A-Z]{2})?"
variants = "[^.]*"
%r{
\A
(?:(?<prefix>.*)/)?
(?<partial>_)?
(?<action>.*?)
(?:\.(?<locale>#{locales}))??
(?:\.(?<format>#{formats}))??
(?:\+(?<variant>#{variants}))??
(?:\.(?<handler>#{handlers}))?
\z
}x
end
- 9
def parse(path)
@regex ||= build_path_regex
match = @regex.match(path)
{
prefix: match[:prefix] || "",
action: match[:action],
partial: !!match[:partial],
locale: match[:locale]&.to_sym,
handler: match[:handler]&.to_sym,
format: match[:format]&.to_sym,
variant: match[:variant]
}
end
end
# Threadsafe template cache
- 9
class Cache #:nodoc:
- 9
class SmallCache < Concurrent::Map
- 9
def initialize(options = {})
- 66
super(options.merge(initial_capacity: 2))
end
end
# Preallocate all the default blocks for performance/memory consumption reasons
- 9
PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
- 9
PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
- 9
NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
- 9
KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) }
# Usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
- 9
NO_TEMPLATES = [].freeze
- 9
def initialize
- 33
@data = SmallCache.new(&KEY_BLOCK)
- 33
@query_cache = SmallCache.new
end
- 9
def inspect
"#{to_s[0..-2]} keys=#{@data.size} queries=#{@query_cache.size}>"
end
# Cache the templates returned by the block
- 9
def cache(key, name, prefix, partial, locals)
@data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
end
- 9
def cache_query(query) # :nodoc:
@query_cache[query] ||= canonical_no_templates(yield)
end
- 9
def clear
- 9
@data.clear
- 9
@query_cache.clear
end
# Get the cache size. Do not call this
# method. This method is not guaranteed to be here ever.
- 9
def size # :nodoc:
size = 0
@data.each_value do |v1|
v1.each_value do |v2|
v2.each_value do |v3|
v3.each_value do |v4|
size += v4.size
end
end
end
end
size + @query_cache.size
end
- 9
private
- 9
def canonical_no_templates(templates)
templates.empty? ? NO_TEMPLATES : templates
end
end
- 9
cattr_accessor :caching, default: true
- 9
class << self
- 9
alias :caching? :caching
end
- 9
def initialize
- 33
@cache = Cache.new
end
- 9
def clear_cache
- 9
@cache.clear
end
# Normalizes the arguments and passes it on to find_templates.
- 9
def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
locals = locals.map(&:to_s).sort!.freeze
cached(key, [name, prefix, partial], details, locals) do
_find_all(name, prefix, partial, details, key, locals)
end
end
- 9
alias :find_all_anywhere :find_all
- 9
deprecate :find_all_anywhere
- 9
def find_all_with_query(query) # :nodoc:
@cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
end
- 9
private
- 9
def _find_all(name, prefix, partial, details, key, locals)
find_templates(name, prefix, partial, details, locals)
end
- 9
delegate :caching?, to: :class
# This is what child classes implement. No defaults are needed
# because Resolver guarantees that the arguments are present and
# normalized.
- 9
def find_templates(name, prefix, partial, details, locals = [])
raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
end
# Handles templates caching. If a key is given and caching is on
# always check the cache before hitting the resolver. Otherwise,
# it always hits the resolver but if the key is present, check if the
# resolver is fresher before returning it.
- 9
def cached(key, path_info, details, locals)
name, prefix, partial = path_info
if key
@cache.cache(key, name, prefix, partial, locals) do
yield
end
else
yield
end
end
end
# An abstract class that implements a Resolver with path semantics.
- 9
class PathResolver < Resolver #:nodoc:
- 9
EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
- 9
DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
- 9
def initialize(pattern = nil)
- 33
if pattern
ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
@pattern = pattern
else
- 33
@pattern = DEFAULT_PATTERN
end
- 33
@unbound_templates = Concurrent::Map.new
- 33
@path_parser = PathParser.new
- 33
super()
end
- 9
def clear_cache
- 9
@unbound_templates.clear
- 9
@path_parser = PathParser.new
- 9
super()
end
- 9
private
- 9
def _find_all(name, prefix, partial, details, key, locals)
path = Path.build(name, prefix, partial)
query(path, details, details[:formats], locals, cache: !!key)
end
- 9
def query(path, details, formats, locals, cache:)
template_paths = find_template_paths_from_details(path, details)
template_paths = reject_files_external_to_app(template_paths)
template_paths.map do |template|
unbound_template =
if cache
@unbound_templates.compute_if_absent([template, path.virtual]) do
build_unbound_template(template, path.virtual)
end
else
build_unbound_template(template, path.virtual)
end
unbound_template.bind_locals(locals)
end
end
- 9
def source_for_template(template)
Template::Sources::File.new(template)
end
- 9
def build_unbound_template(template, virtual_path)
handler, format, variant = extract_handler_and_format_and_variant(template)
source = source_for_template(template)
UnboundTemplate.new(
source,
template,
handler,
virtual_path: virtual_path,
format: format,
variant: variant,
)
end
- 9
def reject_files_external_to_app(files)
files.reject { |filename| !inside_path?(@path, filename) }
end
- 9
def find_template_paths_from_details(path, details)
if path.name.include?(".")
ActiveSupport::Deprecation.warn("Rendering actions with '.' in the name is deprecated: #{path}")
end
query = build_query(path, details)
find_template_paths(query)
end
- 9
def find_template_paths(query)
Dir[query].uniq.reject do |filename|
File.directory?(filename) ||
# deals with case-insensitive file systems.
!File.fnmatch(query, filename, File::FNM_EXTGLOB)
end
end
- 9
def inside_path?(path, filename)
filename = File.expand_path(filename)
path = File.join(path, "")
filename.start_with?(path)
end
# Helper for building query glob string based on resolver's pattern.
- 9
def build_query(path, details)
query = @pattern.dup
prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
query.gsub!(/:prefix(\/)?/, prefix)
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
query.gsub!(":action", partial)
details.each do |ext, candidates|
if ext == :variants && candidates == :any
query.gsub!(/:#{ext}/, "*")
else
query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
end
end
File.expand_path(query, @path)
end
- 9
def escape_entry(entry)
entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
end
# Extract handler, formats and variant from path. If a format cannot be found neither
# from the path, or the handler, we should return the array of formats given
# to the resolver.
- 9
def extract_handler_and_format_and_variant(path)
details = @path_parser.parse(path)
handler = Template.handler_for_extension(details[:handler])
format = details[:format] || handler.try(:default_format)
variant = details[:variant]
# Template::Types[format] and handler.default_format can return nil
[handler, format, variant]
end
end
# A resolver that loads files from the filesystem.
- 9
class FileSystemResolver < PathResolver
- 9
attr_reader :path
- 9
def initialize(path, pattern = nil)
- 33
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
- 33
super(pattern)
- 33
@path = File.expand_path(path)
end
- 9
def to_s
@path.to_s
end
- 9
alias :to_path :to_s
- 9
def eql?(resolver)
self.class.equal?(resolver.class) && to_path == resolver.to_path
end
- 9
alias :== :eql?
end
# An Optimized resolver for Rails' most common case.
- 9
class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
- 9
def initialize(path)
- 27
super(path)
end
- 9
private
- 9
def find_candidate_template_paths(path)
# Instead of checking for every possible path, as our other globs would
# do, scan the directory for files with the right prefix.
query = "#{escape_entry(File.join(@path, path))}*"
Dir[query].reject do |filename|
File.directory?(filename)
end
end
- 9
def find_template_paths_from_details(path, details)
if path.name.include?(".")
# Fall back to the unoptimized resolver, which will warn
return super
end
candidates = find_candidate_template_paths(path)
regex = build_regex(path, details)
candidates.uniq.reject do |filename|
# This regex match does double duty of finding only files which match
# details (instead of just matching the prefix) and also filtering for
# case-insensitive file systems.
!regex.match?(filename) ||
File.directory?(filename)
end.sort_by do |filename|
# Because we scanned the directory, instead of checking for files
# one-by-one, they will be returned in an arbitrary order.
# We can use the matches found by the regex and sort by their index in
# details.
match = filename.match(regex)
EXTENSIONS.keys.map do |ext|
if ext == :variants && details[ext] == :any
match[ext].nil? ? 0 : 1
elsif match[ext].nil?
# No match should be last
details[ext].length
else
found = match[ext].to_sym
details[ext].index(found)
end
end
end
end
- 9
def build_regex(path, details)
query = Regexp.escape(File.join(@path, path))
exts = EXTENSIONS.map do |ext, prefix|
match =
if ext == :variants && details[ext] == :any
".*?"
else
arr = details[ext].compact
arr.uniq!
arr.map! { |e| Regexp.escape(e) }
arr.join("|")
end
prefix = Regexp.escape(prefix)
"(#{prefix}(?<#{ext}>#{match}))?"
end.join
%r{\A#{query}#{exts}\z}
end
end
# The same as FileSystemResolver but does not allow templates to store
# a virtual path since it is invalid for such resolvers.
- 9
class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
- 9
private_class_method :new
- 9
def self.instances
- 3
[new(""), new("/")]
end
- 9
def build_unbound_template(template, _)
super(template, nil)
end
- 9
def reject_files_external_to_app(files)
files
end
end
end
# frozen_string_literal: true
module ActionView
class Template
module Sources
extend ActiveSupport::Autoload
eager_autoload do
autoload :File
end
end
end
end
# frozen_string_literal: true
module ActionView
class Template
module Sources
class File
def initialize(filename)
@filename = filename
end
def to_s
::File.binread @filename
end
end
end
end
end
# frozen_string_literal: true
module ActionView #:nodoc:
# = Action View Text Template
class Template #:nodoc:
class Text #:nodoc:
attr_accessor :type
def initialize(string)
@string = string.to_s
end
def identifier
"text template"
end
alias_method :inspect, :identifier
def to_str
@string
end
def render(*args)
to_str
end
def format
:text
end
def formats; Array(format); end
deprecate :formats
end
end
end
# frozen_string_literal: true
- 3
require "active_support/core_ext/module/attribute_accessors"
- 3
module ActionView
- 3
class Template #:nodoc:
- 3
class Types
- 3
class Type
- 3
SET = Struct.new(:symbols).new([ :html, :text, :js, :css, :xml, :json ])
- 3
def self.[](type)
if type.is_a?(self)
type
else
new(type)
end
end
- 3
attr_reader :symbol
- 3
def initialize(symbol)
@symbol = symbol.to_sym
end
- 3
def to_s
@symbol.to_s
end
- 3
alias to_str to_s
- 3
def ref
@symbol
end
- 3
alias to_sym ref
- 3
def ==(type)
@symbol == type.to_sym unless type.blank?
end
end
- 3
cattr_accessor :type_klass
- 3
def self.delegate_to(klass)
- 9
self.type_klass = klass
end
- 3
delegate_to Type
- 3
def self.[](type)
type_klass[type]
end
- 3
def self.symbols
type_klass::SET.symbols
end
end
end
end
# frozen_string_literal: true
- 6
require "active_support/core_ext/module/redefine_method"
- 6
require "action_controller"
- 6
require "action_controller/test_case"
- 6
require "action_view"
- 6
require "rails-dom-testing"
- 6
module ActionView
# = Action View Test Case
- 6
class TestCase < ActiveSupport::TestCase
- 6
class TestController < ActionController::Base
- 6
include ActionDispatch::TestProcess
- 6
attr_accessor :request, :response, :params
- 6
class << self
- 6
attr_writer :controller_path
end
- 6
def controller_path=(path)
self.class.controller_path = (path)
end
- 6
def initialize
super
self.class.controller_path = ""
@request = ActionController::TestRequest.create(self.class)
@response = ActionDispatch::TestResponse.new
@request.env.delete("PATH_INFO")
@params = ActionController::Parameters.new
end
end
- 6
module Behavior
- 6
extend ActiveSupport::Concern
- 6
include ActionDispatch::Assertions, ActionDispatch::TestProcess
- 6
include Rails::Dom::Testing::Assertions
- 6
include ActionController::TemplateAssertions
- 6
include ActionView::Context
- 6
include ActionDispatch::Routing::PolymorphicRoutes
- 6
include AbstractController::Helpers
- 6
include ActionView::Helpers
- 6
include ActionView::RecordIdentifier
- 6
include ActionView::RoutingUrlFor
- 6
include ActiveSupport::Testing::ConstantLookup
- 6
delegate :lookup_context, to: :controller
- 6
attr_accessor :controller, :output_buffer, :rendered
- 6
module ClassMethods
- 6
def tests(helper_class)
- 81
case helper_class
when String, Symbol
- 6
self.helper_class = "#{helper_class.to_s.underscore}_helper".camelize.safe_constantize
when Module
- 75
self.helper_class = helper_class
end
end
- 6
def determine_default_helper_class(name)
determine_constant_from_test_name(name) do |constant|
Module === constant && !(Class === constant)
end
end
- 6
def helper_method(*methods)
# Almost a duplicate from ActionController::Helpers
- 6
methods.flatten.each do |method|
- 6
_helpers.module_eval <<-end_eval, __FILE__, __LINE__ + 1
def #{method}(*args, &block) # def current_user(*args, &block)
_test_case.send(:'#{method}', *args, &block) # _test_case.send(:'current_user', *args, &block)
end # end
ruby2_keywords(:'#{method}') if respond_to?(:ruby2_keywords, true)
end_eval
end
end
- 6
attr_writer :helper_class
- 6
def helper_class
@helper_class ||= determine_default_helper_class(name)
end
- 6
def new(*)
include_helper_modules!
super
end
- 6
private
- 6
def include_helper_modules!
helper(helper_class) if helper_class
include _helpers
end
end
- 6
def setup_with_controller
@controller = ActionView::TestCase::TestController.new
@request = @controller.request
@view_flow = ActionView::OutputFlow.new
# empty string ensures buffer has UTF-8 encoding as
# new without arguments returns ASCII-8BIT encoded buffer like String#new
@output_buffer = ActiveSupport::SafeBuffer.new ""
@rendered = +""
make_test_case_available_to_view!
say_no_to_protect_against_forgery!
end
- 6
def config
@controller.config if @controller.respond_to?(:config)
end
- 6
def render(options = {}, local_assigns = {}, &block)
view.assign(view_assigns)
@rendered << output = view.render(options, local_assigns, &block)
output
end
- 6
def rendered_views
@_rendered_views ||= RenderedViewsCollection.new
end
- 6
def _routes
@controller._routes if @controller.respond_to?(:_routes)
end
# Need to experiment if this priority is the best one: rendered => output_buffer
- 6
class RenderedViewsCollection
- 6
def initialize
@rendered_views ||= Hash.new { |hash, key| hash[key] = [] }
end
- 6
def add(view, locals)
@rendered_views[view] ||= []
@rendered_views[view] << locals
end
- 6
def locals_for(view)
@rendered_views[view]
end
- 6
def rendered_views
@rendered_views.keys
end
- 6
def view_rendered?(view, expected_locals)
locals_for(view).any? do |actual_locals|
expected_locals.all? { |key, value| value == actual_locals[key] }
end
end
end
- 6
included do
- 6
setup :setup_with_controller
- 6
ActiveSupport.run_load_hooks(:action_view_test_case, self)
end
- 6
private
# Need to experiment if this priority is the best one: rendered => output_buffer
- 6
def document_root_element
Nokogiri::HTML::Document.parse(@rendered.blank? ? @output_buffer : @rendered).root
end
- 6
def say_no_to_protect_against_forgery!
_helpers.module_eval do
silence_redefinition_of_method :protect_against_forgery?
def protect_against_forgery?
false
end
end
end
- 6
def make_test_case_available_to_view!
test_case_instance = self
_helpers.module_eval do
unless private_method_defined?(:_test_case)
define_method(:_test_case) { test_case_instance }
private :_test_case
end
end
end
- 6
module Locals
- 6
attr_accessor :rendered_views
- 6
def render(options = {}, local_assigns = {})
case options
when Hash
if block_given?
rendered_views.add options[:layout], options[:locals]
elsif options.key?(:partial)
rendered_views.add options[:partial], options[:locals]
end
else
rendered_views.add options, local_assigns
end
super
end
end
# The instance of ActionView::Base that is used by +render+.
- 6
def view
@view ||= begin
view = @controller.view_context
view.singleton_class.include(_helpers)
view.extend(Locals)
view.rendered_views = rendered_views
view.output_buffer = output_buffer
view
end
end
- 6
alias_method :_view, :view
- 6
INTERNAL_IVARS = [
:@NAME,
:@failures,
:@assertions,
:@__io__,
:@_assertion_wrapped,
:@_assertions,
:@_result,
:@_routes,
:@controller,
:@_layouts,
:@_files,
:@_rendered_views,
:@method_name,
:@output_buffer,
:@_partials,
:@passed,
:@rendered,
:@request,
:@routes,
:@tagged_logger,
:@_templates,
:@options,
:@test_passed,
:@view,
:@view_context_class,
:@view_flow,
:@_subscribers,
:@html_document
]
- 6
def _user_defined_ivars
instance_variables - INTERNAL_IVARS
end
# Returns a Hash of instance variables and their values, as defined by
# the user in the test case, which are then assigned to the view being
# rendered. This is generally intended for internal use and extension
# frameworks.
- 6
def view_assigns
Hash[_user_defined_ivars.map do |ivar|
[ivar[1..-1].to_sym, instance_variable_get(ivar)]
end]
end
- 6
def method_missing(selector, *args)
begin
routes = @controller.respond_to?(:_routes) && @controller._routes
rescue
# Don't call routes, if there is an error on _routes call
end
if routes &&
(routes.named_routes.route_defined?(selector) ||
routes.mounted_helpers.method_defined?(selector))
@controller.__send__(selector, *args)
else
super
end
end
- 6
def respond_to_missing?(name, include_private = false)
begin
routes = defined?(@controller) && @controller.respond_to?(:_routes) && @controller._routes
rescue
# Don't call routes, if there is an error on _routes call
end
routes &&
(routes.named_routes.route_defined?(name) ||
routes.mounted_helpers.method_defined?(name))
end
end
- 6
include Behavior
end
end
# frozen_string_literal: true
- 9
require "action_view/template/resolver"
- 9
module ActionView #:nodoc:
# Use FixtureResolver in your tests to simulate the presence of files on the
# file system. This is used internally by Rails' own test suite, and is
# useful for testing extensions that have no way of knowing what the file
# system will look like at runtime.
- 9
class FixtureResolver < OptimizedFileSystemResolver
- 9
def initialize(hash = {}, pattern = nil)
- 6
super("")
- 6
if pattern
ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
@pattern = pattern
end
- 6
@hash = hash
- 6
@path = ""
end
- 9
def data
@hash
end
- 9
def to_s
@hash.keys.join(", ")
end
- 9
private
- 9
def find_candidate_template_paths(path)
@hash.keys.select do |fixture|
fixture.start_with?(path.virtual)
end.map do |fixture|
"/#{fixture}"
end
end
- 9
def source_for_template(template)
@hash[template[1..template.size]]
end
end
- 9
class NullResolver < PathResolver
- 9
def query(path, exts, _, locals, cache:)
handler, format, variant = extract_handler_and_format_and_variant(path)
[ActionView::Template.new("Template generated by Null Resolver", path.virtual, handler, virtual_path: path.virtual, format: format, variant: variant, locals: locals)]
end
end
end
# frozen_string_literal: true
require "concurrent/map"
module ActionView
class UnboundTemplate
def initialize(source, identifier, handler, options)
@source = source
@identifier = identifier
@handler = handler
@options = options
@templates = Concurrent::Map.new(initial_capacity: 2)
end
def bind_locals(locals)
@templates[locals] ||= build_template(locals)
end
private
def build_template(locals)
options = @options.merge(locals: locals)
Template.new(
@source,
@identifier,
@handler,
**options
)
end
end
end
# frozen_string_literal: true
- 9
require_relative "gem_version"
- 9
module ActionView
# Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
- 9
def self.version
gem_version
end
end
# frozen_string_literal: true
- 9
module ActionView
- 9
module ViewPaths
- 9
extend ActiveSupport::Concern
- 9
included do
- 24
ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze)
end
- 9
delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
:locale, :locale=, to: :lookup_context
- 9
module ClassMethods
- 9
def _view_paths
- 12
ViewPaths.get_view_paths(self)
end
- 9
def _view_paths=(paths)
- 36
ViewPaths.set_view_paths(self, paths)
end
- 9
def _prefixes # :nodoc:
@_prefixes ||= begin
return local_prefixes if superclass.abstract?
local_prefixes + superclass._prefixes
end
end
# Append a path to the list of view paths for this controller.
#
# ==== Parameters
# * <tt>path</tt> - If a String is provided, it gets converted into
# the default view path. You may also provide a custom view path
# (see ActionView::PathSet for more information)
- 9
def append_view_path(path)
- 6
self._view_paths = view_paths + Array(path)
end
# Prepend a path to the list of view paths for this controller.
#
# ==== Parameters
# * <tt>path</tt> - If a String is provided, it gets converted into
# the default view path. You may also provide a custom view path
# (see ActionView::PathSet for more information)
- 9
def prepend_view_path(path)
self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
end
# A list of all of the default view paths for this controller.
- 9
def view_paths
- 12
_view_paths
end
# Set the view paths.
#
# ==== Parameters
# * <tt>paths</tt> - If a PathSet is provided, use that;
# otherwise, process the parameter into a PathSet.
- 9
def view_paths=(paths)
- 30
self._view_paths = ActionView::PathSet.new(Array(paths))
end
- 9
private
# Override this method in your controller if you want to change paths prefixes for finding views.
# Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
- 9
def local_prefixes
[controller_path]
end
end
# :stopdoc:
- 9
@all_view_paths = {}
- 9
def self.get_view_paths(klass)
- 12
@all_view_paths[klass] || get_view_paths(klass.superclass)
end
- 9
def self.set_view_paths(klass, paths)
- 60
@all_view_paths[klass] = paths
end
- 9
def self.all_view_paths
- 3
@all_view_paths.values.uniq
end
# :startdoc:
# The prefixes used in render "foo" shortcuts.
- 9
def _prefixes # :nodoc:
self.class._prefixes
end
# <tt>LookupContext</tt> is the object responsible for holding all
# information required for looking up templates, i.e. view paths and
# details. Check <tt>ActionView::LookupContext</tt> for more information.
- 9
def lookup_context
@_lookup_context ||=
ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
end
- 9
def details_for_lookup
{}
end
# Append a path to the list of view paths for the current <tt>LookupContext</tt>.
#
# ==== Parameters
# * <tt>path</tt> - If a String is provided, it gets converted into
# the default view path. You may also provide a custom view path
# (see ActionView::PathSet for more information)
- 9
def append_view_path(path)
lookup_context.view_paths.push(*path)
end
# Prepend a path to the list of view paths for the current <tt>LookupContext</tt>.
#
# ==== Parameters
# * <tt>path</tt> - If a String is provided, it gets converted into
# the default view path. You may also provide a custom view path
# (see ActionView::PathSet for more information)
- 9
def prepend_view_path(path)
lookup_context.view_paths.unshift(*path)
end
end
end