Ruby On Rails integration / Reusing component views

Very often you will find that you need to reuse the components you've already defined in some of your views. For instance, looking at the example from the previous page, we could find ourselves needing to reuse both :offer_list and :offer components in two places in our Rails app: on the "Offers" page that's accessible to all users and also on the "My Offers" page that can only be seen by the currently signed in user.

You may rightfully say, that for these purposes, a partial could be used, however with a partial you'd need to pass all these local variables to set component attributes, html attributes and other things. Thus, webface_rails helpers simplify this process.

What we actually want to write on both "Offers" and "My Offers" pages is something like this:

= component :offer_list, per_page: 20 do
  - @offers.each do |offer|
    = component :offer, disabled: offer.is_disabled, title: @offer.title, description: @offer.description

To be able to do that, we need to create two files in the app/views/webface_components directory. Let's start with the offer:

/- app/views/webface_components/offer.html.haml
= define_component :offer, ".offer", {      |
    attr_map: "disabled:disabled",          |
    disabled: false,                        |
    style: "font-weight: bold",             |
    mapped_attrs: { visible: true }         |
  }, options.except(:title,:description) do |

= component_part :body do
  = component_attr :title, "span", text_content: options[:title]
  = component_attr :description, "span", text_content: options[:description]

Notice a few changes from the previous page example. First of all, we've added a fourth argument to the define_component method call, also a hash called options. And, the previous so called options are now wrapped in {} effectively making it the third argument. But why?

This so we can specify default options in the this argument, but then anything that's passed in the = component call will be merged into the third argument, replacing default arguments. The options local variable itself is available in this view context because that's what webface_rails does for us when we reuse components with = component method - it actually renders a partial and passes all of the named arguments as hash into this options variable.

Confused? Ok, let's try again. Here's our = component method call:

= component :offer, disabled: offer.is_disabled, title: @offer.title, description: @offer.description

Suppose offer.disabled is true in this case. Then, within that app/views/webface_components/offer.html.haml file, the options variable will contain the following:

  disabled: true,
  title: "... value from @offer.title",
  description: "... value from @offer.description"

When = define_component is called and is passed options it automatically merges it with the hash in the third argument. The interesting part, though, is how we exclude :title and :description from the options hash before merging it. Why? Because we want the Webface component binding of those attributes to be on some of the child dom elements. If we included them, we'd end up with the the following html:

That is hardly our intention. :title and :description are getting their own dom elements, specifically these:

= component_attr :title, "span", text_content: options[:title]
= component_attr :description, "span", text_content: options[:description]

If description required some more customization, we could pass the options[:description] in a block like this:

= component_attr :title, "span", text_content: options[:title]
= component_attr :description, "span" do
  %h3 Description:
  %p= options[:description]

Now finally, let's take care of our :offer_list component view in the same manner, by creating a file in app/views/webface_components

/- app/views/webface_components/offer_list.html.haml
= define_component :offer_list, ".offerList", {     |
    mapped_attrs: { per_page": 10, visible: true }, |
  }, options do                                     |
  = capture(&block)

Notice how on line 5 here, we called capture(&block). This is because we know that when this component view is reused in some other view with component :offer_list it will be passed a block that includes the content (individual offers). If we forgot to add the capture(&block) call, we'd have an empty offer list.