Class: Erector::Widget

Inherits:
Object show all
Defined in:
lib/erector/widget.rb

Overview

A Widget is the center of the Erector universe.

To create a widget, extend Erector::Widget and implement the content method. Inside this method you may call any of the tag methods like span or p to emit HTML/XML tags.

You can also define a widget on the fly by passing a block to new. This block will get executed when the widget’s content method is called.

To render a widget from the outside, instantiate it and call its to_s method.

A widget’s new method optionally accepts an options hash. Entries in this hash are converted to instance variables, and attr_reader accessors are defined for each.

You can add runtime input checking via the needs macro. See #needs. This mechanism is meant to ameliorate development-time confusion about exactly what parameters are supported by a given widget, avoiding confusing runtime NilClass errors.

To call one widget from another, inside the parent widget’s content method, instantiate the child widget and call the widget method. This assures that the same output stream is used, which gives better performance than using capture or to_s. It also preserves the indentation and helpers of the enclosing class.

In this documentation we’ve tried to keep the distinction clear between methods that emit text and those that return text. “Emit” means that it writes to the output stream; “return” means that it returns a string like a normal method and leaves it up to the caller to emit that string if it wants.

Constant Summary collapse

NON_NEWLINEY =
{'i' => true, 'b' => true, 'small' => true,
  'img' => true, 'span' => true, 'a' => true,
  'input' => true, 'textarea' => true, 'button' => true, 'select' => true
}
SPACES_PER_INDENT =
2
RESERVED_INSTANCE_VARS =
[:helpers, :assigns, :block, :parent, :output, :prettyprint, :indentation, :at_start_of_line]
@@prettyprint_default =
false

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(assigns = {}, &block) ⇒ Widget

Returns a new instance of Widget.



160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/erector/widget.rb', line 160

def initialize(assigns={}, &block)
  unless assigns.is_a? Hash
    raise "Erector's API has changed. Now you should pass only an options hash into Widget.new; the rest come in via to_s, or by using #widget."
  end
  if (respond_to? :render) &&
    !self.method(:render).to_s.include?("(RailsWidget)")
    raise "Erector's API has changed. You should rename #{self.class}#render to #content."
  end
  @assigns = assigns
  assign_locals(assigns)
  @parent = block ? eval("self", block.binding) : nil
  @block = block
  self.class.after_initialize self
end

Class Method Details

.after_initialize(instance = nil, &blk) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/erector/widget.rb', line 68

def after_initialize(instance=nil, &blk)
  if blk
    after_initialize_parts << blk
  elsif instance
    if superclass.respond_to?(:after_initialize)
      superclass.after_initialize instance
    end
    after_initialize_parts.each do |part|
      instance.instance_eval &part
    end
  else
    raise ArgumentError, "You must provide either an instance or a block"
  end
end

.all_tagsObject



37
38
39
# File 'lib/erector/widget.rb', line 37

def all_tags
  Erector::Widget.full_tags + Erector::Widget.empty_tags
end

.empty_tagsObject

Tags which are always self-closing. Click “[Source]” to see the full list.



42
43
44
45
# File 'lib/erector/widget.rb', line 42

def empty_tags
  ['area', 'base', 'br', 'col', 'frame', 
  'hr', 'img', 'input', 'link', 'meta']
end

.full_tagsObject

Tags which can contain other stuff. Click “[Source]” to see the full list.



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/erector/widget.rb', line 48

def full_tags
  [
    'a', 'abbr', 'acronym', 'address', 
    'b', 'bdo', 'big', 'blockquote', 'body', 'button', 
    'caption', 'center', 'cite', 'code', 'colgroup',
    'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em',
    'embed',
    'fieldset', 'form', 'frameset',
    'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 'i',
    'iframe', 'ins', 'kbd', 'label', 'legend', 'li', 'map',
    'noframes', 'noscript', 
    'object', 'ol', 'optgroup', 'option', 'p', 'param', 'pre',
    'q', 's',
    'samp', 'script', 'select', 'small', 'span', 'strike',
    'strong', 'style', 'sub', 'sup',
    'table', 'tbody', 'td', 'textarea', 'tfoot', 
    'th', 'thead', 'title', 'tr', 'tt', 'u', 'ul', 'var'
  ]
