Do Basecamp/Hey use ViewComponents?

I am really curious to know what is the Rails way: do you use ViewComponents for Basecamp/Hey?

Or you still use views and partials?

3 Likes

I’m not a part of Basecamp, but based on observing their philosophy/code snippets/videos over a long time, DHH has generally been in favor of creating a rich web of models and calling into them from view templates and partials. And against presenter-type objects in your controllers. A lot of rails has been built with that in mind (automated collection partials, jbuilder, etc are all to enhance template usability).

3 Likes

IMHO ViewComponents are largely just some extra sugar over a combination of helpers and partials and giving it an object-oriented veneer. Take the example from the ViewComponents documentation:

class ExampleComponent < ViewComponent::Base
  erb_template <<-ERB
    <span title="<%= @title %>"><%= content %></span>
  ERB

  def initialize(title:)
    @title = title
  end
end

An alternative using helpers and partials might be:

module MyHelpers
  def example(title:, &)
    render(layout: "components/example", locals: { title: }, &)
  end
end

The a partial at components/_example.html.erb that is:

<span title="<%= title %>"><%= yield %></span>

I’m not sure the OO approach really has any advantage over the procedural approach given that the process of generating the content is procedural. The docs for ViewComponents claim they make testing easier but it’s just as easy to create tests for the helper IMHO.

At the same time there isn’t anything bad about ViewComponents so if that just jives with how you think better I see no reason not to use them. But the “Rails way” seems to be more built around helpers and partials and the difference between not much IMHO.

1 Like

I think building one object that contains every value that goes into a view is a useful practice, regardless whether you use templates or serialize that object straight into json. It’s useful because when you call directly into models from various templates and partials, you make your view dependencies unmanageable, and scattered across many files. It’s very hard to collect them back and understand what’s needed to render a view. This makes web responses harder to optimize and cache. Instead, if you build a single object first, and then use only that object in your template, you will be able to have your entire response dependencies in one place, which makes it easier to fully optimize sql and other data acquisition necessary to render a response.

That said, I think ViewComponent adds the (imo) unnecessary requirement of having to write templates in ruby. A plain Ruby object combined with standard Rails templates works perfectly fine for this. I recommend portrayal (excuse the plug :slight_smile: ) for lightweight view-objects.

An advantage of ViewComponent is that for more complex components you can extract methods that are scoped to the component instead of being a global helper method.

2 Likes

Templates in ViewComponent can be written in erb, haml, slim etc.

Yeah sorry, I meant embedded into ruby code, not literally written in ruby.

That’s a great point if you build views this way. In my approach (PORO objects with values already formatted for view) you rarely need helpers, and if you do, it’s for something globally applicable.

ViewComponents don’t require putting the HTML into a ruby file - they can go into a .html.erb just like a Rails view or partial (this is how it is commonly done and when you generate a component with their generator this is what happens - putting the ERB into a ruby file is extremely uncommon). The example way above showing a span with a title isn’t complex enough to justify ViewComponents so while it shows how you could build one, it’s not typical of how you would use one.

The one thing View Components makes easier than using partials or helpers in Rails is testing a subset of a view when you have a complex section part of a larger view. Imagine a portion of your view that could have many states and thus many ways it may be rendered.

To test this in View Components, you create the component with whatever input simulates the various conditions (just like you would any normal code, like for a model), but you can assert on the rendered markup, as you would in an integration or system test. So it capitalizes on knowledge you already have to allow effectively a unit test on part of your overall view.

Without View Components I’m not sure how you might test that sort of thing - a system test would be slow and flaky. Perhaps an RSpec view test? Not sure.

1 Like

We do not use ViewComponents at 37signals, and have no plans to do so. Glad to see that they’re working for some people, but in my opinion, you’re usually better off with regular partial templates the majority of the time.

If some views are particularly complicated, we use presenter objects that serve to encapsulate domain object manipulation required by the views. A heuristic there is that if you have helper methods that call other helper methods, you may benefit from a presenter object. But these presenter objects do not call #render directly.

5 Likes

I’ve been utilizing a technique for some time that involves creating classes which piggyback on ActiveModel and have their own template. These classes represent business entities that, while they don’t correspond directly to database tables, play a role within the views.

One of the primary advantages I’ve found with this approach is that it flows well with the rest of the framework. It feels “Railsy”. Surprisingly, despite this, I’ve never seen it documented anywhere.

I’m curious if this approach is used at 37Signals. Has anyone there adopted a similar method, or does anyone have thoughts or insights on this technique?

