3

I'm creating a mobile site next to our normal html site. Using rails 3.1. Mobile site is accessed in subdomain m.site.com.

I have defined mobile format (Mime::Type.register_alias "text/html", :mobile).

In ApplicationController I have "before_filter :mobile_site_before_filter" that recognizes mobile site and sets format according to it.

def mobile_site_before_filter
  if request.subdomains.first == 'm'
    original_format = request.format
    request.format = :mobile
    @mobile_site = true
    request.formats.push(original_format)
  end
end

In layouts I have 'main.html.erb' and 'main.mobile.erb'. So with mobile sites mobile layout is used.

Now it kind of works ok.

In UserController I have index action which chooses index.html.erb or index.mobile.erb automatically. No extra coding in top of mobile-view needed. Success.

But I have lots of other views where the same template could be used to serve inside mobile layouts but with minor changes.

E.g. in MessagesController the same view would be almost fine for mobile

In index.html.erb
Normal user info, common for mobile and html
<%= render(:partial => 'messages') %>

Rendering messages.html.erb, no need for messages.mobile.erb. mobile view can be done with css

<%# By default self.formats = [:html] because this is .html.erb partial, even in mobile site %>
<%= self.formats = [:mobile, :html] if @mobile_site # How to get rid of this? %>
<%= render(:partial => 'vote_form') %>
<!-- rest of page ... -->

Now i want to render vote_form.[mobile|html].erb depending on the site...

Now the index.html.erb partial would be fine to use with mobile if I could just choose between vote_form.html.erb or vote_form.mobile.erb. I can choose to use mobile partial with using "self.formats = [:mobile, :html] if @mobile_site" in the beginning of index.html.erb . But it feels stupid to write this in beginning of all templates.

So the question is:

  • Where does the self.formats come in views (what sets it originally) and how can I set that it's always [:mobile, :html] inside the mobile site? Why isn't it the same as i set in controller before_filter? Can I set it somehow in the controller already?

(minor bonus questions)

  • Is there something wrong in this approach? Using mostly the same html-views and in some specific cases using mobile.erb views instead. Why doesn't this work by default in rails?

  • Is there other ways to choose to render :mobile views if found with fallback to normal html view? Even across the partials so that html.erb-view tries to use _partial.mobile.erb partials.

Community
  • 1
  • 1
holli
  • 1,546
  • 16
  • 17

5 Answers5

4

How can I set that it's always [:mobile, :html] inside the mobile site? Can I set it somehow in the controller already?

Yes.

Here's a simple solution, but it's a bit gross.

class ApplicationController
    ...
    def formats=(values)
        values << :html if values == [:mobile]
        super(values)
    end
    ...
end

It turns out Rails (3.2.11) already adds an :html fallback for requests with the :js format. Here's ActionView::LookupContext#formats=

# Override formats= to expand ["*/*"] values and automatically
# add :html as fallback to :js.
def formats=(values)
  if values
    values.concat(default_formats) if values.delete "*/*"
    values << :html if values == [:js]
  end
  super(values)
end

So you can override formats yourself and it will be conceivably no more gross and hacky than the existing Rails implementation.

Where does the self.formats come in views (what sets it originally)

ActionController::Rendering#process_action assigns the formats array from the request (see action_controller/metal/rendering.rb)

# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
  self.formats = request.formats.map { |x| x.ref }
  super
end

Why isn't it the same as i set in controller before_filter?

It's not the same as what you set in your before_filter because before filters are run before #process_action (as you'd expect). So whatever you set gets clobbered by what #process_action pulls off the request.

Will Madden
  • 6,477
  • 5
  • 28
  • 20
4

I think I've found the best way to do this. I was attempting the same thing that you were, but then I remembered that in rails 3.1 introduced template inheritance, which is exactly what we need for something like this to work. I really can't take much credit for this implementation as its all laid out there in that railscasts link by Ryan Bates.

So this is basically how it goes.

Create a subdirectory in app/views. I labeled mine mobile.

Nest all view templates you want to override in the same structure format that they would be in the views directory. views/posts/index.html.erb -> views/mobile/posts/index.html.erb