end

.needs(*args) ⇒ Object

Class method by which widget classes can declare that they need certain parameters. If needed parameters are not passed in to #new, then an exception will be thrown (with a hopefully useful message about which parameters are missing). This is intended to catch silly bugs like passing in a parameter called ‘name’ to a widget that expects a parameter called ‘title’. Every variable declared in ‘needs’ will get an attr_reader accessor declared for it.

You can also declare default values for parameters using hash syntax. You can put #needs declarations on multiple lines or on the same line; the only caveat is that if there are default values, they all have to be at the end of the line (so they go into the magic hash parameter).

If a widget has no #needs declaration then it will accept any combination of parameters (and make accessors for them) just like normal. In that case there will be no ‘attr_reader’s declared. If a widget wants to declare that it takes no parameters, use the special incantation “needs nil” (and don’t declare any other needs, or kittens will cry).

Usage:

class FancyForm < Erector::Widget
  needs :title, :show_okay => true, :show_cancel => false
  ...
end

That means that

FancyForm.new(:title => 'Login')

will succeed, as will

FancyForm.new(:title => 'Login', :show_cancel => true)

but

FancyForm.new(:name => 'Login')

will fail.



123
124
125
126
127
# File 'lib/erector/widget.rb', line 123

def self.needs(*args)
  args.each do |arg|
    (@needs ||= []) << (arg.nil? ? nil : (arg.is_a? Hash) ? arg : arg.to_sym)
  end
end

.prettyprint_default=(enabled) ⇒ Object



145
146
147
# File 'lib/erector/widget.rb', line 145

def self.prettyprint_default=(enabled)
  @@prettyprint_default = enabled
end

Instance Method Details

#_render(options = {}, &blk) ⇒ Object



268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/erector/widget.rb', line 268

def _render(options = {}, &blk)
  options = {
    :output => "",  # "" is apparently faster than [] in a long-running process
    :prettyprint => prettyprint_default,
    :indentation => 0,
    :helpers => nil,
    :content_method_name => :content,
  }.merge(options)
  context(options[:output], options[:prettyprint], options[:indentation], options[:helpers]) do
    send(options[:content_method_name], &blk)
    output
  end
end

#assign_local(name, value) ⇒ Object

Raises:

  • (ArgumentError)


227
228
229
230
# File 'lib/erector/widget.rb', line 227

def assign_local(name, value)
  raise ArgumentError, "Sorry, #{name} is a reserved variable name for Erector. Please choose a different name." if RESERVED_INSTANCE_VARS.include?(name)
  instance_variable_set("@#{name}", value)
end

#assign_locals(local_assigns) ⇒ Object



200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/erector/widget.rb', line 200

def assign_locals(local_assigns)
  needed = self.class.get_needs.map{|need| need.is_a?(Hash) ? need.keys : need}.flatten
  assigned = []
  local_assigns.each do |name, value|
    unless needed.empty? || needed.include?(name)
      raise "Unknown parameter '#{name}'. #{self.class.name} only accepts #{needed.join(', ')}"
    end
    assign_local(name, value)
    assigned << name
  end

  # set variables with default values
  self.class.get_needs.select{|var| var.is_a? Hash}.each do |hash|
    hash.each_pair do |name, value|
      unless assigned.include?(name)
        assign_local(name, value)
        assigned << name
      end
    end
  end

  missing = needed - assigned
  unless missing.empty? || missing == [nil]
    raise "Missing parameter#{missing.size == 1 ? '' : 's'}: #{missing.join(', ')}"
  end
end

#capture(&block) ⇒ Object

Creates a whole new output string, executes the block, then converts the output string to a string and emits it as raw text. If at all possible you should avoid this method since it hurts performance, and use widget or write_via instead.



