I've been researching the 'recommended' way to use Rails view helpers (e.g. link_to, content_tag) in a plain ruby class, such as a presenter. It seems there's very little information on this front and I wanted to get an idea of what the Stack community thought.
So, the options we have are.. (note I'm using Rails 4, and am less concerned about older versions)
Include the required modules manually
This is probably the cleanest way, since only the helpers needed are included. However I have found this method to not work in some cases, as the usual view context provided in plain Rails helpers is configured for the current request. url_for wouldn't know about the current request for example, so the host might not match.
class MyPresenter
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::CaptureHelper
def wrapped_link
content_tag :div, link_to('My link', root_url)
end
end
Use ActionController::Base.helpers
Since Rails 3, ActionController::Base has included a helpers method to access the current view context. I believe the view context provided by this method is configured as it would be in a rails helper, but I might be wrong. There's not really any documentation about this which seems worrying, but it does work quite well in practice.
class MyPresenter
def wrapped_link
h.content_tag :div, h.link_to('My link', h.root_url)
end
protected
def h
ActionController::Base.helpers
end
end
I believe this view context can also be mixed in with include, but the rails view helpers have hundreds of methods and it feels dirty to include them all indiscriminately.
Inject the view context when calling the presenter
Finally, we could just pass the view context to the class when it's initialized (or alternatively in a render method)
class MyPresenter
attr_accessor :context
alias_method :h, :context
def initialize(context)
#context = context
end
def wrapped_link
h.content_tag :div, h.link_to('My link', h.root_url)
end
end
class MyController < ApplicationController
def show
# WARNING - `view_context` in a controller creates an object
#presenter = MyPresenter.new(view_context)
end
end
Personally I tend to lean towards the latter two options, but with no definitive answer from the Rails team (that I've been able to find) I felt a bit unsure. Who better to ask than Stack!
I would go with the mix of the second and third option, something like:
class MyPresenter
def initialize(helpers)
#h = helpers
end
def wrapped_link
h.content_tag :div, h.link_to('My link', h.root_url)
end
private
attr_reader :h
end
Your second option require all your unit tests to be stubbed as ActionController::Base.helpers which maybe isn't a good option and your third option you're using a huge context to access just some methods.
I would really make that dependent on what kind of methods you use. If it's just the basics like content_tag etc. I would go for the ActionController::Base.helpers way. It is also possible to call some helpers directly, e.g. for paths inside models I almost always use something along the lines of Rails.application.routes.url_helpers.comment_path.
For controller-specific stuff the third option might be useful, but personally the "pure" way seems nicer. Draper has an interesting approach too: They save the view_context for the current request and then delegate the calls to h-helpers to it: https://github.com/drapergem/draper/blob/master/lib/draper/view_context.rb
It really is just a matter of preference. I would never include all helpers at once, as you already said. But the second option is quite nice if you want to build the presentation layer yourself without using a gem like Draper or Cells.
Related
I want to define view helpers in Rails 5.2.0 on runtime (from within some code that lies within my lib folder and / or some initializer) and I came up with this approach so far:
def new_module
Module.new do
def self.create_method(name, &block)
define_method(name, &block)
end
end
end
def define_dynamic_helper(name, &block)
helpers = new_module
helpers.create_method(name, &block)
ActionView::Base.send :include, helpers
end
Now that I can define dynamic modules that get include into ActionView::Base on runtime, I call them e.g. in my controller like this:
define_dynamic_helper("my_helper") do
"some data"
end
And my view uses the helper like this
<%= my_helper %>
But this has a drawback during development: When I remove the line that defines my helper, it is still available but I would expect a MethodMissing error. And as you can guess, this can lead to very complicated situations to debug.
So I got two questions here:
Is it possible to completely remove all dynamic helpers when Rails does a reload during development? Is there some kind of hook I can use?
Is using ActionView::Base.send :include, helpers the right approach for this? Or is there another entry point that I could use (which maybe provides a better reloading approach?)
Within a Plain Old Ruby Object (PORO) in my rails app: I have the following method:
def some_method
content_tag(:li, link_to("Do something", somewhere_path(object.id)))
end
First: the object didn't understand the method content_tag, so I added the following which made the object understand that method:
include ActionView::Helpers::TagHelper
Then the object didn't understand link_to so I added the following which made the object understand that method:
include ActionView::Helpers::UrlHelper
Now, it doesn't understand my route: somewhere_path(object.id).
Question: How can I make the PORO in my rails app understand the helpers which generate routes?
Followup Question: Is there an easier way to include all of this functionality into my PORO object? Perhaps there is a way to only include one major module and get all of this functionality (as opposed to perhaps needing to require 3 different modules).
You either have to do what you describe in your self-answer (link to revision I refer to), or inject some context into your POROs. Where context is something which knows all those methods. Something like this:
class ProjectsController
def update
project = Project.find(params[:id])
presenter = Presenters::Project.new(project, context: view_context) # your PORO
# do something with presenter
end
end
And your PORO would look like this:
module Presenters
class Project
attr_reader :presentable, :context
def initialize(presentable, context:)
#presentable = presentable
#context = context
end
def special_link
context.somewhere_path(presentable)
end
end
end
Me, I like neither of them. But sometimes we have to choose a lesser evil.
If anyone happens to know of a current way to get access to all of these methods with one include statement then let me know.
Why, yes. There is a way.
module MyViewCompatibilityPack
include ActionView::Helpers::TagHelper
include ActionView::Helpers::UrlHelper
def url_helpers
Rails.application.routes.url_helpers
end
end
class MyPoro
include MyViewCompatibilityPack
...
end
The issue is that actionview-related methods are not available to POROs.
In order to get all the great stuff from actionview: you need to utilize the view_context keyword. Then: you can simply call upon actionview-related methods from your view_context:
class BuildLink
attr_accessor :blog, :view_context
def initialize(blog, view_context)
#blog = blog
#view_context = view_context
end
def some_method
content_tag(:li, link_to(“Show Blog“, view_context.blog_path(blog)))
end
end
So for example: from your controller you would call upon this PORO like so:
BuildLink.new(#blog, view_context).some_method
For more information, see below references:
Rails doc on view_context
Utilization of view_context via presenter pattern, shown in this article
Railscast which talks through utilizing view_context via presenter pattern
Is there a proper place for helper methods for models in Rails? There are helper methods for controllers and views, but I'm not sure where the best place to put model helper methods. Aside from adding a method to ActiveRecord::Base, which I'd prefer not to.
UPDATE: It seems Concerns make a lot of sense. Here's an example of what I want. Certain models can never be deleted, so I add a callback that always throws an exception:
before_destroy :nope
def nope
raise 'Deleting not allowed'
end
With concerns, I could do something like this?
class MyModel < ActiveRecord::Base
include Undeletable
end
module Undeletable
extend ActiveSupport::Concern
included do
before_destroy :nope
end
def nope
raise 'Deleting not allowed'
end
end
Is this the Rails way of doing this?
If you want to use a helper_method my_helper_method inside a model, you can write
ApplicationController.helpers.my_helper_method
If you need a bit more flexibility, for example if you also need to override some methods, you can do this:
class HelperProxy < ActionView::Base
include ApplicationController.master_helper_module
def current_user
#let helpers act like we're a guest
nil
end
def self.instance
#instance ||= new
end
end
and then use with
HelperProxy.instance.my_helper_method
If you have strong nerves, you can also try to include the ApplicationController.master_helper_module directly into your model.
via : makandracards's post.
For your reference: http://railscasts.com/episodes/132-helpers-outside-views
If what you are asking is where to put code that is shared across multiple models in rails 4.2, then the standard answer has to be to use Concerns: How to use concerns in Rails 4
However, there are some good arguments (e.g. this) to just using standard rails module includes, and extends as marek-lipka suggests.
I would strongly recommend NOT using ApplicationController helper methods in a model, as you'll be importing a lot unnecessary baggage along with it. Doing so is usually a bad smell in my opinion, as it means you are not separating the MVC elements, and there is too much interdependency in your app.
If you need to modify a model object by adding a method that is just used within a view, then have a look at decorators. For example https://github.com/drapergem/draper
I'm trying to implement Decorators using the learnings from "Rails 4 Patterns" Code School course, but I'm running into trouble as I need a view helper in the Decorator class.
I want my view to have:
<%= #model_decorator.previous %>
Then in the decorator:
def previous
if object.prev_item.nil?
"Previous"
else
link_to("Previous", object)
end
end
The course suggests you make a call to the decorator within your view helper in the view file itself, but that's no good if the logic could output one result with a helper and one without. (i.e. need the output to be a link or not).
I've tried using helpers.link_to but it errors out as not providing the correct information for the url_for option. I've confirmed link_to("Previous", object) works within the view itself.
For Rails 4
include ActionView::Helpers::UrlHelper
link_to("Previous", Rails.application.routes.url_helpers.send("#{object.class.name.underscore}s_path".to_sym, object))
As for me it`s better to make a decorator for it:
class LinkDecorator
include ActionView::Helpers::UrlHelper
def initialize(label, object)
#label = label
#object = object
end
def show
link_to(label, url_helpers.send("#{object.class.name.underscore}s_path".to_sym, object))
end
def index
link_to(label, url_helpers.send("#{object.class.name}s_path".to_sym))
end
...
private
attr_reader :label, :object
def url_helpers
Rails.application.routes.url_helpers
end
end
Example usage:
LinkDecorator.new(object.name, object).show
If I understand your problem correctly, you essentially want links in a plain old ruby object.
My solution would be this:
include ActionView::Helpers::UrlHelper
link_to("Previous", Rails.application.routes.url_helpers.objects_path(object))
# assuming the object is always of one class
If the object is of a different class, than it would be possible to use the .send method to send the correct message to app ie.:
include ActionView::Helpers::UrlHelper
link_to("Previous", Rails.application.routes.url_helpers.send("#{object.class}s_path".downcase.to_sym, object))
# I'd create a function out of that line to make it a bit neater
It sounds like the error thrown by url_for comes from missing the routes and there's a few ways to include those. My solution kinda avoids that problem by using Rails.application.routes.url_helpers. Hope this helps!
Some recommendation [1] suggest you to use
<%= current_user.welcome_message %>
instead of
<% if current_user.admin? %>
<%= current_user.admin_welcome_message %>
<% else %>
<%= current_user.user_welcome_message %>
<% end %>
But the problem is you must have the decision logic somewhere in your code.
My understanding is putting the decision in template is better than controller as it make your controller more clean. Is it correct?
Are there better way to handle this?
http://robots.thoughtbot.com/post/27572137956/tell-dont-ask
You are not the first to wonder this. If views and controllers should have little to no logic, and the model should be presentation agnostic, where does presentation logic belong?
Turns out we can use an old technique called the decorator pattern. The idea is to wrap your model object with another class that contains your presentation logic. This wrapper class is called the decorator. The decorator abstracts away logic from your view, while keeping your models isolated from their presentation.
Draper is an excellent gem that helps define decorators.
The sample code you gave could be abstracted like so:
Pass a decorator to the view with #user = UserDecorator.new current_user in your controller.
Your decorator could look as below.
class UserDecorator
decorates :user
def welcome_message
if user.admin?
"Welcome back, boss"
else
"Welcome, #{user.first_name}"
end
end
end
And your view would simply contain #user.welcome_message
Notice that the model itself doesn't contain the logic to create the messages. Instead, the decorator wraps the model and translates model data into a presentable form.
Hope this helps!
I would use a helper for this. Suppose you have to translate the welcome-message, based on some locale.
In the app/helper/user_helper.rb write
module UserHelper
def welcome_message(user)
if user.admin?
I18n.t("admin_welcome_message", :name => user.name)
else
I18n.t("user_welcome_message", :name => user.name)
end
end
end
and in your view you can then just write
<%= welcome_message(user) %>
Note that the decorator/presenter offers a really clean object-oriented approach, but imho using a helper is much simpler and sufficient.
No, you don't want any conditionals at all in the user class nor the controller. The point of that example on the blog post is to make reference to polymorphism, just good old fashioned OO design.
# in application_controller for example
def current_user
if signed_in?
User.find(session[:user_id])
else
Guest.new
end
end
#app/models/user.rb
class User
def welcome_message
"hello #{name}"
end
end
#app/models/guest.rb
class Guest
def welcome_message
"welcome newcomer"
end
end
...you get the idea.
Only, instead of littering your model with presentation-only methods, create a decorator that acts as a presenter:
require 'delegate'
class UserPresenter < SimpleDelegator
def welcome_message
"hello #{name}"
end
end
And now current_user looks like so:
# application_controller
def current_user
if signed_in?
UserPresenter.new(User.find(session[:user_id]))
else
Guest.new
end
end
Decorate the user model and add the welcome_message to it directly. Yes, this may involve some kind of conditional statement at some point.
http://robots.thoughtbot.com/post/14825364877/evaluating-alternative-decorator-implementations-in
In my opinion, if the text is the only thing that changes, it doesn't belong in a view. If you needed to restructure the page, that's presentation logic. This, this is just data being different.
I think you should watch the railscasts episode on Presenters for the answer.
Logic in the view is hard to maintain, we should put the business logic in the model and all view logic in helpers.
If you want your code to be in Object Oriented fashion, make use of Decorators (object oriented way of helpers)
Best Example : https://github.com/jcasimir/draper
Put the code defining current_user.welcome_message in _app/helpers/application_helper.rb_, it will then be accessible by any view rendered with the application layout.
Another option is to define a custom helper module, one which is not necessarily associated with a given view or controller (See the video I linked below), and include it in the modules of the view/controllers you wish to have that functionality in.
This is not something that is black and white. But, from what you have described it sounds like this is code that is obtrusive to stick in your application_controller.rb and it is not code with functionality which justifies it's own controller, the most effective and efficient option may be to create a custom helper module and include it in the helpers you wish to have that functionality. That said, this is ultimately a judgement call which the designer of the application (i.e. you) needs to decide upon.
Here is a good article outlining helper modules from May, 2011
Here is is a RailsCast outlining custom helper modules (i.e. custom as in modules not necessarily associated with a given controller or view). Short, sweet, and to the point.
You can define helper method for that stuff. I don't think it is a good Idea to make a welcome sentences in a model, but in the controller too. But you should try to make you views clean from code, and if you can use helpers for that then you should to.
A good practice would be to have real View instances. Rails parody of MVP (there is difference, look it up) unfortunately seems to pretend that views are templates. That is wrong.
Views are supposed to contain the presentation logic in MVC and MVC-inspired patterns. They are also supposed to manipulate multiple templates and make decision on which templates to employ to represent the state and information from the model layer (yes, model is a layer not an ORM instance).
So, to answer the question: presentation logic has no place in controllers.