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