Under the hood
You’ve successfully installed Phlex and rendered your first component. Now let’s take a moment to understand what’s happening behind the scenes. To do this, we’re going to build a miniature version of Phlex from scratch.
It won’t have advanced features, performance optimizations or HTML safety, but I think it’ll give you a good sense of things.
Buffers and hirearchy
We’ll start by creating a Component
class with a @buffer
instance variable. Phlex uses a mutable String for its buffer, but we’ll use an Array since it’s easier to debug.
class Component
def initialize
@buffer = []
end
end
Now we want to be able to render HTML tags. Let’s start with <div>
and we can add a few more later.
def div
@buffer << "<div>"
yield if block_given?
@buffer << "</div>"
end
Our div
method first pushes an opening <div>
tag onto the buffer, then yields itself to the block if a block is given. Finally, it pushes a closing </div>
tag onto the buffer.
Let’s add one more method so we can render our components to a string.
def call
view_template
@buffer.join
end
This method, call
, first calls the view_template
method (which we haven’t defined yet) then it joins the buffer into a single string and returns it.
The whole class should look like this:
class Component
def initialize
@buffer = []
end
def call
view_template
@buffer.join
end
def div
@buffer << "<div>"
yield if block_given?
@buffer << "</div>"
end
end
Now we’re ready to create a component. Let’s make a simple HelloWorld
component — though we’re not quite ready to say "Hello World"
just yet. Instead, we’ll render a couple of nested divs.
class HelloWorld < Component
def view_template
div {
div
}
end
end
This HelloWorld
component inherits from our abstract Component
class and implements the view_template
method that we called from the call
method before.
Let’s see what it looks like when we call
our HelloWorld
component to render it:
puts HelloWorld.new.call
You should see the following output with one div nested inside another:
<div><div></div></div>
Ruby handled the hierarchy for us. We got part way into the first div when we yielded to the block which started a new div. Since there was no block given to this inner div, it closed immediately yielding control back to the original outer div, which then closed.
Handling text content
Let’s add the ability to render text content inside our divs. We’ll update the method to accept an optional content
argument. If content
is provided, we’ll push it onto the buffer. Otherwise, we’ll yield to the block as before if a block is given.
def div(content = nil)
@buffer << "<div>"
if content
@buffer << content
elsif block_given?
yield
end
@buffer << "</div>"
end
WARNING
This implementation doesn’t handle HTML safety. In a real-world scenario, you’d want to escape any HTML content to prevent cross-site-scripting attacks, where a user could inject malicious code into your page by providing it as their name, for example.
Let’s go back to our HelloWorld
component and add some text content to the inner div.
class HelloWorld < Component
def view_template
div {
div("Hello, World!")
}
end
end
Now when we render our HelloWorld
component, we should see the following output:
<div><div>Hello, World!</div></div>
Handling attributes
Let’s add the ability to render attributes on our divs. We’ll update the method to accept an optional attributes
hash. We can do this by accepting a double splat argument, **attributes
, which will collect all the keyword arguments into a new Hash
.
Now we want to start by pushing just the opening <div
onto the buffer. Then we’ll iterate over the attributes
hash and push each key-value pair onto the buffer. Finally, we’ll push the closing >
onto the buffer and continue as before.
def div(content = nil, **attributes)
@buffer << "<div"
attributes.each do |key, value|
@buffer << " #{key}=\"#{value}\""
end
@buffer << ">"
if content
@buffer << content
elsif block_given?
yield
end
@buffer << "</div>"
end
Let’s go back to our HelloWorld
component and add a class attribute to each of the divs.
class HelloWorld < Component
def view_template
div(class: "outer") {
div("Hello, World!", class: "inner")
}
end
end
Now when we render our HelloWorld
component, we should see the following output:
<div class="outer"><div class="inner">Hello, World!</div></div>
Nesting components
For the final step, let’s add the ability to nest components inside one another.
To do this we’ll need the ability to pass a buffer to a component when we come to rendering it. Let’s remove the original initialize
method and update the call
method to accept a buffer argument instead.
We’ll also accept a block (&
) and pass it to the view_template
method.
def call(buffer = [], &)
@buffer = buffer
view_template(&)
@buffer.join
end
The buffer still defaults to an empty array, but now we can pass in a buffer from the outside. The block allows us to yield content in our template.
Let’s define a render
method that takes a component and renders, passing the buffer and the block.
def render(component, &)
component.call(@buffer, &)
end
The whole Component
class should now look like this:
class Component
def call(buffer = [], &)
@buffer = buffer
view_template(&)
@buffer.join
end
def div(content = nil, **attributes)
@buffer << "<div"
attributes.each do |key, value|
@buffer << " #{key}=\"#{value}\""
end
@buffer << ">"
if content
@buffer << content
elsif block_given?
yield
end
@buffer << "</div>"
end
def render(component, &)
component.call(@buffer, &)
end
end
Now let’s create a new component called Card
:
class Card < Component
def view_template(&)
div(class: "card", &)
end
end
Back in our HelloWorld
component, let’s update it to render our Card
component:
class HelloWorld < Component
def view_template
div(class: "outer") {
render Card.new do
div("Hello, World!", class: "inner")
end
}
end
end
The output should now be something like this (without newlines and indentation):
<div class="outer">
<div class="card">
<div class="inner">Hello, World!</div>
</div>
</div>
Plain text
In about 30 lines of code, we’ve build a simple component abstraction for rendering HTML. We can render nested divs with content and attributes and we can nest components inside one another.
However, what we can’t do is render text without wrapping it in a div. Let’s fix that with a new method called plain
that simply pushes content onto the buffer.
def plain(content)
@buffer << content
end
Now we can update our HelloWorld
component to render the text directly inside the Card
component:
class HelloWorld < Component
def view_template
div(class: "outer") {
render Card.new do
plain "Hello, World!"
end
}
end
end
Supporting advanced DSLs
What if we want to our Card component to expose an interface for interacting with it. For example, we might want to set the title.
Let’s start by updating the Card component as if this worked and then we’ll get it working.
class Card < Component
def view_template(&)
div(class: "card", &)
end
def title(content)
div(content, class: "card-title")
end
end
This title
method perfectly encapsulates the card title. But how can we call it at just the right moment so that it pushes to the buffer in the right place?
The trick here is to yield the component to the block that’s passed to view_template
from the render
method. This will allow us to pick up the card component when passing in the block.
class HelloWorld < Component
def view_template
div(class: "outer") {
render Card.new do |card|
card.title "Hello, World!"
end
}
end
end
To get this to work, we’ll need to find the point where we yield and make it yield(self)
. We could do this in the div
method, but there’s a better way.
When the block comes into call
, we can wrap it in a new block that yields self
. This way, it will always yield the component instance even if we forget to.
def call(buffer = [])
@buffer = buffer
view_template { yield(self) if block_given? }
@buffer.join
end
This is a little mind-bending. We’re now always passing a block to view_template
, that yields self to the block that was passed to call
if a block was passed.
The final Component
class should look like this — just 33 lines of Ruby:
class Component
def call(buffer = [])
@buffer = buffer
view_template { yield(self) if block_given? }
@buffer.join
end
def div(content = nil, **attributes)
@buffer << "<div"
attributes.each do |key, value|
@buffer << " #{key}=\"#{value}\""
end
@buffer << ">"
if content
@buffer << content
elsif block_given?
yield
end
@buffer << "</div>"
end
def plain(content)
@buffer << content
end
def render(component, &)
component.call(@buffer, &)
end
end
Our HelloWorld
component with the card.title
interface should now render the title correctly:
<div class="outer">
<div class="card">
<div class="card-title">Hello, World!</div>
</div>
</div>