Create a before_filter in your Application_Controller and do something to this effect.

 before_filter :prep_mobile
 def is_mobile?
   request.user_agent =~ /Mobile|webOS|iPhone/
 end 
 def prep_mobile
   prepend_view_path "app/views/mobile" if is_mobile?
 end

Once thats done, your files will default to the mobile views if they are on a mobile device and fallback to the regular templates if a mobile one is not present.

Justin Herrick
  • 2,981
  • 17
  • 18
  • Cool. This is probably the best way to go if you have many files. With only couple of files its a bit annoying that files are in different folders. Might make it harder to see what partial is used. – holli Feb 29 '12 at 10:21
  • In case you are interested, I created [a gem](https://github.com/matthewrobertson/ress) that implements this solution (as well as adding a few more features). I also wrote up some more info about the strategy on [my blog](http://matthewrobertson.org/blog/2013/02/15/introducing-ress/) – Matthew Feb 20 '13 at 23:55
3

Rails 4.1 includes a pretty neat feature:

Variants

Allows you to have different templates and action responses for the same mime type (say, HTML). This is a magic bullet for any Rails app that's serving mobile clients. You can now have individual templates for the desktop, tablet, and phone views while sharing all the same controller logic.

Now you can do something like this:

class PostController < ApplicationController
  def show
    @post = Post.find(params[:id])

    respond_to do |format|
      format.json
      format.html               # /app/views/posts/show.html.erb
      format.html.phone         # /app/views/posts/show.html+phone.erb
      format.html.tablet do
        @show_edit_link = false
      end
    end
  end
end

You simply need to set the variant depending on your needs, for example within a before_filter:

class ApplicationController < ActionController::Base
  before_action :detect_device_variant

  private

    def detect_device_variant
      case request.user_agent
      when /iPad/i
        request.variant = :tablet
      when /iPhone/i
        request.variant = :phone
      end
    end
end

What's new in Rails 4.1

Community
  • 1
  • 1
S. A.
  • 3,714
  • 2
  • 20
  • 31
2

You can register new format for the whole application in your mime type initializers:

 Mime::Type.register_alias "text/html", :mobile

Now you can do something like this in your templates to specify format priority(see How do I render a partial of a different format in Rails?):

<% self.formats = [:mobile, :html] %>

In this case if there is mobile template it will be used for rendering with fallback to ordinary html template. Now you should only determine if user is browsing via mobile browser and conditionally execute this code. Or you can just assign formats value in ApplicationController filter so correct template will be chosen automaticaly.

UPDATE:

It seems like by this time there is no "legal" way to solve this problem using Rails. Here is unclosed issue in the rails repository. There you can find patch that can solve your problem, but it uses private Rails API, so it can be unstable.

Also you can try implement your own view resolver that possibly can solve the problem: http://jkfill.com/2011/03/11/implementing-a-rails-3-view-resolver/

Community
  • 1
  • 1
Volodymyr Rudyi
  • 638
  • 11
  • 25
  • Yeh this is what I have done. But the more problematic question is how to set it application wide in the controller before_filter. And so that even if you render .html.erb template. Inside the template it would choose .mobile.erb partials if they exist (without using self.formats in the beginning of all templates) – holli Jan 14 '12 at 16:14
  • Damn, almost missed your update. That's correct. Thank you for finding the issue and links. Nice to know. Although effort/value wise I'll probably stick with <% self.formats = [:mobile, :html] if mobile_site? %> in beginning of all templates. In case someone else wonders about the coding. The monkeypatch in the issue seems ok. And originally its PathResolver which sets the formats in/for the view (https://github.com/rails/rails/blob/859a4556dd3374f893e0a756434dc41f0574fedf/actionpack/lib/action_view/template/resolver.rb#L135) – holli Jan 27 '12 at 16:56
0

Why don't you look into Responsive Web Design instead, use CSS and media queries to render the page. That way your controller need never know whether the view is mobile or not.

Hiltmon
  • 1,265
  • 9
  • 6
  • Responsive web design has its place, but it's not always an alternative for being able to tailor an interface to a mobile device. – markquezada Sep 30 '12 at 07:19