An opinionated way of organizing front-end code in Ruby on Rails, based on components
Clone or download

README.md

Komponent

Build Status GitHub release Maintainability Coveralls github

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

Compatibility

  • Ruby 2.2+
  • 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:install

This command will:

  • check that the dependencies (currently, webpacker) are installed
  • rename the app/javascript folder to frontend 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 and import it in frontend/packs/application.js

Usage

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.

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
  = @text

Passing 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_at

Passing 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
  = yield

You 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
  property :href, required: true
  property :text, default: 'My button'
end
/ frontend/components/button/_button.html.slim

a.button(href=@href)
  = @text

Helpers

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"

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/header

This 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 --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.

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 --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

Available 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-i18n gem in your Gemfile, you should whitelist locales to prevent creating a lot of locale files when you generate a new component.

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
end

Change 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 :css

Force 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 = :haml

Additional 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
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 in config/webpacker.yml from your main app
resolved_paths:
  - engine/frontend

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/komposable/komponent.

License

The gem is available as open source under the terms of the MIT License.