Magic template methods
Part of the interface for Phlex views is that they define an instance method for the template. The template method is called when rendering a view to determine the output.
For example, this view has a template
method that renders an <h1>
tag with the content “Hello”.
class Hello < Phlex::View
def template
h1 { "Hello" }
end
end
As you can see, HTML elements are created by calling instance methods — the h1
method here creates an <h1>
tag — but this presents a problem: the HTML spec includes an element named template
. Calling template
with a template
is expected to create a <template>
element but instead, it creates an infinite loop because it references itself.
The natural solution is just to find another name, but after searching and pondering on this for quite some time, I haven’t been able to come up with anything I’m remotely happy with.
The only other option that comes close in my mind is render
, but unfortunately the render
instance method is also taken — it’s used to render other views inside the template.
Without an alternative name, I started to explore options for changing what the template method does depending on where it’s used. When it’s called from inside a template, it should create a <template>
tag; otherwise it should render the view template.
Tracking rendering state in an instance variable
The first idea was to prepend a module that overrides the template method and tracks the rendering state in an instance variable on the view.
module TemplateOverride
def template(**kwargs, &block)
if @rendering
template_tag(**kwargs, &block)
else
@rendering = true
super
end
end
end
The first time this method is called, @rendering
will be nil
, so we’ll hit the else
condition which calls the original template method, super
. But right before we do that we set @rendering
to true
.
If this method is called again from within the template, @rendering
will be true
, so it redirects to the template_tag
method for our <template>
tag. We can use the inherited
hook to prepend this override whenever Phlex::View
is subclassed.
class Phlex::View
def self.inherited(child)
child.prepend(TemplateOverride)
end
end
This technique has a performance cost because every template render now goes through an extra method which allocates a Hash
when it picks up **kwargs
for the potential template tag attributes, but it works. You can define a template
method that calls the template
method and outputs a <template>
tag rather than an infinite loop.
class Modal < Phlex::View
def template
template do
...
end
end
end
Unfortunately, this technique has some problems.
If we subclass an abstract view and don’t define a template
method, we end up with two prepended template methods stacked on top of each other.
class A < Phlex::View
def template
h1 { "Hello" }
end
end
class B < A; end
If we look at B.ancestors
, we’ll see the ancestry looks like this:
TemplateOverride < B < TemplateOverride < A < Phlex::View < Object
And because B
doesn’t itself define a template
method, B
’s template
method and its super method are both overrides. When the first override calls super
, it hits the second override but this time with @rendering
set to true
. So the second override thinks template
has been called from within the template and redirects to the template_tag
method.
B’s output will be <template></template>
, not <h1>Hello</h1>
.
Tagged blocks
We need a way to communicate our calling intentions (whether we want to render or whether we want the tag) with stacked overrides that can be passed up the chain from one override to the next. But overrides don’t know if their super method is another override, so whenever we send a call up the super chain, it can’t interfere with the interface of the actual user-defined template
method. We can’t use an argument otherwise framework users would need to write their own template methods to handle that argument.
There is, however, one special argument that you can always send to any method: the block argument. Whenever we want to actually render the template that the user defined, we can call template
with a block that we’ve tagged with an instance variable so our override knows it’s us.
Phlex renders views by calling template
from the call
method. We can update that method like this.
def call
...
# Create a no-op Proc if we don't already have a block
block ||= -> (*args) {}
# Tag the block with an instance variable
block.instance_variable_set(:@render, true)
# Call template with our tagged block
template(&block)
end
Now we can update our TemplateOverride
to look for this instance variable. If it’s true
, we want the template
method defined by the user, otherwise we must want to render a <template>
tag.
module TemplateOverride
def template(**kwargs, &block)
if block.instance_variable_get(:@render)
super(&block)
else
template_tag(**kwargs, &block)
end
end
end
This technique works, even when two or more overrides are stacked, because the tagged block gets passed up all the overrides. But it has another problem.
Sometimes we want to define a view template that depends on the super view template.
Here’s an example:
class AbstractForm < Phlex::View
def template(&)
form do
input type: "hidden", name: "auth", value: auth_token
yield_content(&)
end
end
end
class ConcreteForm < AbstractForm
def template
super do
input type: "text", name: "foo"
...
end
end
end
Our ConcreteForm
defines a template
method that depends on the super template
method, calling super
with a block. The problem is, that block hasn’t been tagged so, when it hits the override, it is redirected to the template tag method. ConcreteForm
now outputs <template></template>
.
Okay, okay. But we’ve come so far! Can’t we hack our way around it? Yes we can. Phlex::View
could provide an alternative super
method for use within a template.
def super_template(&block)
block.instance_variable_set(:@render, true)
method(:template).super_method.super_method.call(&block)
end
Our new super_template
method has the same interface as the super
keyword, but it tags the block first and then jumps two places up the inheritance tree to hand it to a method that is either the intended super target or an override that will pass it on up the tree.
At this point, we’re kind of back where we started in the sense that we have another snowflake to document and explain to framework users — and this one is even more difficult to explain.
Bound method equality
Ruby allows you to compare bound Method objects for equality. One option I explored was having the override compare itself to its super.
method(__method__) == method(__method__).super_method
Unfortunately, it appears that stacked prepended methods have different bindings so it is not possible to compare them like this.
The ol’ Switcheroo
Throw everything else away and start again: what if we let the user define a template
method, but then we rename it to something else and rename template_tag
to template
at the last minute. We can use its new name when we want to render the view (since this happens in call
and is never exposed) and super
will still work as normal.
Additionally, we can get back to the original performance profile for these methods as there is no indirection and no unnecessary Hash
object allocations.
There is, however one final problem. We can’t permanently rename the methods on the classes. If we did, calling super
from template
in a subclass would point to the renamed tag method in its super class rather than the original. What we can do is rename the methods on the singleton class instead. That way, template
will always be defined as a sub-method of the original template method in the super class rather than the renamed template tag method.
Back in our call
method, we can open the singleton class and use alias_method
to alias view_template
to the original template
. Then, we can alias template
to template_tag
.
def call
...
class << self
alias_method :view_template, :template
alias_method :template, :template_tag
end
view_template(&block)
...
end
For slightly better performance, we can actually omit this first alias and save the original template
method to a variable instead.
def call
...
view_template = method(:template)
class << self
alias_method :template, :template_tag
end
view_template::(&block)
...
end
Here we call the view_template
variable using the ()
method, which is an alias for call
.
The final cost of all this is about 5.73% worse performance than before. I expect this is because a cache in Ruby is invalidated when we do the alias on our singleton class. That’s an unacceptable performance cost unfortunately.
Compilation
In the future, Phlex views will be transparently compiled by default. I’ll share more about that in the next few weeks but the TL;DR is the compiler gives us an opportunity to completely optimise this problem away. We can use SyntaxTree to target template
calls inside Phlex views and replace them with calls to template_tag
instead.
My goal for the Phlex compiler is to speed up view rendering without inventing a new language. That means I don’t want the compiler to let you do something you otherwise couldn’t do without it. But since we have a solution in pure Ruby, this optimisation is now technically in scope.
It’s still a tough call whether it would be worth the added complexity in the end. Unfortunately, you’ll have to continue to use template_tag
not template
when you want a <template>
tag. At least for now.
Maybe one day I’ll find a new name for the template
method instead. 🤷♂️