New Experimental Feature in Cells: View Models

The “cells”: gem has been around for almost 7 years now. With more than 300.000 “downloads”: within 3 years it has gained some traction in the Rails community. Many projects are using it heavily to write reusable widgets, testable partials or just to have well-encapsulated view components.

We felt it was time to breath some fresh air and take this mature project a step further.

h3. View Models?

Cells still works the same way as it used to work. Relax, you can still use @render_cell@ as before.

However, we now got a second “dialect” “in version 3.9.0”: The new view models in cells addresses two issues that have been around for longer.

A *streamlined DSL* makes it easier to work with the cell instance itself. This is extremely helpful now that view models keep *helper methods on the instance level*.

Let’s see how that all works in an example.

class SongCell < Cell::Rails
  include Cell::Rails::ViewModel

  property :title

  def show

Mix in the @ViewModel@ feature and get a new semantic for your cell. First, check how a view model is created.

class DashboardController < ApplicationController
  def index
    song  = Song.find(1) # 

    @cell = cell(:song, song)

Cell _instances_ are created in the controller. This could also happen in the view – if you really want that. Note that the second argument is the *decorated model* that this cell represents.

h3. Decorating Models – Step 1.

Attributes declared with @::property@ (line 4) will automatically be delegated to the cell’s model. So, the following call works without any additional code.

@cell.title #=> "Roxanne"

But more on that later.

View rendering still happens by using @#render@ – exactly as in the “old dialect”. That’ll invoke the existing rendering with all the nice things like view inheritance, caching, etc.

The DSL looks a bit different, thou. #=> invokes show state

This line will call the @#show@ method (“state”) which in turn renders @app/cells/song/show.haml@.

h3. Helpers Are Instance Methods.

We should look at the rendered view to understand what changed in terms of helpers and their scope.

/ app/cells/song/show.haml

%h1 #{title}

This song is awesome!

= link_to "Permalink", song_url(model)

Four helpers are used in this view. It is important to understand that all helpers in a view model view are *invoked in the cell instance context*.

Now, what does that mean?

Well, the call to @title@ (line 3) is not called in some strange module, it is simply invoked on the cell, as we did earlier when doing @cell.title.

The same happens with @link_to@ and @song_url@: The view model automatically provides the URL helpers on the instance.

The @model@ method is another view model “helper”, an instance method, returning the decorated object (line 7).

h3. Decorating Models – Step 2.

This might look confusing at first glance, but imagine how simple it becomes to write your own “helper” now.

Why not extract the entire @#link_to@ line to a separate, testable method?

class SongCell < Cell::Rails
  # ..

  def permalink
    link_to "Permalink", song_url(model)

You can just _move_ the entire line to the cell class and it’ll work.

h3. Testing Helpers.

Not that you suddenly get benefits like encapsulation and inheritance, no, also your testing is greatly improved for your new “helper”.

it "renders #self_link" do
  cell(:song, song).permalink.
    should eq "<a href.."

This doesn’t fake an environment as Rails helper tests do. It executes the same code in the same environment as in production.

h3. Using Existing Helpers.

To use one of Rails’ numerous helpers, you _include_ the modules into your cell class.

class SongCell < Cell::Rails
  include TagHelper
  # ..

You can then use the methods in your view – or in your instance methods.

Again, the magical copying of methods into your view doesn’t happen anymore. The view model instance will be the view’s context itself.

h3. Pollution.

I can hear people complaining about stuffing all those helper methods in the the poor cell class. But let me ask you? Do you really feel comfortable *pushing your helpers into a scopeless, not object-oriented _module_* that gets mixed into the view somewhere in the stack and hopefully doesn’t collide namespaces?

Also, a cell typically embraces a small part of your UI. As this has a well-defined functionality you’d not mix in all helpers but only those you need. That reduces the number of “polluting” methods.

Another point against pollution is: When including a helper, it should ideally import the public helper methods, only. The internals and private methods should be in separate classes.

Actually, the only helper that does this is the @FormHelper@ that delegates @#form_for@ to the @FormBuilder@ class.

Please, blame Rails’ helper implementation for the pollution, not me 😉

h3. What About Real Decorators?

Don’t use a view model where you just need a simple helper. Use a decorator gem like “draper”: to decorate your model (and push that into the cell, if you like).

Use a cell view model when there’s rendering of markup involved. Cells help to clean up Rails hard-wired partial mess and allow clean testing of the encapsulated widgets.

Use a cell view model when the decorations are needed for a special widget, only, and not across your application.

And use a view model if you found the @#render_cell@ to clumsy and you wanted to invoke different methods on the cell instance.

h3. From Here…

The experimental view model feature is an attempt to move view logic – or, _helpers_ – into an object-oriented space while reducing its complexity.

You still got all of cells core behaviour like rendering views, nesting, inheritance across code and view level, OOP caching. Anyhow, you get an easy way to wire helper methods into your views without falling back into a procedural programming style from the 60s.

There will be problems with the way Rails helpers are programmed, and hopefully we can fix those finally making helpers predictable.

Give it a go, we can’t wait to hear your opinions about this new approach!


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s