If you’ve ever worked on a Rails app, you’ve probably run into problems with the view layer turning into a mess as the app gets older or the team gets bigger. Rails attempts to tame this problem with solutions like strict locals, but I’ve found them to be cumbersome and view them as a worse implementation of Ruby method definitions.
Fortunately component libraries like Phlex and ViewComponent use Ruby method definitions to create a more sane boundary between application code and views, but using them as controller views hasn’t been straightforward and requires a lot of boilerplate.
That’s where Superview comes in—Superview is a gem that makes it possible to build Rails applications from the ground-up using nothing but components. If a class instance responds to renders_in
and has attr_writer
methods, Superview can assign the controllers’ instance variables to the component and render it if the class name matches the action name.
Before we dive into Superview, let’s understand how good we have it in Rails with view templates. Today a typical Rails blog posts controller action might look like this:
# ./app/controllers/posts_controller.rb class PostsController < ApplicationController before_action { @post = Post.find(params[:id]) } # Implicitly renders ./app/views/posts/show.html.erb end
Out of the box, Rails is configured to look for views in the ./app/views
directory for the requested format. The major accomplishment is the code we didn’t have to write to find and render the template. Can we accomplish the same thing with component views? Let’s find out.
Phlex and ViewComponent views can be rendered in a Rails controller today, but it requires a bit of boilerplate that starts to feel really repetative as the number of actions grows in your apps.
# ./app/controllers/posts_controller.rb class PostsController < ApplicationController before_action { @post = Post.find(params[:id]) } def show respond_to do |format| # Rendering a component without Superview đź‘Ž format.html { render PostComponent.new(post: @post) } end end end
The lines of code start to add up when you multiply these lines of code across each action in your Rails application. There has to be a better way, right?
Since we’ve been spoiled by implicit template rendering in Rails, nobody really wants to write this boilerplate every time they want to render a component. That’s where Superview can help, and just for fun let’s make peoples’ heads explode 🤯 with an example of inline views in a controller.
# ./app/controllers/posts_controller.rb class PostsController < ApplicationController include Superview::Actions before_action { @post = Post.find(params[:id]) } # The `Show` class maps to the `show` action đź‘Ť class Show < ApplicationComponent attr_writer :post def view_template h1 { @post.title } article { @post.body } end end # The `Edit` class maps to the `edit` action đź‘Ť class Edit < ViewComponent::Base attr_writer :post erb_template <<~ERB <h1>Editing Post</h1> <form> <input type="text" value="<%= @post.title %>"> <textarea><%= @post.body %></textarea> </form> ERB end end
With Superview installed, the Show
and Edit
views will render for their corresponding actions.
Of course, you’ll need to install Superview to get the controller above working with Superview::Actions
. You can add it to your Rails app by running:
Run from the root of your Rails app
bundle add superview
Don't forget to restart your Rails server
Restart your Rails server and you should be good to go.
I’ve found that inline views are polarizing in Rails. I love them because I can see my view code right next to my controller actions and it feels like Sinatra, but lots of people want their views in the corresponding ./app/views/posts
directory. Fortunately for those who prefer to keep their views far, far away from the controller, it’s possible to move your views into their own folder.
Let’s move our inline views from above into the ./app/views/posts
directory.
tree -R ./app/views/posts
|-- show.rb
|-- edit.rb
There’s a bit of Rails app configuration we need to do to get these views automatically loading with Zeitwerk, which is the code loader Rails uses to make your application work.
app/views
to Rails autoload pathsWe need to add the following to our config/application.rb
file so Rails autoloader, Zeitwerk, picks up the view files.
# ./config/application.rb module YourApp class Application < Rails::Application config.autoload_paths += Rails.root.join("app/views") end end
Zeitwerk will automatically load the files in the ./app/views/posts
directory, but only if we namespace the view in the Posts
module. Here’s what view files look like, namespaced, in the ./app/views/posts
directory.
# ./app/views/posts/show.rb class Posts::Show < ApplicationComponent attr_writer :post def view_template h1 { @post.title } article { @post.body } end end
Don’t forget the edit view!
# ./app/views/posts/edit.rb class Posts::Edit < ViewComponent::Base attr_writer :post erb_template <<~ERB <h1>Editing Post</h1> <form> <input type="text" value="<%= @post.title %>"> <textarea><%= @post.body %></textarea> </form> ERB end
If you’re wondering, “Why Posts and not Post”, it’s because the singular name Post
is likely taken by a model. Pluralizing the namespace for views makes it less likely that you’ll run into a situation where a model and view namespace have the same name.
Now there’s just one more thing…
Posts
module in the controllerWe need to include the Posts
module in the controller so that the view files are loaded by Zeitwerk.
# ./app/controllers/posts_controller.rb class PostsController < ApplicationController include Superview::Actions include Posts before_action { @post = Post.find(params[:id]) } end
The include Posts
line loads the Show
and Edit
constants into the PostsController
namespace, which means Superview can resolve them for the show
and edit
actions. If you have other controllers that need to render these views, you’d include the Posts
module in those controllers as well.
Good abstractions always have escape hatches for situations where the implicit abstraction won’t work, or needs a little help. Let’s look at how a few of those edge cases are handled.
For example, let’s say we need to render a JSON format in the same controller; we can use the render
method to render the JSON response.
# ./app/controllers/posts_controller.rb class PostsController < ApplicationController include Superview::Actions include Posts before_action { @post = Post.find(params[:id]) } def show respond_to do |format| # The `component` method returns the component class for the action format.html { render component } # Renders the `show.json` view in the `./app/views/posts` directory format.json end end end
The component
method will return the component class for the action, which is Posts::Show
in this case. If you need to render a different component, you can pass the component class to the render
method.
One common pattern for the update
action is to render the edit
view if there’s an error so the user can see form errors and update their input. We can render the edit
component in the update
action by passing the Edit
class to the component
method.
# ./app/controllers/posts_controller.rb class PostsController < ApplicationController include Superview::Actions include Posts before_action { @post = Post.find(params[:id]) } def update if @post.update(post_params) redirect_to @post else render component Edit end end end
If you have a controller that only needs to render a component for a single action, you can override the component_view
method to return the component class for that action.
# ./app/controllers/posts_controller.rb class PostsController < ApplicationController include Superview::Actions before_action { @post = Post.find(params[:id]) } # Set the constant in the controller to the constant # of the view you wish to render. Show = Posts::Show end
Superview makes it possible to build Rails applications from the ground-up using nothing but components. If it responds to renders_in
and attr_writer
, Superview can assign the controllers’ instance variables to the component and render it if the class name matches the action name.
You can start using Superview in new and existing Rails apps by running:
Run from the root of your Rails app
bundle add superview
And then including the Superview::Actions
module in your controllers. The module is backwards compatible with existing Rails views, so you can start using components in new controllers and views as you see fit.
Now you have another tool to build more maintainable Rails applications with components and automatically render Phlex and ViewComponents for controller actions with Superview.
If you like what you read and want to see more articles like this, please consider using Terminalwire for your web application’s command-line interface. In under 10 minutes you can build a command-line in your favorite language and web framework, deploy it to your server, then stream it to the Terminalwire thin-client that runs on your users desktops. Terminalwire manages the binaries, installation, and updates, so you can focus on building a great CLI experience.