In the latest project we make an object per view, containing everything a view needs, including form data and urls, and excluding only markup itself. We call them page objects and form objects, in app/pages and app/forms. Some pages have forms in them. They’re all simple read-only POROs with good constructors. The form objects have a convenient method to work with form_with, but are actually renderer agnostic. The entire pages with forms can be dumped as json, and used to serve React, or mobile apps. We were curious how far we can take this, an so far it’s been very neat and pleasant to work with. In our team this seems like a sweet spot where to draw the line between data and presentation.

A fun side-effect, is that in console you can write:

> SettingsPage.new
=> an empty settings page structure

> SettingsPage.from_user(User.first)
=> a filled settings page for this user

Thanks, I stand corrected.

I agree that this is an objective win. You get to test individual pieces of markup with every input combo. I guess the choice is about team composition: if you have a full stack Rails team, the same people own/test backend and markup. If you have a separate front-end team (like in our case), then we prefer to leave markup testing to them, and focus on testing the “page objects” described above. We prefer to keep it “data-only” all the way to the end. This way we were able to build the app’s prototype with page objects rendered via Rails templates, and now seamlessly switched to having the same page objects rendering a React Native front-end.

1 Like

I am wondering how I’d build a Rails app with TailwindCSS without a library in the likes of ViewComponents. I’d probably rely on a lot of tag helpers to abstract the CSS required for recurrent UI elements such as cards, buttons, labels etc. but I don’t know if someone managed to do it at scale.

That’s pretty much what we did. We created our own version of TailwindCSS to keep size down, which we called Fudgeball, then we namespaced the helpers and made all methods start with its name:

Fudgeball::FormHelper
  def fudgeball_form(model: nil, scope: nil, url: nil, format: nil, remote: true, **options, &block)
  end
end

Fudgeball::ComponentHelper
  def fudgeball_button(key, options = {}, html_options = {})
  end
end

Fudgeball::PageHelper
  def fudgeball_navbar
  end
end

And finally we created a custom builder for forms, which could build all inputs already configured for custom Stimulus controllers that were capable of masking, validating, etc values:

class Fudgeball::FormBuilder < ActionView::Helpers::FormBuilder
  def input(attribute, type = "text", options = {})
  end

  def select_input(attribute, choices = nil, options = {}, html_options = {})
  end
end

For some of the more complex components in Fudgeball::ComponentsHelper we built poros:

class Fudgeball::Button
end
class Fudgeball::Alert
end
class Fudgeball::Chip
end

So a form that has a name, email and phone field that needs to validate mandatory and mask would look like this

<%= fudgeball_form model: user, url: admin_design_system_form_path do |form| %>
  <%= form.input :name, :text, required: true %>
  <%= form.input :email, :email, required: true %>
  <%= form.input :phone, :phone %>
  <%= form.submit %>
<% end %> 

And would look like this: Screenshot 2023-07-26 at 10.44.55

3 Likes

Although Tailwind lets you create a CSS class based on its classes, it recommends using the templating system for abstracting components. Without ViewComponents, what DHH says above it pretty common and can work well: helpers for small things, partials for complicated things. You can additional create a helper that renders a partial if your partial requires a lot of parameters.

This approaches scales more or less the same as any approach that uses helpers & partials as the mechanism for server-rendered component extraction. If this approach doesn’t work for you, or you are hitting scaling limits, in my experience ViewComponents will work great as a next step, since it allows for a full abstraction of the server-rendered component: markup, CSS, JS (optional), logic (optional).

1 Like

I have to throw Phlex components into the hat since people who read this will be generally interested about building Rails apps from components. I’ve figured out how to build a Rails app from the ground up with 100% components and zero Erb. I love this approach, but I know it’s highly opinionated and won’t be liked by all, but I think its worth sharing since it’s another way of doing it that I haven’t seen before.

Here’s what a controller looks like:

