Tim talks “Views, from the top”
I had the honour of speaking last month at RubyConf AU 2019, where I got to share a topic I’m especially excited about: building better views!
For those who’d like to follow along at home, this post includes the video, the slides, and a full transcript of the talk!
So please, set aside 20 minutes, and join me for a very important meeting:
Let’s get started!
Thanks for coming along to this meeting, everyone. I know your time is precious, and for some reason this meeting room is booked up solid today! I think we have maybe… 19 minutes left, so let’s get right to it.
We’re here because enough of us have decided: it’s time we do something about our views.
Yes, I’m talking about the humble server-rendered view. These things continue to be important in our apps, and while they may not trendy, I think they remain pretty important industry-wide.
But for something so essential, things have gotten out of hand. It’s why we’re in this meeting today…
Because our views are just a mess. Yes, I know, I know: views are kind of special…
They have markup and Ruby code intermingled… and they’re a place where both Rubyists and front-enders go to work together… and this does makes them particularly prone to becoming messy. But that’s no excuse! We can and should do better!
But we can’t expect to continue writing our views in the same way and somehow get different results.
And the truth is, we’re writing views in pretty much the same way as they shipped in Rails 1.0, with controllers, templates, and helpers.
It’s kind of damning if you think about it. Nearly 15 years has passed since then: have we really learnt nothing in that time about how to write better views?
Rails 1.0 shipped in 2005.
That’s the very same year in which Vin Diesel shipped The Pacifier.
Now, I know this wasn’t a significant moment in cinema history, but as a moment in time, it’s very interesting, because since then, we’ve seen Mr Diesel go on and relentlessly iterate:
He becomes faster and more furious with every subsequent release! Over this whole 15 year period, too.
But over this time, our views have barely changed. Now that’s what makes me furious.
And I don’t think it’s just that we’re writing bad code. I think it’s the whole approach to views that has issues.
Let’s review the situation.
We’ll start with controllers.
So while the HTTP processing layer is where we often want to render views, it certainly shouldn’t be their forever home. That’s mixing too many responsibilities together.
Helpers. Well, what can I say.
It might look like we organise them, with all those modules, but they all get mixed together in the end anyway, into some global soup of implicitly interdependent methods that we have no control over.
I’ve asked around the team, and nope, none of you feels good about writing a helper. It’s just a thing we put up with when we feel there’s no better option.
For a while there we felt like some add-on decorator gems might be that better option.
And they did help a bit, but it turns out they occupied this kind of uncanny valley of view libraries: helpful in many ways, yes, but because they’re not native to the view system, they lacked certain abilities.
And since they’re an opt-in addition, there was always just a little too much friction in using them.
And the lack of good options in the end meant we often wound up with too much logic cluttering our template files, where we’d much rather be focused on markup and presentation.
So in the end, it’s still just too hard to write good view code. And really, what we should be striving for is for our view code to be good anything code.
Just because we’re writing for views doesn’t mean we shouldn’t be able to following all the good guiding principles that we apply elsewhere.
So let’s fix this, let’s build a better view system. And of course, we should publish it as a gem.
We’ll call it “dry-view.”
Now, I appreciate you taking the time to think this over and sending through all your requirements for better views.
After looking them over, I think there’s a way we can bring them all together into a nice, coherent system.
1. Views as objects
So first up, you said that a view needs to be a thing, an object, something we can address directly, and pass around.
So lets start by defining a class for a view, inheriting from Dry::View
class Show < Dry::View
end
view = Show.new
And yes, it looks like we can instantiate this typical Ruby style, so now we have our view object!
We’ll make it so our views can have dependencies injected too.
This means they can collaborate with whatever parts of the application they might need to do their work.
Here we’re building a view for showing an article, so we’ll want to give it an article repository:
class Show < Dry::View
attr_reader :article_repo
def initialize(article_repo:)
@article_repo = article_repo
end
end
The view can use this to load its article from the database.
And we can pass it in when initializing the view, just like so.
view = Show.new(article_repo: repo)
2. Explicit template locals
So now we have views as objects, the next thing you said was that we should be clear about declaring what values we make available to our templates. We’re crossing a boundary at that point, so we should be purposeful about it.
Let’s solve this by adding exposures to our view. In this case, we want to expose a single article to our template:
class Show < Dry::View
attr_reader :article_repo
def initialize(article_repo:)
@article_repo = article_repo
end
expose :article
end
Inside these exposures we can do whatever we need to prepare their values. In this case, we’ll expect a slug to be passed in, and then we’ll use it to load our article from the database:
class Show < Dry::View
attr_reader :article_repo
def initialize(article_repo:)
@article_repo = article_repo
end
expose :article do |slug:|
article_repo.find_by_slug(slug)
end
end
And, if we’re going to be passing this to a template, let’s give ourselves a place to configure its name:
class Show < Dry::View
config.template = "articles/show"
attr_reader :article_repo
def initialize(article_repo:)
@article_repo = article_repo
end
expose :article do |slug:|
article_repo.find_by_slug(slug)
end
end
3. Templates!
This brings us to our next requirement: we still want to build our views on templates! We’re comfortable working with them, and they’re still a great place for our front-end developers to go to work.
So here’s our template:
h1 = article.title
We’re writing this with the slim template language. We’re creating a header for our page, and then filling it with the title from the article
value we exposed before.
With the template in place, we can go back to our view object and #call
it, passing in an article slug, and look, here we have our rendered view!
view = Show.new(…)
view.call(slug: "together-breakfast").to_s
#=> "<h1>Together breakfast</h1>"
So those are the basics, but there’s more we can do.
3a. (Simple) templates
While it’s was clear we want templates, what we don’t want is to get mired in the same morass of complex template logic that’s weighed us down before.
We want our template code to remain simple, and push as much logic as we can to other places in our view system.
Let’s take a look at some aspect of this logic now. So this is a simple example, but here’s how we might have used a helper before, to render an article’s body:
h1 = article.title
== markdown(article.body)
The downside to using helpers like this is that it’s something we have to remember to do in every template that deals with articles, and then the accumulation of 100s of these things, for a whole range of different concerns, is what got us into our mess in the first place.
Now this, this is is more like what you wanted to see in templates:
h1 = article.title
== article.body_html
We’re seeing the view-specific behaviour hanging off the values we’re passing to the template in the first place.
4. View logic on decorated values
Yes, what we’re seeing here is a kind of decorator object, something to wrap the value we’re passing from our exposures and and carry the view-specific behaviour we want to use.
So let build one of these up up. We’ll call these “View Parts”, and this one here will carry the view behaviour for articles.
class Parts::Article < Dry::View::Part
end
This gives us a place to write this #body_html
method, right here alongside the article’s own data:
class Parts::Article < Dry::View::Part
def body_html
render_markdown(body)
end
private
def render_markdown(str)
# …
end
end
So now we’ve simplified our templates, and we’ve taken care of the other side of that equation, which is to decorate our values with objects providing their own specific view behaviour.
5. View facilities are automatic
OK, this next requirement’s an interesting one. I can see you’ve gone back and looked at how we’ve used decorators before, and you realised a decorator is only good if we use it.
And you want to see here is a system that nudges us towards the right abstractions every time, with things like these decorators being made available automatically, from the beginning.
And I think we found a way to make this work!
If we configure a part_namespace
on our view, then we can make it that the value from every exposure gets automatically wrapped in a matching part object!
class Show < Dry::View
config.part_namespace = Parts
# Wrapped with `Parts::Article`
expose :article do |slug:|
# ...
end
end
So all we need to do is create a part class, and then it’ll get picked up automatically. This way, we’ll be much more inclined to turn to parts when we need to add view logic.
6. View facilities are integrated
OK, I get what you’re saying here: if we’re going to do our own take on decorators, we need to make it worthwhile, and close the gap we felt when using add-on systems before.
So you want to see our view parts become fully integrated with the rest of this view system, so anything you can do in a template, you should be able to do in a view part, too.
Let’s find a way for this to work.
We’ll go back to our article template here. Say we wanted to put a sharing widget at the bottom:
h1 = article.title
== article.body_html
.share-widget[
data-share-url=article.url
data-share-title=article.title
data-share-body=article.body_preview_text
]
This thing has some pretty tedious markup, and we have to pass in all the different aspects of the article that need to appear in the widget.
Now, at this point we might realise this is a fairly self-contained component, and also something we want to reuse, so we can extract it into a partial:
h1 = article.title
== article.body_html
== render(:share_widget,
url: article.url,
title: article.title,
preview_text: article.body_preview_text)
Oh yes, we have partials here too, which we can call on by using the render
method.
But with a partial, this leaves us with all this awkward attribute passing every time we want to use it.
So how about this: what if we could render partials from within our view parts, just like we can in our templates? Then we could move everything into the article’s part class, where we could hide away the particulars of the rendering:
class Parts::Article < Dry::View::Part
def share_widget
render(
:share_widget,
url: url,
title: title,
preview_text: body_preview_text,
)
end
end
This means we end up with a much cleaner, more intention-revealing template, like this:
h1 = article.title
== article.body_html
== article.share_widget
Now this is the kind of benefit we want to see from having our view facilities be fully integrated!
7. View logic on specific templates
.Things are feeling pretty good now, but I see a few of you pointing out a gap: with view parts, we worked out an approach for providing view-specific behaviour with particular values, but what about behaviour that might need to go along with a specific template or partial?
I noticed that you shared an example where something like this could really help, like here with this related_article
partial:
== render(:related_article, article: article)
Now at this point, this partial rendering looks pretty innocuous… but look inside, and gosh, this is some curly looking template code:
- show_author = \
defined?(show_author) ? show_author : false
- link_prefix = \
defined?(link_prefix) ? link_prefix : "Related: "
.related-article
a href=article.url = "#{link_prefix} #{article.title}"
- if show_author
.author …
There’s some logic there that I just don’t like to see in templates.
I mean, I can see what we’re trying here, making it so we can provide some default values if the partial is rendered without these locals passed in
But this really feels like the wrong place to be doing this… wait… I wrote that code, didn’t I? 😬
Let me just check something…
OK OK, this one’s on me! 😂
But I’m glad we’re working this out together, because you thought of a concept to clean this all up: alongside our parts, we’ll add scopes.
So let’s build up a scope for our related article partial now:
class Scopes::RelatedArticle < Dry::View::Scope
end
While parts deal with a single value, the idea for scopes is to deal with templates, so we’ll make it that they can access to the template’s complete set of locals. This gives us the chance to write methods that can properly handle missing locals and provide default values.
Like this one, for whether to show the author:
class Scopes::RelatedArticle < Dry::View::Scope
def show_author?
locals.fetch(:show_author, false)
end
end
And this one here, which provides a default for the prefix text, and then combines it with the article title for us to return the link text in full:
class Scopes::RelatedArticle < Dry::View::Scope
def show_author?
locals.fetch(:show_author, false)
end
def link_text
prefix = locals.fetch(:link_prefix, "Related:")
"#{prefix} #{article.title}”
end
end
This means we can say goodbye to all that founder’s code of mine, and and up with this much nicer template, using the scope’s methods to keep things tidy:
.related-article
a href=article.url = link_text
- if show_author?
.author …
Now to make sure our partial is rendered with our custom scope, we want to build the scope first, still using the same name, and still passing our article to it:
== scope(:related_article, article: article)
Then we can tell the scope to render itself and it’ll pick up that existing partial:
== scope(:related_article, article: article).render
So our requirements are feeling pretty complete. We’re done, right?
8. Common helpers 😱
Wait, some kind of helpers, really? After everything I said?
OK, you make a fair case: it’s likely there’ll be a small number of things that are truly common to all views, and all their templates, some kind of baseline rendering context.
So let’s take that idea and make it the name of this facility: a context object:
class Context < Dry::View::Context
end
Just like our views, the context will need to interact with other aspects of our app. We’ll make those available by injected dependencies here too.
In this case we’ll pass in a static assets manifest, the kind of thing that Webpack generates for us:
class Context < Dry::View::Context
def initialize(assets:, **args)
@assets = assets
super(**args)
end
end
Then we’ll make a method to work with this manifest and give us the full path for any given asset:
class Context < Dry::View::Context
def initialize(assets:, **args)
@assets = assets
super(**args)
end
def asset_path(asset_name)
@assets[asset_name]
end
end
And to tie everything together, we’ll make it so that whatever methods we define on this context class are available to use within any template:
img src=asset_path("header.png")
h1 = article.title
== article.body_html
== article.share_widget
And since we’re all about our view rendering facilities being fully integrated, our context methods are available in both parts and scopes too!
class Parts::Article < Dry::View::Part
def feature_image_url
url = value.feature_image_url
url || asset_path("article.png")
end
end
This means we can continue keep our view logic in the best possible place, while still taking advantage of every aspect of the view system.
And I like this point you make, too: while these might feel like helpers in regular usage, that’s the only thing they share in common with the helpers that bogged us down before.
In practice, these are just regular methods, defined in a regular class, with its own state, and its own clear dependencies. This is something we can keep on top of, and much more easily refactor over time.
A complete system
Well, now I think we made it.
And not just because we’re out of space on our giant whiteboard, I think we’ve given this system everything it needs.
And we did it by working through our requirements, one by one, and using them to help us discover the concepts around which we should organise our views:
What did we learn?
After all of this, I think we should step back and ask ourselves: what did we just learn, through doing all of this?
Well, I think we’ve rightly recognised that views are complex.
They’re complex enough that templates and helpers are just not up to the task of handling them anymore.
In working through our requirements, we created a whole 6 different places to keep our view logic, with each one serving a distinct, necessary role.
This wasn’t architecture for the sake of it, either.
This really feels like minimum viable views. Each one of these structures exists because it helps us write better view code.
Take one of these away, and our view code will worsen.
And the benefit of having all of these structures from the get-go is that they make the easy thing to do also the right thing to do.
And when those things align, that’s when the quality of our code really levels up.
Let’s just look at our outcomes here:
By allowing our views to be represented as standalone objects, and allowing those objects to be injected with other parts of our app, we’ve enabled a clean separation of concerns.
We’ve disentangled our views from the rest of our app, letting them stand apart in a truly distinct layer.
Then by moving the bulk of our view logic into part and scope objects, at last we’re properly encapsulating this logic, combining our custom behaviour with the data that it relates to
And all the different objects in our view layer we designed to be immutable: once they’re initialized, their state will never change. This makes them easier to understand.
And it gives us a much clearer flow of data throughout all of our views.
And this makes for a view layer that is far easier to test.
We can now unit test our views more than ever before, and at whatever level of granularity makes the most sense.
If we have parts or scopes with some complex behaviour, we can unit test them directly.
Or if we want to test a view in full, we can build a view object, pass some test values as input, and make assertions against the generated HTML. This can be done in complete isolation, no HTTP request or database access required.
And it’s this ease of testing that really brings home the lesson here:
That better view code is really just better object-oriented code, full stop.
This is really something we should be striving for in every aspect of our applications, and views should be no exception.
And better object-oriented code is what gives us views that are a joy to write again.
Views we can be proud of. Views that we can trust will work the way we intend.
And this is what helps us build better applications.
Whether other folks use dry-view or not, hopefully these - ideas - at least, can help push things in a better direction.
I also think what we’ve done here is about more than just apps.
It’s about creating a better, stronger Ruby ecosystem.
In building a new view system, we’ve created choice. Choice creates diversity.
And through diversity, we encourage the healthy sharing of ideas that drives our community forward.
And importantly, what we’ve created is open to everyone, because this is a standalone system. As long as you use Ruby, you’re good to go. Use it from Rails, use it from Sinatra, use it on its own, it’s all good!
And once you’re using it, how you use it is up to you. Render HTTP responses, sure, but also use it for emails, or for background rendering. Because it’s standalone, it’s flexible, and you can integrate it however you please.
And in building this, what we’ve done is innovated. And innovated in a space that’s been largely stagnant for 15 years!
We felt the existing tools were getting in the way of us writing well-designed view code, so we did something about it.
And we did it in what turned out to be ~900 lines of code. None of this was rocket science – anyone could do it!
So what we’ve shown ourselves, and our community, is that although the Ruby story may be considered settled to some, there’s always the opportunity to take another look at something fundamental and offer a new and interesting take.
So what did we do? We invented dry-view, and gave ourselves a chance to finally write some better views.
And with that, I think someone else needs the meeting room, so I think we can wrap this up!
As I mentioned at the conference: I’m Tim, please say hello, I’d love to talk views or really anything else.
And you can find out about dry-view and more on the dry-rb site.
Thanks for coming everyone, and I’ll see you at tomorrow’s standup 😉