Skip to content

Commit

Permalink
HTML tag dynamic proxy - new modern sytax for tag helpers. Fixes rail…
Browse files Browse the repository at this point in the history
  • Loading branch information
marekkirejczyk committed Jun 21, 2016
1 parent b401467 commit 397a85d
Show file tree
Hide file tree
Showing 4 changed files with 292 additions and 35 deletions.
31 changes: 31 additions & 0 deletions actionview/CHANGELOG.md
@@ -1,3 +1,34 @@
* New syntax for tag helpers. Avoid positional parameters and suport HTML5 by default.
Example usage of tag helpers before:

```ruby
tag(:br)

content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
```

```html
<%= content_tag :div, class: "strong" do -%>
Hello world!
<% end -%>
```

Example usage of tag helpers after:

```ruby
tag.br

tag.div tag.p("Hello world!"), class: "strong"
```

```html
<%= tag.div class: "strong" do %>
Hello world!
<% end %>
```

*Marek Kirejczyk*

* `select_tag`'s `include_blank` option for generation for blank option tag, now adds an empty space label,
when the value as well as content for option tag are empty, so that we confirm with html specification.
Ref: https://www.w3.org/TR/html5/forms.html#the-option-element.
Expand Down
128 changes: 103 additions & 25 deletions actionview/lib/action_view/helpers/tag_helper.rb
Expand Up @@ -5,7 +5,7 @@ module ActionView
# = Action View Tag Helpers
module Helpers #:nodoc:
# Provides methods to generate HTML tags programmatically when you can't use
# a Builder. By default, they output XHTML compliant tags.
# a Builder.
module TagHelper
extend ActiveSupport::Concern
include CaptureHelper
Expand All @@ -26,44 +26,67 @@ module TagHelper
PRE_CONTENT_STRINGS[:textarea] = "\n"
PRE_CONTENT_STRINGS["textarea"] = "\n"

# TagBuilder work in progress
# TODO:
# * Documentation
# * More tests
# * support for NEED_CLOSING element
# * Method missing -> raise if unknown html tag
# * fill NEED_CLOSING
# * include support for escape argument
# * blocks
# * Extract to sepearete file (?)

class TagBuilder
include ActionView::Helpers::TagHelper

VOID_ELEMENTS = %w(base br col embed hr img input keygen link meta param source track wbr).to_set
class TagBuilder #:nodoc:
include TagHelper

VOID_ELEMENTS = %w(base br col embed hr img input keygen link
meta param source track wbr).to_set

VOID_ELEMENTS.merge(VOID_ELEMENTS.map(&:to_sym))

ELEMENTS = %w(a abbr acronym address applet area article aside audio b
base basefont bdi bdo bgsound big blink blockquote body br
button canvas caption center cite code col colgroup command
content data datalist dd del details dfn dialog dir div dl
dt element em embed fieldset figcaption figure font footer
form frame frameset h1 h2 h3 h4 h5 h6 head header hgroup hr
html i iframe image img input ins isindex kbd keygen label
legend li link listing main map mark marquee menu menuitem
meta meter multicol nav nextid nobr noembed noframes
noscript object ol optgroup option output p param picture
plaintext pre progress q rp rt rtc ruby s samp script
section select shadow small source spacer span strike strong
style sub summary sup table tbody td template textarea tfoot
th thead time title tr track tt u ul var video wbr xmp
).to_set

ELEMENTS.merge(ELEMENTS.map(&:to_sym))

def initialize(view_context)
@view_context = view_context
end

private

def render_tag(name, content_or_options = nil, options = nil, &block)
def tag_string(name, content_or_options = nil, options_or_escape = nil, escape = true, &block)
raise_if_void_tag_with_content(name, content_or_options, &block)
if block_given?
content_tag(name, yield(self), content_or_options)
content_tag_string(name, @view_context.capture(self, &block), content_or_options, options_or_escape)
elsif content_or_options.is_a? String
content_tag(name, content_or_options, options)
content_tag_string(name, content_or_options, options_or_escape, escape)
elsif VOID_ELEMENTS.include?(name)
tag(name, content_or_options, false, escape = true)
options_or_escape = true if options_or_escape.nil?
"<#{name}#{tag_options(content_or_options, options_or_escape)}>".html_safe
else
content_tag(name, "", content_or_options)
options_or_escape = true if options_or_escape.nil?
content_tag_string(name, "", content_or_options, options_or_escape)
end
end

def method_missing(called, *args, &block)
render_tag(called, args[0], args[1], &block)
def raise_if_void_tag_with_content(name, content_or_options, &block)
has_content = block_given? || content_or_options.is_a?(String)
void_with_content = has_content && VOID_ELEMENTS.include?(name)
raise ArgumentError, "Void tag with content" if void_with_content
end

def method_missing(called, *args, &block)
return tag_string(called, *args, &block) if ELEMENTS.include?(called)
super
end
end


# Returns an HTML tag. Supports two syntax variants: traditonal and modern.
# === Traditional syntax
# 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
Expand Down Expand Up @@ -109,8 +132,63 @@ def method_missing(called, *args, &block)
#
# tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)})
# # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
#
# === Modern syntax
# Modern syntax uses one of following format:
# tag.<name>(options, escape)
# tag.<name>(content, options, escape)
# Returns an HTML tag. Conetent has to be a string. If content is passed tag is surrounding the content. Otherwise tag will be empty. You can also use a block to pass the content inside ERB templates. Result is by default is HTML5 compliant. Set escape parameter to false to disable attribute value escaping. The tag will be generated with related closing tag unless tag is a void[https://www.w3.org/TR/html5/syntax.html#void-elements] element. Method will rise NoMethodError if element is not a part of the standard and ArgumentError if you try to pass content to a void element.
#
# ==== Options
# Like with traditional syntax the options hash can be used with attributes with no value like (disabled and readonly), which you can give a value of true in the options hash. You can use symbols or strings for the attribute names.
#
# ==== Examples
# tag.span
# # => <span></span>
#
# tag.span(class: "bookmark")
# # => <span class=\"bookmark\"></span>
#
# 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 &amp; shut.png">
#
# tag.img({src: "open &amp; shut.png"}, nil, false)
# # => <img src="open &amp; shut.png">
#
# tag.div(data: {name: 'Stephen', city_state: %w(Chicago IL)})
# # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]"></div>
#
# tag.p "Hello world!"
# # => <p>Hello world!</p>
#
# tag.div tag.p("Hello world!"), class: "strong"
# # => <div class="strong"><p>Hello world!</p></div>
#
# tag.div "Hello world!", class: ["strong", "highlight"]
# # => <div class="strong highlight">Hello world!</div>
#
# tag.select options, multiple: true
# # => <select multiple="multiple">...options...</select>
#
# <%= tag.div class: "strong" do %>
# Hello world!
# <% end %>
# # => <div class="strong">Hello world!</div>
#
# <%= tag.div class: "strong" do |t| %>
# <% t.p("Hello world!") %>
# <% end %>
# # => <div class="strong"><p>Hello world!</p></div>


def tag(name = nil, options = nil, open = false, escape = true)
return TagBuilder.new if name == nil
return TagBuilder.new(self) if name == nil
"<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
end

Expand Down
@@ -0,0 +1,3 @@
<%= tag.p do %>
<%= tag.b 'Hello' %>
<% end %>

0 comments on commit 397a85d

Please sign in to comment.