Komponent implements an opinionated way of organizing front-end code in Ruby on Rails, based on components.
Each component has its own folder, containing a Ruby module, a partial, a stylesheet and a JavaScript file.
Komponent relies heavily on webpacker to manage dependencies and generate the production JS and CSS files.
This README examples are written in Slim, but Komponent is compatible with:
- your preferred templating language (Slim, Haml, erb)
- your stylesheet language of choice (Sass, SCSS, CSS, PostCSS)
This gem has been inspired by our Rails development practices at Ouvrages and Etamin Studio, and the (excellent) Modern Front-end in Rails article from Evil Martians.
# Gemfile
gem "komponent"
Run the following command to set up your project instantly:
rails generate komponent:install
This command will:
- check that the dependencies (currently, webpacker) are installed
- rename the
app/javascript
folder tofrontend
and modify webpacker config accordingly - create the
frontend/components
folder where you will put your component - create the
frontend/components/index.js
file that will list your components andimport
it infrontend/packs/application.js
Generate a new component with the component
generator:
rails generate component button
Then, render it in your views with the component
helper (or its alias c
).
/ app/views/pages/home.html.slim
= component "button"
= c "button"
Make sure to include javascript pack tag and stylesheet pack tag in your application layout file, for instance:
/ app/views/layouts/application.html.slim
doctype html
html
head
= stylesheet_pack_tag "application"
body
= yield
= javascript_pack_tag "application"
Check Webpacker documentation for further information.
You can pass locals
to the helper. They are accessible within the component partial, as instance variables.
/ app/views/pages/home.html.slim
= component "button", text: "My button"
/ frontend/components/button/_button.html.slim
.button
= @text
The component also accepts a block
. To render the block, just use the standard yield
.
/ app/views/pages/home.html.slim
= component "button"
span= "My button"
/ frontend/components/button/_button.html.slim
.button
= yield
You can check if the component has been called with a block using the block_given_to_component?
helper from within the component.
Each component comes with a Ruby module
. You can use it to set properties:
# frontend/components/button/button_component.rb
module ButtonComponent
property :href, required: true
property :text, default: "My button"
end
/ frontend/components/button/_button.html.slim
a.button(href=@href)
= @text
If your partial becomes too complex and you want to extract logic from it, you may want to define custom helpers in the ButtonComponent
module:
# frontend/components/button/button_component.rb
module ButtonComponent
property :href, required: true
property :text, default: "My button"
def external_link?
@href.starts_with? "http"
end
end
/ frontend/components/button/_button.html.slim
a.button(href=@href)
= @text
= " (external link)" if external_link?
/ app/views/pages/home.html.slim
= component "button", text: "My button", href: "http://github.com"
You can also choose to split your component into partials. In this case, we can use the default render
helper to render a partial, stored inside the component directory.
/ frontend/components/button/_button.html.slim
= a.button(href=@href)
= @text
= render("suffix", text: "external link") if external_link?
/ frontend/components/button/_suffix.html.slim
= " (#{text})"
To organize different types of components, you can group them in namespaces when you use the generator:
rails generate component admin/header
This will create the component in an admin
folder, and name its Ruby module AdminHeaderComponent
.
Komponent supports stimulus 1.0.
You can pass --stimulus
to both generators to use Stimulus in your components.
rails generate komponent:install --stimulus
This will yarn stimulus
package, and create a stimulus_application.js
in the frontend
folder.
rails generate component button --stimulus
This will create a component with an additional button_controller.js
file, and define a data-controller
in the generated view.
In case your component will contain text strings you want to localize, you can pass the --locale
option to generate localization files in your component directory.
rails generate component button --locale
This will create a yml
file for each locale (using I18n.available_locales
). In your component, the t
helper will use the same "lazy" lookup as Rails.
/ frontend/components/button/_button.html.slim
= a.button(href=@href)
= @text
= render("suffix", text: t(".external_link")) if external_link?
# frontend/components/button/button.en.yml
en:
button_component:
external_link: "external link"
# frontend/components/button/button.fr.yml
fr:
button_component:
external_link: "lien externe"
You can whitelist the locales you use by setting this into an initializer, as explained in the "official guide":
I18n.available_locales = [:en, :fr]
You can configure the generators in an initializer or in application.rb
, so you don't have to add --locale
and/or --stimulus
flags every time you generate a fresh component.
config.generators do |g|
g.komponent stimulus: true, locale: true # both are false by default
end
You can change the default root path (frontend
) to another path where Komponent should be installed and components generated. You need to change komponent.root
in an initializer.
Rails.application.config.komponent.root = Rails.root.join("app/frontend")
If for some reason your preferred templating engine is not detected by Komponent, you can force it by manually defining it in your config:
Rails.application.config.generators.template_engine = :haml
You may want to use components in a gem, or a Rails engine, and expose them to the main app. In order to do that, you just have to configure the paths where Komponent will look for components.
From a gem:
module MyGem
class Railtie < Rails::Railtie
config.after_initialize do |app|
app.config.komponent.component_paths.append(MyGem.root.join("frontend/components"))
end
initializer "my_gem.action_dispatch" do |app|
ActiveSupport.on_load :action_controller do
ActionController::Base.prepend_view_path MyGem.root.join("frontend")
end
end
initializer 'my_gem.autoload', before: :set_autoload_paths do |app|
app.config.autoload_paths << MyGem.root.join("frontend")
end
end
private
def self.root
Pathname.new(File.dirname(__dir__))
end
end
or from an engine:
module MyEngine
class Engine < Rails::Engine
isolate_namespace MyEngine
config.after_initialize do |app|
app.config.komponent.component_paths.append(MyEngine::Engine.root.join("frontend/components"))
end
initializer "my_engine.action_dispatch" do |app|
ActiveSupport.on_load :action_controller do
ActionController::Base.prepend_view_path MyEngine::Engine.root.join("frontend")
end
end
initializer 'my_engine.autoload', before: :set_autoload_paths do |app|
app.config.autoload_paths << MyEngine::Engine.root.join("frontend")
end
end
end
Make sure you add komponent
to the runtime dependencies in your gemspec
.
In order to compile packs from engine, and to use javascript_pack_tag 'engine'
, you need to:
- Create a pack file in main app
// frontend/packs/engine.js
import 'packs/engine';
- Append engine frontend folder to
resolved_paths
inconfig/webpacker.yml
from your main app
resolved_paths:
- engine/frontend
Bug reports and pull requests are welcome on GitHub at https://github.com/komposable/komponent.
The gem is available as open source under the terms of the MIT License.