Skip to content


Subversion checkout URL

You can clone with
Download ZIP
view components
Ruby JavaScript
Fetching latest commit...
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
examples added bootstrap progress bar example
lib bump verson
.gitignore first pass
Gemfile init viewlet
MIT.LICENSE added mit license
Rakefile init viewlet
viewlet.gemspec added specs and fixed haml syntax attributes hash bug


  • to ease creation of view components

    Problem: Most likely your site has few similar view structures that are repeated throughout the site (e.g. list of members, groups, etc.). One solution is to refactor such code into a shared partial (may be _list_section.html.haml) and pass customization options via locals hash; however, with this approach it can become quite challenging/inelegant to apply customizations.

  • to organize HTML/JS/CSS files based on a feature rather than file type

    Problem: As soon as you start extracting reusable view components from your pages it becomes weird to have HTML/CSS/JS component files spread out in three different directories. Turning your component into a gem remedies that problem since gems can have separate assets directory; however, I don't see a benefit in making every single component into a gem, especially when it's application specific.


gem "viewlet", :git => ""


Let's say we have GroupsController#show that lists group members. Here is how show.html.haml could look:

%h1= "Group: #{}"
%p= @group.description

= viewlet(:list_section) do |s|
  - s.heading "Group members"
  - s.empty_description "No members in this group"

  - s.collapse_button false
  - s.add_button do
    = link_to "Invite Members", new_group_member_path(@group)

  - s.items @group.members

  - s.row_title do |member|
    .summary= member.summary

  - s.row_details do |member|
    = render :partial => "some_other_partial", :locals => {:member => member}

Now let's define list_section viewlet. Viewlets live in app/viewlets and each one must have at least <name>.html.haml.

In app/viewlets/list_section/list_section.html.haml:

    = heading

    - if add_button
      %small= add_button

    - if collapse_button
      %small.collapse_button= link_to "Collapse", "#"

  - if items.empty?
    - # outputs value regardless being defined as an argument-less block or a plain value
    %p= empty_description

  - else
      - items.each do |item|
        %li{:class => cycle("odd", "even", :name => :list_section)}
          .left= list_section.row_title(item)

          - # alternative way of capturing block's content
          .right= capture(item, &row_details)

All viewlet options (heading, add_button, etc.) set in show.html.haml become available in list_section.html.haml as local variables. None of those options are special and you can make up as many as you want.

Note: If there aren't CSS or JS files you want to keep next to your viewlet HTML file you don't need to create a directory for each viewlet; simply put them in app/viewlets e.g. app/viewlets/list_section.html.haml.

Special HAML syntax

If you are using HAML you can use special syntax to output a viewlet:

%list_section_viewlet # viewlet name suffixed with '_viewlet'
  heading "Group members"
  empty_description "No members in this group"

  collapse_button false
  add_button do
    = link_to "Invite Members", new_group_member_path(@group)

  items @group.members

  row_title do |member|
    .summary= member.summary

  row_details do |member|
    = render :partial => "some_other_partial", :locals => {:member => member}

%password_strength_viewlet{:levels => %w(none weak good)}

  - levels %w(none weak good) # notice optional dash at the beginning


You can also add other types of files to app/viewlets/list_section/. Idea here is that your viewlet is self-contained and encapsulates all needed parts - HTML, CSS, and JS.

In app/viewlets/list_section/plugin.css.scss:

.list_section {
  width: 300px;

  ul {
    margin: 0;

  li {
    border: 1px solid #ccc;
    margin-bottom: -1px;
    padding: 10px;
    list-style-type: none;
    overflow: hidden;

  .left {
    float: left;

  .right {
    float: right;

To include list_section viewlet CSS in your application add

*= require list_section/plugin

to your application.css

In app/viewlets/list_section/plugin.js:

// Probably define listSection() jQuery plugin

To include list_section viewlet JS in your application add

//= require list_section/plugin

to your application.js


  • You do not have to provide a block to viewlet:
= viewlet(:password_strength)
  • You can use hash syntax (and block syntax):
= viewlet(:password_strength, :levels => %w(none weak good))

= viewlet(:password_strength, :levels => %w(none weak good)) do |ps|
  - ps.levels %w(none weak good excellent) # overrides levels
  • Let's say we decide to make our list_section viewlet use third-party list re-ordering library (e.g. orderable-list.js). You can add orderable-list.js javascript file to app/viewlets/list_section and require it from plugin.js:
//= require ./orderable-list
  • Let's say our plugin.js defined jQuery plugin listSection so that in our application.js we can do something like this:

This is fine; however, that means that our component is not really functional until we add that javascript piece somewhere. Alternatively you can put it right after HTML so everytime list_section is rendered it will be automatically initialized.

For example in list_section.html.haml:

.list_section{:id => unique_id}
  %h2= heading

- unless defined?(no_script)

Every viewlet has a predefined local variable unique_id that could be used as HTML id.

  • It's trivial to subclass Viewlet::Base to add new functionality. class_name option lets you set custom viewlet class:
= viewlet(:list_section, {}, :class_name => "CustomListSectionViewlet") do


  • come up with a better name for main files - plugin doesn't sound that good
  • lib/viewlets/ as fallback viewlet lookup path
  • automatically load custom Viewlet::Base subclass from some_viewlet/plugin.rb
Something went wrong with that request. Please try again.