‘Iacta alea est’
In a previous post I looked at how Markaby integrated variables (assigns) and helper methods into its API. In this post I’d like to explore what a similar API in Hypertext DSL would look like.
Again, the Hypertext::DSL
class is incredibly concise (twenty-five percent of it’s 61 lines contain nothing other than an enumeration of HTML5 tags), so we’ll simply reproduce it in this post with a class called DSL
which gives us the freedom to experiment.
If we start with what Markaby calls the assigns, and what I am calling the params, then we can adjust the initialize
method from this:
def initialize(&block)
@ht = Hypertext.new
instance_eval(&block)
end
to this:
def initialize(params, &block)
@ht = Hypertext.new
@params = params
@params.each do |key, val|
next if [:ht, :dom].include?(key)
instance_variable_set(sprintf("@%s", key), val)
end
instance_eval(&block)
end
The implementation of this method is similar to our previous experiment with variables although we can make one improvement by partitioning “private” instance variables with those that will be set via params
using an underscore (_
):
def initialize(params, &block)
@_ht = Hypertext.new
@_params = params
@_params.each do |key, val|
instance_variable_set(sprintf("@%s", key), val)
end
instance_eval(&block)
end
The other references to @ht
in the DSL should now be replaced by @_ht
.
Running the following code which loads our DSL
class then confirms that variables are being defined on the DSL instance and are available to the template:
html = DSL.new page_title: "Hello, World!" do
html lang: "en-GB" do
head do
title do
text @page_title
end
end
body do
h1 do
text "Welcome"
end
end
end
end
puts html.to_s
#=>
# <html lang="en-GB">
# <head>
# <title>
# Hello, World!
# </title>
# </head>
# <body>
# <h1>
# Welcome
# </h1>
# </body>
# </html>
We can check params/variables/assigns off our to-do list. Moving onto helper methods, we’ll again use the same approach, the introduction of a helper object to which we delegate unknown messages. Now we’ll extend the DSL#initialize
method even further, adding a helper
parameter and assigning it to an instance variable:
def initialize(params, helper = nil, &block)
@_ht = Hypertext.new
@_params = params
@_params.each do |key, val|
instance_variable_set(sprintf("@%s", key), val)
end
@_helper = helper
instance_eval(&block)
end
And we’ll need to pair that assignment with method delegation via method_missing
.
def method_missing(sym, *args, &block)
if @_helper.respond_to?(sym, true)
@_helper.send(sym, *args, &block)
end
end
This is indeed a simplified version which only looks for methods on the helper object and doesn’t take into account instance variables. This is enough for us for now. Let’s reintroduce a helper with our id_generator
method:
class Helper
def id_generator
"id-#{rand(100)}"
end
end
And then, introducing this method into the template and running the example:
html = DSL.new({page_title: "Hello, World!"}, Helper.new) do
html lang: "en-GB" do
head do
title do
text @page_title
end
end
body do
h1 id: id_generator do
text "Welcome"
end
end
end
end
puts html.to_s
# =>
# <html lang="en-GB">
# <head>
# <title>
# Hello, World!
# </title>
# </head>
# <body>
# <h1 id="id-90">
# Welcome
# </h1>
# </body>
# </html>
Who knew it would be so simple? The only other thing to test out at this stage is how it works with loading in templates from a file. Then we can move on to figuring out if these primitives allow us to build higher level constructs, HTML helpers, components and the like.
In the meantime, is this the best way forward? Are there other, simpler, easier, more fitting APIs? Is there a way to accomplish the delegation of unknown methods to the helper without method_missing
?
—Tuesday 30th March 2021.