472
473
474
475
476
477
478
479
480
481
# File 'lib/erector/widget.rb', line 472

def capture(&block)
  begin
    original_output = output
    @output = ""
    yield
    raw(output.to_s)
  ensure
    @output = original_output
  end
end

#character(code_point_or_name) ⇒ Object

Return a character given its unicode code point or unicode name.



423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/erector/widget.rb', line 423

def character(code_point_or_name)
  if code_point_or_name.is_a?(Symbol)
    found = Erector::CHARACTERS[code_point_or_name]
    if found.nil?
      raise "Unrecognized character #{code_point_or_name}"
    end
    raw("&#x#{sprintf '%x', found};")
  elsif code_point_or_name.is_a?(Integer)
    raw("&#x#{sprintf '%x', code_point_or_name};")
  else
    raise "Unrecognized argument to character: #{code_point_or_name}"
  end
end

#close_tag(tag_name) ⇒ Object

Emits a close tag, consisting of ‘<’, ‘/’, tag name, and ‘>’



438
439
440
441
442
443
444
445
446
447
# File 'lib/erector/widget.rb', line 438

def close_tag(tag_name)
  @indentation -= SPACES_PER_INDENT
  indent()

  output <<("</#{tag_name}>")

  if newliney(tag_name)
    _newline
  end
end

#contentObject

Template method which must be overridden by all widget subclasses. Inside this method you call the magic #element methods which emit HTML and text to the output string. If you call “super” (or don’t override content) then your widget will render any block that was passed into its constructor. If you want this block to have access to Erector methods then see Erector::Inline#content.



290
291
292
293
294
# File 'lib/erector/widget.rb', line 290

def content
  if @block
    @block.call
  end
end

#css(href) ⇒ Object

Convenience method to emit a css file link, which looks like this: <link href=“erector.css” rel=“stylesheet” type=“text/css” /> The parameter is the full contents of the href attribute, including any “.css” extension.

If you want to emit raw CSS inline, use the #style method instead.



548
549
550
# File 'lib/erector/widget.rb', line 548

def css(href)
  link :rel => 'stylesheet', :type => 'text/css', :href => href
end

#element(*args, &block) ⇒ Object

Internal method used to emit an HTML/XML element, including an open tag, attributes (optional, via the default hash), contents (also optional), and close tag.

Using the arcane powers of Ruby, there are magic methods that call element for all the standard HTML tags, like a, body, p, and so forth. Look at the source of #full_tags for the full list. Unfortunately, this big mojo confuses rdoc, so we can’t see each method in this rdoc page, but trust us, they’re there.

When calling one of these magic methods, put attributes in the default hash. If there is a string parameter, then it is used as the contents. If there is a block, then it is executed (yielded), and the string parameter is ignored. The block will usually be in the scope of the child widget, which means it has access to all the methods of Widget, which will eventually end up appending text to the output string. See how elegant it is? Not confusing at all if you don’t think about it.



354
355
356
# File 'lib/erector/widget.rb', line 354

def element(*args, &block)
  __element__(*args, &block)
end

#empty_element(*args, &block) ⇒ Object

Internal method used to emit a self-closing HTML/XML element, including a tag name and optional attributes (passed in via the default hash).

Using the arcane powers of Ruby, there are magic methods that call empty_element for all the standard HTML tags, like img, br, and so forth. Look at the source of #empty_tags for the full list. Unfortunately, this big mojo confuses rdoc, so we can’t see each method in this rdoc page, but trust us, they’re there.



367
368
369
# File 'lib/erector/widget.rb', line 367

def empty_element(*args, &block)
  __empty_element__(*args, &block)
end

#h(content) ⇒ Object

Returns an HTML-escaped version of its parameter. Leaves the output string untouched. Note that the #text method automatically HTML-escapes its parameter, so be careful not to do something like text(h(“2<4”)) since that will double-escape the less-than sign (you’ll get “2&amp;lt;4” instead of “2&lt;4”).



