Komponent implements an opinionated way of organizing front-end code in Ruby on Rails, based on components.
👋 HEADS UP! The future of this gem is currently being discussed. Please have your say!
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.
- Compatibility
- Getting started
- Usage
- Passing variables
- Passing options
- Component caching
- Passing a block
- Properties
- Helpers
- Component partials
- Namespacing components
- Stimulus integration
- Internationalization
- Available locales configuration
- Styleguide
- Configuration
- Change default root path
- Default options for the generators
- Change default stylesheet engine
- Force default templating engine
- Additional paths
- Contributing
- Releasing a new version
- License
Compatibility
- Ruby 2.5+
- Rails 5.0+
- Webpacker 3.0.0+
Getting started
# Gemfile
gem 'komponent'Run the following command to set up your project instantly:
rails generate komponent:installThis command will:
- check that the dependencies (currently, webpacker) are installed
- rename the
app/javascriptfolder tofrontendand modify webpacker config accordingly - create the
frontend/componentsfolder where you will put your component - create the
frontend/components/index.jsfile that will list your components andimportit infrontend/packs/application.js
Usage
Generate a new component with the component generator:
rails generate component buttonThen, 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')
endMake 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.
Passing variables
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
= @textPassing options
Component caching
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_atPassing a block
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
= yieldYou can check if the component has been called with a block using the block_given_to_component? helper from within the component.
Properties
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)
= @textHelpers
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"Component partials
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})"Namespacing components
To organize different types of components, you can group them in namespaces when you use the generator:
rails generate component admin/headerThis will create the component in an admin folder, and name its Ruby module AdminHeaderComponent.
Stimulus integration
Komponent supports Stimulus >= 1.0.
You can pass --stimulus to both generators to use Stimulus in your components.
rails generate komponent:install --stimulusThis will yarn add stimulus and create a stimulus_application.js in the frontend folder.
rails generate component button --stimulusThis will create a component with an additional button_controller.js file, and define a data-controller in the generated view.
Internationalization
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 --localeThis 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 externeAvailable locales configuration
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-i18ngem in yourGemfile, you should whitelist locales to prevent creating a lot of locale files when you generate a new component.
Styleguide
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:styleguideThis command will:
- copy the styleguide components (
komponent/container,komponent/footer,komponent/headerandkomponent/sidebar) to your components folder, so you can customize them - add a new
komponent.jspack 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:examplesFinally, visit http://localhost:3000/styleguide to access your styleguide.
Configuration
Change default root path
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')Default options for the generators
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
endChange default stylesheet engine
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 :cssForce default templating engine
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 = :hamlAdditional paths
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
endor 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
endMake 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_pathsinconfig/webpacker.ymlfrom your main app
resolved_paths:
- engine/frontendRunning tests
Run all Cucumber features and unit tests with bundle exec appraisal rake test
Run the full test matrix with bundle exec appraisal rake test
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/komposable/komponent.
Releasing a new version
- Update the CHANGELOG (add a title and a date for the new version)
- Update the version number in
lib/komponent/version - Install the
gem-releasegem 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
License
The gem is available as open source under the terms of the MIT License.
