Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

view components

branch: master

Fetching latest commit…

Octocat-spinner-32-eaf2f5

Cannot retrieve the latest commit at this time

Octocat-spinner-32 examples
Octocat-spinner-32 lib
Octocat-spinner-32 spec
Octocat-spinner-32 .gitignore
Octocat-spinner-32 .rspec
Octocat-spinner-32 Gemfile
Octocat-spinner-32 MIT.LICENSE
Octocat-spinner-32 README.md
Octocat-spinner-32 Rakefile
Octocat-spinner-32 viewlet.gemspec
README.md

Goals

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

Installation

gem "viewlet", :git => "https://github.com/cppforlife/viewlet.git"

Usage

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

%h1= "Group: #{@group.name}"
%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|
    .name= member.name
    .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:

.list_section
  %h2
    = 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
    %ul
      - 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|
    .name= member.name
    .summary= member.summary

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

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

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

CSS & JS

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

Tips

  • 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:
$(document).ready(function(){
  $(".list_section").listSection();
});

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)
  :javascript
    $(document).ready(function(){
      $("##{unique_id}").listSection();
    });

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

Todo

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