376
377
378
# File 'lib/erector/widget.rb', line 376

def h(content)
  content.html_escape
end

#html_escapeObject

(Should we make this hidden?)



329
330
331
# File 'lib/erector/widget.rb', line 329

def html_escape
  return to_s
end

#instruct(attributes = {:version => "1.0", :encoding => "UTF-8"}) ⇒ Object

Emits an XML instruction, which looks like this: <?xml version="1.0" encoding="UTF-8"?>



464
465
466
# File 'lib/erector/widget.rb', line 464

def instruct(attributes={:version => "1.0", :encoding => "UTF-8"})
  output << "<?xml#{format_sorted(sort_for_xml_declaration(attributes))}?>"
end

#javascript(*args, &block) ⇒ Object

Emits a javascript block inside a script tag, wrapped in CDATA doohickeys like all the cool JS kids do.



505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/erector/widget.rb', line 505

def javascript(*args, &block)
  if args.length > 2
    raise ArgumentError, "Cannot accept more than two arguments"
  end
  attributes, value = nil, nil
  arg0 = args[0]
  if arg0.is_a?(Hash)
    attributes = arg0
  else
    value = arg0
    arg1 = args[1]
    if arg1.is_a?(Hash)
      attributes = arg1
    end
  end
  attributes ||= {}
  attributes[:type] = "text/javascript"
  open_tag 'script', attributes

  # Shouldn't this be a "cdata" HtmlPart?
  # (maybe, but the syntax is specific to javascript; it isn't
  # really a generic XML CDATA section.  Specifically,
  # ]]> within value is not treated as ending the
  # CDATA section by Firefox2 when parsing text/html,
  # although I guess we could refuse to generate ]]>
  # there, for the benefit of XML/XHTML parsers).
  rawtext "\n// <![CDATA[\n"
  if block
    instance_eval(&block)
  else
    rawtext value
  end
  rawtext "\n// ]]>\n"

  close_tag 'script'
  rawtext "\n"
end

#join(array, separator) ⇒ Object

Emits the result of joining the elements in array with the separator. The array elements and separator can be Erector::Widget objects, which are rendered, or strings, which are html-escaped and output.



452
453
454
455
456
457
458
459
460
461
# File 'lib/erector/widget.rb', line 452

def join(array, separator)
  first = true
  array.each do |widget_or_text|
    if !first
      text separator
    end
    first = false
    text widget_or_text
  end
end

#nbsp(value = " ") ⇒ Object

Returns a copy of value with spaces replaced by non-breaking space characters. With no arguments, return a single non-breaking space. The output uses the escaping format ‘&#160;’ since that works in both HTML and XML (as opposed to ‘&nbsp;’ which only works in HTML).



418
419
420
# File 'lib/erector/widget.rb', line 418

def nbsp(value = " ")
  raw(value.html_escape.gsub(/ /,'&#160;'))
end

#newliney(tag_name) ⇒ Object



558
559
560
561
562
563
564
# File 'lib/erector/widget.rb', line 558

def newliney(tag_name)
  if @prettyprint
    !NON_NEWLINEY.include?(tag_name)
  else
    false
  end
end

#open_tag(tag_name, attributes = {}) ⇒ Object

Emits an open tag, comprising ‘<’, tag name, optional attributes, and ‘>’



381
382
383
384
385
386
387
# File 'lib/erector/widget.rb', line 381

def open_tag(tag_name, attributes={})
  indent_for_open_tag(tag_name)
  @indentation += SPACES_PER_INDENT

  output << "<#{tag_name}#{format_attributes(attributes)}>"
  @at_start_of_line = false
end

#prettyprint_defaultObject



141
142
143
# File 'lib/erector/widget.rb', line 141

def prettyprint_default
  @@prettyprint_default
end

#raw(value) ⇒ Object

Returns text which will not be HTML-escaped.



