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.
Table of Contents
- Ruby 2.5+
- Rails 5.0+
- Webpacker 3.0.0+
# 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'
Or, directly from your controllers:
# app/controllers/pages_controller.rb
def home
render html: helpers.component('home')
end
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. Additionally, the entire locals
hash is made available through a properties
helper method.
/ app/views/pages/home.html.slim
= component 'button', text: 'My button'
/ frontend/components/button/_button.html.slim
.button
= @text
Komponent relies on Rails Low-level caching.
You can cache the component by passing the cached: true
option. The cache will expire when the locals, options or block change.
If you want better control of the cache expiration, you can provide a custom cache_key
. When the cache_key
changes, the cache will be cleared.
/ app/views/pages/home.html.slim
/ Cache the component based on its locals
= component "button", { text: 'Click here' }, cached: true
/ or cache the component with a specific key, such as the last update of a model
= component "button", { text: 'Click here' }, cached: true, cache_key: @product.updated_at
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
extend ComponentHelper
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
extend ComponentHelper
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
- if external_link?
= render 'suffix', text: '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 add stimulus
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]
If you have the
rails-i18n
gem in yourGemfile
, you should whitelist locales to prevent creating a lot of locale files when you generate a new component.
Komponent includes a basic styleguide engine that you can use in your project to document your components.
To set it up, you can use the generator:
rails generate komponent:styleguide
This command will:
- copy the styleguide components (
komponent/container
,komponent/footer
,komponent/header
andkomponent/sidebar
) to your components folder, so you can customize them - add a new
komponent.js
pack to your packs folder - mount the engine in your routes
Then, for each component, you can describe it and render examples for each state in the _example.html.slim
file from the component folder. The engine will then render it on the component page.
If you have existing components, you can generate all their example files at once with:
rails generate komponent:examples
Finally, visit http://localhost:3000/styleguide
to access your styleguide.
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')
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 configure the stylesheet engine used for generate stylesheet file, allowed values are :css
, :scss
, :sass
.
Rails.application.config.komponent.stylesheet_engine = :css # default value is :css
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
Run all Cucumber features and unit tests with bundle exec appraisal rake test
Run the full test matrix with bundle exec appraisal rake test
Bug reports and pull requests are welcome on GitHub at https://github.com/komposable/komponent.
- Update the CHANGELOG (add a title and a date for the new version)
- Update the version number in
lib/komponent/version
- Install the
gem-release
gem if you haven't already - Run
gem release --tag --push
- Create or update the release on Github with the same version number and copy-paste the description from the CHANGELOG
Please note:
If you're releasing a patch version (eg. from 2.0.1 to 2.0.2) you can run gem bump patch --release --tag --push --sign
so you don't have to manually change the version number.
If you want to release a specific version (eg. beta, RC...), you can run gem bump 3.0.0.beta1 --release --tag --push --sign
The gem is available as open source under the terms of the MIT License.