I think I figured it out with the help of Joel Draper and Joel Moss over at Phlex’s github! [Link]
I’ll paste my solution here for anyone who’s interested.
- Co-locate JS, CSS, and component files for each component. For example:
├── app
│ ├── views
│ │ ├── components
│ │ │ ├── navbar
│ │ │ │ ├── navbar.css
│ │ │ │ ├── navbar.rb
│ │ │ │ ├── navbar.js
- Use PostCSS Modules to transform any classes found under views before bundling. For example
class="container"
becomes class="Components--Navbar--container"
// /postcss.config.js
const path = require('path');
module.exports = {
plugins: [
require('postcss-import-ext-glob'),
require('postcss-import')({
plugins: [
require('postcss-modules')({
generateScopedName: (name, filename, _css) => {
const path = require('path');
return path.relative('./app/views/', filename).
split('/').
toSpliced(-1).
map((f) => String(f).charAt(0).toUpperCase() + String(f).slice(1)).
join('--') + "--" + name;
},
// Don't generate *.css.json files (we don't need them)
getJSON: () => {},
})],
}),
require('postcss-nesting'),
require('autoprefixer')
],
};
- Use a helper to transform CSS classes in the component the same way as above. Export the module function so it can become an instance method in step 4.
# /app/helpers/css_helper.rb
module CssHelper
def modularize(class_name)
component = self.class.name.gsub("::", "--")
component + "--" + class_name
end
module_function :modularize
end
- Include the helper module in the Components::Base
# /app/views/components/base.rb
class Components::Base < Phlex::HTML
include Components
include Phlex::Rails::Helpers::Routes
include Phlex::Rails::Helpers::ImageTag
include Phlex::Translation
include CssHelper # <==
end
- Call the helper from the component when defining HTML element classes.
# /app/views/components/navbar/navbar.rb
class Components::Navbar < Components::Base
def view_template(&)
div(class: modularize("container"), &)
end
end
This is what the CSS file would look like pre-processing:
/* /app/views/components/navbar/navbar.css */
.container {
background-color: blue;
}
And post:
/* /app/assets/builds/application.css */
.Components--Navbar--container {
background-color: blue;
}
The helper we defined above will transform our HTML to match! The benefit of this solution is that we are shipping plain, functional CSS and HTML with scoped classes. This is better than trying to update our HTML in the client via JS. With that said, it’s a very complex solution. I’m curious about the Web Components approach described in the ViewComponent docs