405
406
407
# File 'lib/erector/widget.rb', line 405

def raw(value)
  RawString.new(value.to_s)
end

#rawtext(value) ⇒ Object

Emits text which will not be HTML-escaped. Same effect as text(raw(s))



410
411
412
# File 'lib/erector/widget.rb', line 410

def rawtext(value)
  text raw(value)
end

#text(value) ⇒ Object

Emits text. If a string is passed in, it will be HTML-escaped. If a widget or the result of calling methods such as raw is passed in, the HTML will not be HTML-escaped again. If another kind of object is passed in, the result of calling its to_s method will be treated as a string would be.



394
395
396
397
398
399
400
401
402
# File 'lib/erector/widget.rb', line 394

def text(value)
  if value.is_a? Widget
    widget value
  else
    output <<(value.html_escape)
  end
  @at_start_of_line = false
  nil
end

#to_a(options = {}, &blk) ⇒ Object

Entry point for rendering a widget (and all its children). Same as #to_s only it returns an array, for theoretical performance improvements when using a Rack server (like Sinatra or Rails Metal).

# Options: see #to_s



264
265
266
# File 'lib/erector/widget.rb', line 264

def to_a(options = {}, &blk)
  _render({:output => []}.merge(options), &blk).to_a
end

#to_prettyObject

Render (like to_s) but adding newlines and indentation. This is a convenience method; you may just want to call to_s(:prettyprint => true) so you can pass in other rendering options as well.



235
236
237
# File 'lib/erector/widget.rb', line 235

def to_pretty
  to_s(:prettyprint => true)
end

#to_s(options = {}, &blk) ⇒ Object Also known as: inspect

Entry point for rendering a widget (and all its children). This method creates a new output string (if necessary), calls this widget’s #content method and returns the string.

Options:

output

the string to output to. Default: a new empty string

prettyprint

whether Erector should add newlines and indentation. Default: the value of prettyprint_default (which is false by default).

indentation

the amount of spaces to indent. Ignored unless prettyprint is true.

helpers

a helpers object containing utility methods. Usually this is a Rails view object.

content_method_name

in case you want to call a method other than #content, pass its name in here.



254
255
256
257
# File 'lib/erector/widget.rb', line 254

def to_s(options = {}, &blk)
  raise "Erector::Widget#to_s now takes an options hash, not a symbol. Try calling \"to_s(:content_method_name=> :#{options})\"" if options.is_a? Symbol
  _render(options, &blk).to_s
end

#url(href) ⇒ Object

Convenience method to emit an anchor tag whose href and text are the same, e.g. <a href=“example.com”>example.com</a>



554
555
556
# File 'lib/erector/widget.rb', line 554

def url(href)
  a href, :href => href
end

#widget(target, assigns = {}, &block) ⇒ Object

Emits a (nested) widget onto the current widget’s output stream. Accepts either a class or an instance. If the first argument is a class, then the second argument is a hash used to populate its instance variables. If the first argument is an instance then the hash must be unspecified (or empty).

The sub-widget will have access to the methods of the parent class, via some method_missing magic and a “parent” pointer.



316
317
318
319
320
321
322
323
324
325
326
# File 'lib/erector/widget.rb', line 316

def widget(target, assigns={}, &block)
  child = if target.is_a? Class
    target.new(assigns, &block)
  else
    unless assigns.empty?
      raise "Unexpected second parameter. Did you mean to pass in variables when you instantiated the #{target.class.to_s}?"
    end
    target
  end
  child.write_via(self)
end

#write_via(parent) ⇒ Object

To call one widget from another, inside the parent widget’s content method, instantiate the child widget and call its write_via method, passing in self. This assures that the same output string is used, which gives better performance than using capture or to_s. You can also use the widget method.



301
302
303
304
305
306
# File 'lib/erector/widget.rb', line 301

def write_via(parent)
  @parent = parent
  context(parent.output, parent.prettyprint, parent.indentation, parent.helpers) do
    content
  end
end