"Hey, there is a small change ..."
We've all had that Friday afternoon when a client approaches us with a very serious request: they want to give the button element we just shipped a makeover. Apparently, the button looked so drab compared to the rest of the folks at the party. They insisted it needed a brighter shade of blue, rounded edges, and a shadow effect to give it some depth—like a button with aspirations to stand out!
"And let's add some visual feedback," they added, "like a pop-up that says 'Are you sure?' every time someone clicks it."
With a few keystrokes and a sprinkle of developer magic, we've transformed that button into a dazzling, interactive masterpiece. And it only took three hours.
So cumbersome!
Giving a UI element the needed glow up may not be as straightforward as it seems. To illustrate what I mean, let's take a look at a typical Rails app directory structure:
app
├── assets
| ├── ...
| └── stylesheets
| ├── ...
| └── example.css
├── helpers
| ├── ...
| └── example_helper.rb
├── javascript
| ├── ...
| └── controllers
| ├── ...
| └── example_controller.js
├── views
| ├── ...
| └── example
| | ├── ...
| | └── show.html.erb
| └── shared
| ├── ...
| └── _example.html.erb
├── ...
Where do we even begin? We'd typically find our target element in an HTML template in the app/views folder. However, a friendly developer might have added another layer of abstraction to its rendering in the form of a helper method found in app/helpers, or a decorator/presenter type object.
Next, we locate the CSS files containing the classes used by our target element in the app/assets/stylesheets folder. In all likelihood, these classes are shared across multiple other elements; hence, we make sure our changes only apply to our target. We may deal with this less frequently, though, with the help of [Tailwind CSS](https://tailwindcss.com) if we don't mind being more verbose in our HTML class attributes.
Then, we track down the JS files in the app/javascript folder containing the functions that handle our target element's behavior. And if we're using [Stimulus](https://stimulus.hotwired.dev), we look for the controller name defined in a data-controller attribute somewhere in the HTML, and find the matching file in the app/javascript/controllers folder.
To top it all off, especially in less-than-ideal circumstances, we also deal with complexity brought about by bad naming conventions, a random < style > tag and attribute sprinkled here and there, or the incorporation of an icon/image asset. Yes, and we haven't even talked about the dreadful trial-and-error effort that comes along all of this.
It certainly requires a particular level of familiarity to navigate comfortably through a codebase, even a well-structured one. This separation can make it tricky to locate and manage closely related files, and it all feels so, well, cluttered. The kind of workflow this produces can lead to additional overhead and can make updating styles or JavaScript a labor-intensive process.
Make it make sense
While it's generally understandable to organise similar files together, this can be a detriment when designing something component-specific. Maybe our file structure also needs a makeover:
app/components
├── ...
├── example
| ├── ...
| ├── component.rb
| ├── component.html.erb
| └── component_controller.js
├── ...
What a clean look! We can achieve this using ViewComponent's sidecar capabilities.
ViewComponent also allows for a variety of structure and naming conventions see [discussion. I go for the one shown above see comment because it looks cleaner in my eyes even with the namespacing:
html
<%= render Example::Component.new %>
html
It does require a bit of tweaking, including adjustments for Stimulus and Tailwind:
js
/* app/assets/config/manifest.js */
//= link_tree ../../components .js
ruby
# config/application.rb
config.autoload_paths
<< Rails.root.join('app/components')
ruby
# config/importmap.rb
components_path = Rails.root.join('app/components')
components_path
.glob('**/*_controller.js')
.each do |controller|
name = controller.relative_path_from(components_path).to_s.remove(/\.js$/)
pin "components/#{name}", to: "#{name}.js"
end
ruby
# config/initializers/assets.rb
Rails.application.config.assets.paths
<< Rails.root.join('app/components') # for component sidecar js
Rails.application.config.importmap.cache_sweepers
<< Rails.root.join('app/components') # sweep importmap cache for components
js
/* config/tailwind.config.js */
module.exports = {
content: [
'./app/components/**/*'
]
}
- __Separation of Concerns__. Each component encapsulates a specific UI design and presentation. All related logic, files, and assets that were previously scattered across different locations are consolidated into one cohesive whole, making the codebase more manageable.
- __Modularity__. The sidecar pattern allows components to manage their own styles and behavior independently, leading to reduced interdependencies and improved testability.
- __Reusability__. Components can be easily reused across different views, reducing code duplication and promoting consistency in design.
- __Improved Collaboration__. You and that friendly developer can work on different components simultaneously without affecting each other's work, streamlining team collaboration and making version control much simpler.
It's just more fun!
That Friday afternoon shouldn't be so bad anymore. Using ViewComponent and its sidecar pattern capabilities can significantly enhance your Ruby on Rails development experience, making it not only more productive but also more enjoyable. And, honestly, it kinda scratches that OCD itch, at least for me.
Ps. if you have any questions
Ask here