class PostsController < ApplicationController
  resources :posts, from: :current_user

  class Form < ApplicationForm
    def template
      labeled field(:title).input.focus
      labeled field(:publish_at).input
      labeled field(:content).textarea(rows: 6)

      submit
    end
  end

  class Index < ApplicationView
    attr_writer :posts, :current_user

    def title = "#{@current_user.name}'s Posts"

    def template
      render TableComponent.new(items: @posts) do |table|
        table.column("Title") { show(_1, :title) }
        table.column do |column|
          # Titles might not always be text, so we need to handle rendering
          # Phlex markup within.
          column.title do
            link_to(user_blogs_path(@current_user)) { "Blogs" }
          end
          column.item { show(_1.blog, :title) }
        end
      end
    end
  end

  class Show < ApplicationView
    attr_writer :post

    def title = @post.title
    def subtitle = show(@post.blog, :title)

    def template
      table do
        tbody do
          tr do
            th { "Status" }
            td { @post.status }
          end
          tr do
            th { "Publish at" }
            td { @post.publish_at&.to_formatted_s(:long) }
          end
          tr do
            th { "Content" }
            td do
              article { @post.content }
            end
          end
        end
      end
      nav do
        edit(@post, role: "button")
        delete(@post)
      end
    end
  end

  class Edit < ApplicationView
    attr_writer :post

    def title = @post.title
    def subtitle = show(@post.blog, :title)

    def template
      render Form.new(@post)
    end
  end

  private

  def destroyed_url
    @post.blog
  end
end

Some things you’ll notice:

  • Views are Phlex components embedded in the controller - I really love doing it this way because I have everything in one spot. It’s the same sense of productivity I get when building Sinatra apps, without the regret of, “Doh! I should have built this in Rails because they’ve solved a lot of my problems”. I can also put views in ./app/posts/index.rb when I have a need to share that view between different controllers (or if I prefer to do it this way).

  • View classes map to Rails resources - If you’ve built a RESTful Rails application then you’re familiar with def index; def show; etc. for your resources actions. I do the same thing, except I map the resources to the class names instead, which get rendered. That means PostsController::Index gets rendered for /posts and PostsController::Show for /posts/1. At the moment I’m focusing on making this work in Rails, which you can use today with Erb, but it’s built with a core class that’s framework agnostic.

  • Layouts are in the superclass, and I can pass them values via inheritance - You’ll see def title and def subtitle in all my views. These values are called by the superclass, which renders the layout. I don’t have to worry about content_for vs just assigning a variable.

  • The forms permit their own parameters - I figured out how to make a form builder that I call Superform (GitHub - rubymonolith/superform: Build highly customizable forms in Rails) that’s also built entirely from Phlex. My favorite part about it is I no longer need to use Rails strong params because the form does that for me—I assign values through the form and they end up on my object. If the field isn’t in the form, it doesn’t assign.

Demo repo is at GitHub - rubymonolith/demo: Demo of Monolith, a highly productive way of building web applications for those who want to see a different approach to using components in Rails outside of partials and ViewComponents. Happy to answer any questions on here about it.

2 Likes

We created our own version of TailwindCSS to keep size down

Can you say more about this? TailwindCSS, properly configured, already only generates the CSS for the classes in use in your project.

That was phrased poorly. “To keep the size down” was not comparing to Tailwind, but to other CSS frameworks. Our version is a few years old and we didn’t know tailwindcss (and frameworks like it) existed at the time.

1 Like

I also tend to build my own form builders since form helpers and VCs do not play along nicely. I do keep the same api for inputs though than the one rails provide (apart from custom inputs of course).

I’ve seen Tailwind creators write there’s a possibility the API to do could be deprecated so it felt like not the best bet, and a component system is better suited for this case.

That’s how I feel right now when building a new project. Take what Rails has to offer til you feel like you’ve hit a ceiling and look for an alternative then. Feels like the proper approach!

1 Like

This is correct out of the box, but the workarounds listed on the site may work for you. The capture compatibility patch might help, but I’m a particular fan of view_component-form! Please don’t be deterred from giving ViewComponent a shot if you’re reading this.

The important part here is encapsulation, I think. While you’re right that helper tests can be straightforward to write as component tests, the fact that they sit in the global scope and may not always return HTML (e.g. if you’re using capture) can go on to mean that they don’t scale well. I think encapsulation is perhaps the biggest problem with the Rails view layer, but I can respect that the factory that makes the sharp knives (37signals) probably has a good handle on how to use them safely. If you do decide on adopting ViewComponent, though, you won’t be alone in that decision!

For full disclosure, I’m a maintainer on ViewComponent.

2 Likes

For us, the winning combination has been ViewComponents, TailwindCSS, and Hotwire. A single component can encapsulate a lot of “presentation logic” (CSS classes, Stimulus controller, configuration, variants, etc) and be reused throughout our application. If we decide to change styles, we can update it in one place. If we decide we want create a new component that is similar (but different) from an existing component, we can subclass a component. Having a component API/DSL makes creating view templates much cleaner and easier to read. I highly recommend the trio of ViewComponents, TailwindCSS, and Hotwire.