New Discussion

Notifications

You’re not receiving notifications from this thread.

A Simpler Build with Broadcasts Refreshes Discussion

0
General

My general rule of thumb is I'll use redirecting back/morphing for very simple or low-frequency pages such as admin pages where there's not a lot of user traffic, but will reach for turbo streams when the page has multiple/expensive DB queries. The thing I can't really get over is re-executing all the DB queries, processing the full page, and then DOM morphing out the pieces to change. That's a lot of overhead when all I usually need to do is update a few elements on the page or add a new one. It is convenient for simple pages, and does reduce developer complexity, but I'd only use them sparingly on complex pages.

This is my #1 issue with hotwire: there isn't a cleaner "built-in" way to easily update attributes or data on the page without a decent amount of manually wrapping and updating. I spent the last week building out a working version where I can list attributes for a record and turbo stream can update them easily via:

# app/views/users/update.turbo_stream.erb
<%= turbo_stream.replace_wrapped_attributes(@user, :name, :email, :phone_number) %>

It uses a custom turbo action and a #wrap method in the model that outputs the attributes with a span dom_id wrapper:

# app/models/application_record.rb
# Wrap attributes so they can easily be updated via TurboStreams.
# Requires the model to define #attributes_to_wrap.
def wrap(attribute)
  entry = attributes_to_wrap[attribute].presence || {}
  formatter = entry.fetch(:formatter, nil)
  default = entry.fetch(:default, nil)
  component = AttributeComponent.new(self, attribute, formatter: formatter, default: default)
  ApplicationController.new.view_context.render(component)
end

# a view file
<%= @user.wrap(:name) %>
# => <span class="name_user_1">John Smith</span>

Since I'm using Github ViewComponents in my app, I even allowed formatting of data such as phone numbers in the model:

# app/models/user.rb
def attributes_to_wrap
  {
    name: { formatter: nil, default: "No name" },
    phone_number: { formatter: :phone, default: "No phone number" }
  }
end

Then I have a generic AttributeComponent that handles formatting based on what it reads from the model. This certainly violates some concepts of MVC, but when updating an attribute dynamically on the page from the back-end, I need to know how to format the data, and the only way I could cleanly get that was adding some "wrapping" content to the model and returning a ViewComponent object with formatting built into it.

class AttributeComponent < ApplicationComponent
  attr_reader :model, :attribute, :formatter, :default

  def initialize(model, attribute, formatter: nil, default: nil)
    @model = model
    @attribute = attribute
    @formatter = formatter
    @default = default.presence || "No #{@attribute.to_s.downcase.humanize}"
  end

  def formatter?
    @formatter.present?
  end

  def raw_attribute
    @raw_attribute ||= model.public_send(attribute)
  end

  # Format the value if an optional formatter is provided
  def formatted_value
    case formatter
    when :phone
      helpers.number_to_phone(raw_attribute.to_s)
    else
      raw_attribute # Unknown formatter
    end
  end

  def attribute_identifier
    model.dom_id(attribute)
  end

  def value
    if raw_attribute.present?
      formatter? ? formatted_value : raw_attribute
    else
      default
    end
  end

  def call
    content_tag(:span, value, class: attribute_identifier, data: { view_component: true, wrapped_attribute: true })
  end
end

Similar to the argument for a global Current object in Rails, I violate the principle when it makes sense and saves me a ton of time, and the above code saves me dozens of lines of code in various turbo_stream.erb files.

Hope others may find it useful or recommend ways to make it better!

Really enjoying the hotwire episodes Collin, keep them coming!

Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 89,509+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.