@beaugaines you might want to look into instance_eval
. Alternatively, you could accomplish something similar by just yielding to a block. Here’s some rough and dirty code:
class DOMElement
INDENT_STEP = " ".freeze
ELEMENTS = [:body, :div, :ul, :li]
ELEMENTS.each do |element|
define_method(element) do |attributes={}, &block|
append_child(element, attributes, &block)
end
end
attr_reader :children, :tag_name, :attributes
def initialize(tag_name, attributes={}, indent="")
@attributes = attributes
@children = []
@tag_name = tag_name
@indent = indent
end
def text=(text)
children << next_indent + text + "\n"
end
def to_s
"#{indent}<#{tag_name} #{html_attributes}>\n" +
children.map(&:to_s).join +
"#{indent}</#{tag_name}>\n"
end
private
attr_reader :indent
def append_child(name, attributes)
child = DOMElement.new(name, attributes, next_indent)
yield child if block_given?
children << child
end
def next_indent
indent + INDENT_STEP
end
def html_attributes
attributes.map { |key, value| %(#{key}="#{value}") }.join(" ")
end
end
How this works:
Calling a method such as div
on a DOMElement
creates a new DOMElement
object and yields it to a block. The user can then call methods on this div element if desired (including creating children). Once the user is done, the div is appended to the original elements children.
Since the implementation of all the methods that create child elements is the same apart for the name of the element created, I used define_method
to programmatically create a method for each element type in ELEMENTS
.
Text is appended to children just like a DOMElement
would. In a fancier implementation, I might wrap the strings with a TextElement
object.
to_s
does three things:
- Creates an opening tag based on
name
and adds the attributes
- Creates a closing tag based on
name
- Populates the tag by calling
to_s
on each of it’s children
This recursive approach allows us to render arbitrarily large DOM trees.
Indentation is handled by incrementing a prefix by INDENT_STEP
every time we go down a level
In Action
Recreating your example:
doc = DOMElement.new(:html)
doc.body do |body|
body.div id: "container" do |div|
div.ul class: "pretty" do |ul|
ul.li class: "active" do |li|
li.text = "Item 1"
end
ul.li do |li|
li.text = "Item 2"
end
end
end
end
doc.to_s
generates the following:
<html >
<body >
<div id="container">
<ul class="pretty">
<li class="active">
Item 1
</li>
<li >
Item 2
</li>
</ul>
</div>
</body>
</html>
Hope this helps!