#286 Draper
Clean up complex view logic using Draper. This gem provides decorators (much like presenters) which bundles view logic in an object oriented fashion. In this episode I do a step-by-step refactoring of a complex template into a decorator.
- Download:
- source code
- mp4
- m4v
- webm
- ogv
It's about time you got some financial reward for all your hard work.
I have an issue about this episode though. Surely the logic that is being placed in the decorator belongs in the model?
I see things like if there is no uploaded avatar then provide a default avatar as business logic and default values provided by the model can be used for xml, json responses, rake tasks etc...
I feel this approach goes against the grain and I feel that this sort of approach is a solution to a deeper problem which is that not enough developers know where to put their logic.
In other words I can't help thinking that this seems to be fixing symptoms of bad coding rather than providing a solution to the cause. We have models, why not use them?
Good point, there is some logic I can see going in the model here. Primarily the
full_name
vsusername
portion. Those are both attributes on the model and feels ugly in the presenter.The only other methods which don't have an HTML tag in them are
avatar_name
andmember_since
. In these cases, I ask myself, is there a case I may want to embed markup into the logic? I can see treating the default avatar image special, perhaps giving it a CSS class to make it look different, not linking it, etc. Same with themember_since
method. We may want to make the year italic for example.That said, this may be a bit of a pre-refactoring and doesn't fallow YAGNI since that markup requirement doesn't exist. I can see that point.
As for having similar logic in xml/json responses. Those are also views and can have their own presenters with their own set of logic. If there is duplication in logic then it's a sign it should go in the model or maybe a shared module.
How is putting something in the decorator any better than putting it right in the model class? The Presenter pattern is designed to abstract display logic from one (or several) model objects whose data model isn't much like the view. That's not what's going on here, as far as I can tell. Looks rather like you're reinventing helpers.
Now, there may be some call for doing that: as you point out, helpers are among the least satisfactory parts of Rails from an OO design perspective. But I tend to think this is not the right solution to that problem.
OTOH, I usually trust you, so I'm interested in hearing more about your rationale here.
...and looking at the 'cast again, I'm further conflicted. I'd be inclined to put something like
avatar_name
in its own partial (or even helper), but neither is satisfactory from an OO point of view, as I mentioned already. At the same time, I wouldn't want to put that much HTML generation in the model, I think, and Cells, while very promising, seems to be trying to solve a different problem (though I could well be wrong). Hmm.Update: I finally found a use case for Draper. I had a method that would choose a CSS class name for a particular model. It was too model-specific to put it in a helper, but too presentation-specific to feel good about putting it in the model. A presentation decorator turned out to be a good solution.
Any reason you chose Draper over Cells which has 116 votes? They appear to address the same issues.
Cells feels a bit heavy for this case being like mini controllers and views. I just want an object that I can place some view logic into.
Doesn't that logic go in the model?
I use both Cells and Draper. They're entirely different.
Wow, so this is the best day of my OSS-writing life.
Kevin: Cells is a much bigger idea than Draper. Draper's decorators are forcing Ruby objects into the existing view layer. Cells are almost self-contained MVC systems.
JamesW: Models are about data and business logic, not presentation. In your example, deciding on the default avatar is a presentation issue, not really business logic. For XML or JSON it would be more logical to return an empty string from the default avatar, rather than referencing an irrelevant image.
Joel: I still need to work on the CanCan-compatibility, sorry I haven't had much OSS time lately. If Ryan has any suggestions I'll jump to implement them.
Sounds neat, but all the examples look like a single resource. What about more complex examples; such as a User has_many Posts... In the view one might have a @user.posts.each |post| do ... how do you get a decorator object for those?
This is one reason I prefer keeping presenters in the view layer as I discuss in episode 287. It helps for cases like pagination.
In this specific case you could create a
UserDecorator#posts
method which loops through the posts, fetches the decorator for each one, and then renders a post partial with the decorator.Thanks Ryan (and Jeff!)
Just a small question: when refactoring a view, how to choose between using a partial, or using a decorator?
If you have more ERB tags than HTML tags, use a decorator. ;)
I also like to think about a partial being at an object scope (a _post partial) but a decorator method being at the attribute scope (a post.title method). It's finer bits of logic.
Ryan,
Great episode as always. I had a question about this line:
h.content_tag :span, "None given", class: "none"
What is the difference between :span (which I know is a symbol) and class:? Why is the colon after class?
It's a new syntax introduced in Ruby 1.9 for hashes that use symbols as keys.
{class: "none"}
is equivalent to{:class => "none"}
I'm curious: why the preference for h.content_tag, instead of a little snippet of HTML ?? Is there a reason, aside from aesthetics?
You also need to worry about HTML escaping when putting HTML directly in a string. The
content_tag
method handles that for us, unless we're doing something more with that string."<span class='none'>None given</span>".html_safe
just feels ugly.Caution: When using decorators, all your routes are accessibles when prefixed with the h method.
PressReleaseDecotator.rb
h.press_release_path(model)
But if you set default url options in your ApplicationController, they will not be apply. In my case, it's the locale setting...
ApplicationController.rb
def default_url_options(options={})
{:locale => I18n.locale}
end
Thanks for another great episode Ryan!
What would your thoughts on using decorators to show/hide certain information based on the current user's permissions (eg. with CanCan) - by doing something like:
within the decorator? A use case might be a member directory where non-members can only see members name but logged-in members can see each others email addresses.
Maybe it's a bit of an edge case? Do you think it would appropriate to put authorisation logic in a decorator?
Glad to see this - we've started using Draper at Tribesports recently, and we're really happy with it. We wrote a couple of blog posts on how it can help implement a clean, versioned API, if anyone's interested:
Part 1 - Versioning the Tribesports API
Part 2 - Separating API logic with decorators
We're actually using it in combination with Cells (mentioned above); we think they're highly complementary. Our site makes heavy use of small resource-specific elements repeated in multiple contexts. Cells lets us pick out the resources we're displaying in a structured manner (cleaning up our controllers), and draper handles the decoration, cleaning up the views.
Thanks for the great episode!
I have a question. If my user model has_one appointment model, both of which have decorators declared, is there a way to call the decorator for appointment from the user? like user.appointment.avatar ? or maybe user.appointmentDecorator.avatar ?
Like Ryan said about decorating a User - Posts assocation, you just have to create a method UserDecorator#appointment with AppointmentDecorator.new(model.appointment).
yes, but when you want to keep the lazy loading behaviour? doing that will hit the database immediately (well, has_many association is more problematic than belongs_to) what about that?
I think you have to eagerload all associations needed by your view in you controller:
Then there should be no extra query when calling user_decorator.appointment.
but the appointment will be of class Appointment not AppointmentDecorator, or?
The ActiveRecord association User-Appointment is wrapped by a decorator through UserDecorator#appointment mentioned above, so you get a AppointmentDecorator when calling @user.appointment
Thanks Ryan !!
The discussions in comments are also very informative, can't we just have a like options there for comments. Most of the times, it happens i don't have something to say, but just liked answers.
Agree, small red/green numbers in the corner of comment could simplify navigation.
@ryan how about using draper with gems like decent exposure. The easy way would be something like
how about a modified episode that show how by default to use decent exposure and Decorators.
Love the screen cast, they are my weekly dose of something awesome in the rails world.
Why not use the presence method to make the avatar_name method a 1-liner?
Very nice. I've been wanting to get rid of helpers in our application for a quite a while and this looks like a very clean and elegant solution.
i like the idea, but if i start refactoring with a decorator i come across one big problem.
if i wanna wrap a small method to a decorator like user.full_name i would have to make tons of decorators to wrap all my associations, like note.user.full_name, project.owner.full_name etc. well maybe its just realy for big view stuff. but i would love to use it also for the small parts.
+ cancan not liking it :)
Hey guys. Delighted there's now more Railscasts loveliness each week. Yay.
I'm a little confused about what to do with application wide presenters. For example, I want to show either a "Sign In" link or a "Log Out" link depending on whether the user's logged in. This is somewhere I would normally include some logic in the view, probably the app layout and it'd be great to move it to a presenter. But which one?
I'm playing with Sorcery following your recent episode, so I have a current_user object I can inspect to determine whether anybody's logged in. But it's not referenced through a model so I don't know how to move it to a decorator. I could decorate the user model but creating an @user instance in every view feels, well, odd.
Any guidance would be gratefully received.
I'm also curious about this. I would like to move template-related things from my ApplicationHelper into ApplicationDecorator (such as site-wide logo, sign in/out links, etc). I tried simply moving the code over, but then ran into problems trying to test and reference ApplicationDecorator.
If ApplicationDecorator is just meant to act as a global parent that all children will have access to, that's fair, but then to repeat darwalenator's question - what would be the appropriate place for such code?
What's the best way to test a decorator? More specifically, how would you test the following method (in rspec):
When I try the following, I get
undefined method 'number_to_currency' for nil:NilClass
:There's must be some magic going on that sets the view context somewhere. Anyone write decorator helpers for rspec yet?
Austin - look at this page
See "Integration with Rspec"...
What if instead of displaying "None Give" for missing fields you wanted to not render the field. That would mean something like
would have to become
or just
<%= @user.twitter %>
and have the presenter output the container markup, which I don't really like. Wondering if there was any cleaner way to do that? Thanks!
Maybe I would centrally put this method in ApplicationDecorator (not tested):
Then you have to call @user.element_if_present('your_scope.twitter', 'twitter') instead and also have the presenter output this container markup but only once in ApplicationDecorator.
is it possible to decoreate current_user ?
I put this in my application_helper.rb.
It does the trick quite nicely.
I am sure I am just not getting it, but it seems that decorators are just sweeping dirt under the rug. Every time I see changes like this that alter the way apps are written by making them more complex in the name of cleanliness, I feel that it is just one more thing that stands between me and delivering a product to my clients that can be handed off to other developers easily.
I think Jeff is a great guy.. We hung out in Salt Lake City way back and I realized the guy knows his stuff.. This is why I don't want to discount the idea in my head, but are we trying to solve a problem that might not need to be solved? And in turn, are we making it more complex?
I tend to store attributes like
status
orrole
as underscored strings (a string that would besuitable for a method/variable name) sometimes a simple .humanize will do the trick when
it comes to displaying that value in the UI user but other times you need to customize
them a bit which is one reason I18n is great
Here's my ApplicationDecorator which helps automate the usage of I18n for
such a purpose.
https://gist.github.com/1338134
What's the most elegant way to use draper in common with the ancestry gem from episode #262? How do I get all children and parents easily decorated?
I've found a solution: http://stackoverflow.com/questions/10870306/how-should-i-use-draper-in-my-applicationcontroller
The generator for creating decorators is now called just
decorator
. Thus, userails g decorator user
in place ofrails g draper:decorator user
I'm using draper and in my controller's index action, i have following code
and in my view
and I have a partial, _post.html.erb
I want to access methods defined in PostDecorator and UserDecorator in my partial
like
post.post_title
What should i need to do to pass @posts variable into my _post partial and access decorator methods
I tried following but failed
<%= render @posts, locals: { post: @posts} %>
and
<%= render @posts, object: @posts %>
Please help me
Thanks :)
Since I've started using Draper, I've generally been pretty happy with it. The one thing that keeps biting me is object equality (
decorated_object != object
) and in general code that expects to be dealing with an actual instance of the model class and not an instance of the decorator behaves differently or incorrectly when your object isn't actually an instance of your model class.If
@user
is aUserDecorator
object andcurrent_user
is aUser
object, for example, then even a simple test like this:doesn't work as expected.
Have also had some occasional odd behavior when working with CanCan. (To get error messages to work as expected, for example I had to add a section for
user_decorator:
in addition touser:
in my locale file because it looks up messages based on the object's class.)I wish Draper worked by mixing in a module instead of creating a an entirely new class. Then a decorated
User
object, for example, would still be aUser
and object equality would work as expected.But I guess Draper was only intended to be used in the view layer, so my mistake was probably that I am decorating objects in the controller as soon as I fetch them from the database. Maybe I will try to only create and use the decorated objects in the view and see if that works any better...
From the documentation it is possible to include draper as a module:
https://github.com/drapergem/draper#making-models-decoratable
I have had the same issue with equality. I ended up letting Struct solve the equality issues for me. This preserves equality for usage with arrays, hash (keys), sets etc..
The example below decorates ServiceType with start_date & end_date from another model:
Code is upgraded to Rails 4.2.4 and latest draper version : https://github.com/bparanj/draper