22

I'm using this code (taken from here) in ApplicationController to detect iPhone, iPod Touch and iPad requests:

before_filter :detect_mobile_request, :detect_tablet_request

protected

def detect_mobile_request
  request.format = :mobile if mobile_request?
end

def mobile_request?
  #request.subdomains.first == 'm'
  request.user_agent =~ /iPhone/ || request.user_agent =~ /iPod/
end

def detect_tablet_request
  request.format = :tablet if tablet_request?
end

def tablet_request?
  #request.subdomains.first == 't'
  request.user_agent =~ /iPad/
end

This allows me to have templates like show.html.erb, show.mobile.erb, and show.tablet.erb, which is great, but there's a problem: It seems I must define every template for each mime type. For example, requesting the "show" action from an iPhone without defining show.mobile.erb will throw an error even if show.html.erb is defined. If a mobile or tablet template is missing, I'd like to simply fall back on the html one. It doesn't seem too far fetched since "mobile" is defined as an alias to "text/html" in mime_types.rb.

So, a few questions:

  1. Am I doing this wrong? Or, is there a better way to do this?
  2. If not, can I get the mobile and tablet mime types to fall back on html if a mobile or tablet file is not present?

If it matters, I'm using Rails 3.0.1. Thanks in advance for any pointers.

EDIT: Something I forgot to mention: I'll eventually be moving to separate sub-domains (as you can see commented out in my example) so the template loading really needs to happen automatically regardless of which before_filter has run.

markquezada
  • 8,444
  • 6
  • 45
  • 52

15 Answers15

17

Possible Duplicate of Changing view formats in rails 3.1 (delivering mobile html formats, fallback on normal html)

However, I struggled with this exact same problem and came up with a fairly elegant solution that met my needs perfectly. Here is my answer from the other post.

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.

Community
  • 1
  • 1
Justin Herrick
  • 2,981
  • 17
  • 18
  • 4
    How can my question, which was asked over a year ago, be a possible duplicate of a question that was asked 1 month ago? Also, notice in the original question that Rails 3.0.1 was the current version. With that said though, this looks like a viable solution for Rails 3.1. – markquezada Mar 07 '12 at 02:41
  • 1
    Ah, I didn't check the date close enough, but more or less I meant that my comment was a duplicate of what I had said on another question. Sorry about the confusion. – Justin Herrick Mar 07 '12 at 03:32
4

You need to do several things to wire this up, but the good news is that Rails 3 actually makes this a lot simpler than it used to be, and you can let the router do most of the hard work for you.

First off, you need to make a special route that sets up the correct mime type for you:

# In routes.rb:
resources :things, :user_agent => /iPhone/, :format => :iphone
resources :things

Now you have things accessed by an iphone user agent being marked with the iphone mime type. Rails will explode at you for a missing mime type though, so head over to config/initializers/mime_types.rb and uncomment the iphone one:

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

Now you're mime type is ready for use, but your controller probably doesn't yet know about your new mime type, and as such you'll see 406 responses. To solve this, just add a mime-type allowance at the top of the controller, using repsond_to:

class ThingsController < ApplicationController
  respond_to :html, :xml, :iphone

Now you can just use respond_to blocks or respond_with as normal.

There currently is no API to easily perform the automatic fallback other than the monkeypatch or non-mime template approaches already discussed. You might be able to wire up an override more cleanly using a specialized responder class.

Other recommended reading includes:

https://github.com/plataformatec/responders

http://www.railsdispatch.com/posts/rails-3-makes-life-better

raggi
  • 1,290
  • 9
  • 12
  • Your answer is better fit for the question: "How do I add a mobile mime type to rails?" (You'll notice that the question shows that I'm already doing this.) You don't address the real question which is how to fall back to the html mime type if a mobile template doesn't exist. A better approach is to use template inheritance instead of a custom mime type as described in Justin Herrick's answer. – markquezada Nov 05 '12 at 00:57
3

Trying removing the .html from the .html.erb and both iPhone and browser will fallback to the common file.

Joe
  • 31
  • 1
  • 1
    Hey Joe, thanks for the answer. This works, but seems a little strange since the new .erb file would be the default for *any* mime type not just mobile (html aliased) templates. Also, I thought I read somewhere that this was deprecated in Rails 3? – markquezada Nov 19 '10 at 07:27
3

I have added a new answer for version 3.2.X. This answer is valid for <~ 3.0.1.

I came to this question while looking to be able to have multiple fallbacks on the view. For example if my product can be white-labeled and in turn if my white-label partner is able to sell sponsorship, then I need a cascade of views on every page like this:

  • Sponsor View: .sponsor_html
  • Partner View: .partner_html
  • Default View: .html

The answer by Joe, of just removing .html works (really well) if you only have one level above the default, but in actual application I needed 5 levels in some cases.

There did not seem to be anyway to implement this short of some monkey patching in the same vein as Jeremy.

The Rails core makes some fairly wide ranging assumptions that you only want one format and that it maps to a single extension (with the default of NO extension).

I needed a single solution that would work for all view elements -- layouts, templates, and partials.

Attempting to make this more along the lines of convention I came up with the following.

# app/config/initializers/resolver.rb
module ActionView
  class Base
    cattr_accessor :extension_fallbacks
    @@extension_fallbacks = nil
  end

  class PathResolver < Resolver
    private
      def find_templates_with_fallbacks(name, prefix, partial, details)
        fallbacks = Rails.application.config.action_view.extension_fallbacks
        format = details[:formats].first

        unless fallbacks && fallbacks[format]
          return find_templates_without_fallbacks(name, prefix, partial, details)
        end

        deets = details.dup
        deets[:formats] = fallbacks[format]

        path = build_path(name, prefix, partial, deets)
        query(path, EXTENSION_ORDER.map {|ext| deets[ext] }, details[:formats])
      end
      alias_method_chain :find_templates, :fallbacks
  end
end

# config/application.rb
config.after_initialize do 
config.action_view.extension_fallbacks = {
  html: [:sponsor_html, :partner_html, :html],
  mobile: [:sponsor_mobile, :partner_mobile, :sponsor_html, :partner_html, :html]
}

# config/initializers/mime_types.rb
register_alias 'text/html', :mobile

# app/controllers/examples_controller.rb
class ExamplesController
  respond_to :html, :mobile

  def index
    @examples = Examples.all

    respond_with(@examples)
  end
end

Note: I did see the comments around alias_method_chain, and initially did make a call to super at the appropriate spot. This actually called ActionView::Resolver#find_templates (which raises a NotImplemented exception) rather than the ActionView::PathResolver#find_templates in some cases. I wasn't patient enough to track down why. I suspect its because of being a private method.

Plus, Rails, at this time, does not report alias_method_chain as deprecated. Just that post does.

I do not like this answer as it involves some very brittle implementation around that find_templates call. In particular the assumption that you only have ONE format, but this is an assumption made all over the place in the template request.

After 4 days of trying to solve this and combing through the whole of the template request stack its the best I can come up with.

Simon
  • 521
  • 2
  • 9
  • This worked in a clean rails app, but in my full application it is failing for a couple of reasons. So, this answer is not complete...yet. More soon. – Simon Mar 09 '11 at 14:14
  • Code edits put in. It should work as expected now. Note you do not need to add all the extensions in as mime_types, just the format. – Simon Mar 09 '11 at 16:10
  • Doesn't works with Rails 3 (the array in configuration is badly written and `undefined method `extension_fallbacks=' for ActionView::Base:Class`) – Dorian Dec 05 '11 at 01:03
  • I have been using this with Rails 3.0 since writing it...But, perhaps you are using Rails 3.1? I haven't tried it there yet. Given that this is hacking on private methods it is unlikely to survive major upgrades (like 3.1) without further hackery. We're going to start evaluating 3.1 in January...this will get updated then. – Simon Dec 05 '11 at 17:06
2

Here's a simpler solution:

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

It turns out Rails (3.2.11) adds an :html fallback for requests with the :js format. Here's how it works:

  • ActionController::Rendering#process_action assigns the formats array from the request (see action_controller/metal/rendering.rb)
  • ActionView::LookupContext#formats= gets called with the result

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

This solution is gross but I don't know a better way to get Rails to interpret a request MIME type of "mobile" as formatters [:mobile, :html] - and Rails already does it this way.

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

The way that I'm handling this is to simply skip_before_filter on those requests that I know I want to render the HTML views for. Obviously, that will work with partials.

If your site has a lot of mobile and/or tablet views, you probably want to set your filter in ApplicationController and skip them in subclasses, but if only a few actions have mobile specific views, you should only call the before filter on those actions/controllers you want.

developish
  • 21
  • 1
  • That's actually a really interesting (and useful!) approach. It would definitely work for many cases, but for my specific case (as you can see in the commented out part of my example) I'll eventually be moving to separate sub-domains for each type of device so I can use a more robust caching strategy. This means I really need the code that loads templates not to be dependent on the before filter. Thanks for your input. – markquezada Nov 19 '10 at 19:21
2

If your OS has symlinks you could use those.

$ ln -s show.html.erb show.mobile.erb
Samuel Danielson
  • 5,231
  • 3
  • 35
  • 37
2

I am adding another answer now that we have updated to 3.2.X. Leaving the old answer as it was in case someone needs that one. But, I will edit it to direct people to this one for current versions.

The significant difference here is to make use of the "new" (since 3.1) availability of adding in custom path resolvers. Which does make the code shorter, as Jeroen suggested. But taken a little bit further. In particular the #find_templates is no longer private and it is expected that you will write a custom one.

# lib/fallback_resolver.rb
class FallbackResolver < ::ActionView::FileSystemResolver
  def initialize(path, fallbacks = nil)
    @fallback_list = fallbacks
    super(path)
  end

  def find_templates(name, prefix, partial, details)
    format = details[:formats].first

    return super unless @fallback_list && @fallback_list[format]

    formats = Array.wrap(@fallback_list[format])
    details_copy = details.dup
    details_copy[:formats] = formats
    path = Path.build(name, prefix, partial)
    query(path, details_copy, formats)
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  append_view_path 'app/views', {
    mobile: [:sponsor_mobile, :mobile, :sponsor_html, :html],
    html: [:sponsor_html, :html]
  }
  respond_to :html, :mobile

# config/initializers/mime_types.rb
register_alias 'text/html', :mobile
Anton
  • 927
  • 10
  • 15
Simon
  • 521
  • 2
  • 9
  • This didn't work for me on Rails 3.2.3 After adding in the patch, I can't start webrick. I get the following error at the line in application_controller (for the append_view_path) gems/activeadmin-0.4.4/lib/active_admin/namespace.rb:191:in `eval': wrong number of arguments (2 for 1) (ArgumentError) from /Users/nikhil/Projects/niceniche/LocalNote3/app/controllers/application_controller.rb:7:in `' – Nikhil Gupte Jun 07 '12 at 19:17
1

Rails 4.1 includes Variants, this is a great feature that allow you to set different views for the same mime. You can now simply add a before_action and let the variant to do the magic:

before_action :detect_device_variant

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

Then, in your action:

respond_to do |format|
  format.json
  format.html               # /app/views/the_controller/the_action.html.erb
  format.html.phone         # /app/views/the_controller/the_action.html+phone.erb
  format.html.tablet do
    @some_tablet_specific_variable = "foo"
  end
end

More info here.

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

Yes, I'm pretty sure this is the right way to do this in rails. I've defined iphone formats this way before. That's a good question about getting the format to default back to :html if a template for iphone doesn't exist. It sounds simple enough, but I think you'll have to add in a monkeypath to either rescue the missing template error, or to check if the template exists before rendering. Take a look a the type of patches shown in this question. Something like this would probably do the trick (writing this code in my browser, so more pseudo code) but throw this in an initializer

# config/initializers/default_html_view.rb
module ActionView
  class PathSet

    def find_template_with_exception_handling(original_template_path, format = nil, html_fallback = true)
      begin
        find_template_without_exception_handling(original_template_path, format, html_fallback)
      rescue ActionView::MissingTemplate => e
        # Template wasn't found
        template_path = original_template_path.sub(/^\//, '')
        # Check to see if the html version exists
        if template = load_path["#{template_path}.#{I18n.locale}.html"]
          # Return html version
          return template
        else
          # The html format doesn't exist either
          raise e
        end
      end
    end
    alias_method_chain :find_template, :exception_handling

  end
end
Community
  • 1
  • 1
Jeremy
  • 4,880
  • 1
  • 19
  • 15
  • Although I'd rather not use a monkeypatch, this seems like a relatively clean solution. I read somewhere that this doesn't work for partials though. I'll try it out. Thanks. – markquezada Oct 31 '10 at 21:43
  • the alias_method_chain system is always not a clean way to do. And it's can be works only with this version maybe not with futur version :( – shingara Oct 31 '10 at 22:10
  • Yeah, this isn't guaranteed to work for future versions, although I took it from a 2.x example, and it still seems to apply in 3.0.1 which is promising. Here's the relevant documentation [ActionView::PathSet#find_template](http://apidock.com/rails/ActionView/PathSet/find_template) it looks like rails currently only defaults to :html if it was trying to use the :js format, instead of any other format, hence the need for a monkey patch. – Jeremy Nov 01 '10 at 01:32
  • Hmm... doesn't seem to work in 3.0.1... `alias_method': undefined method `find_template' for class `ActionView::PathSet' (NameError) – markquezada Nov 03 '10 at 10:20
  • Hmm, it's definitely there, see the documentation I linked to in my previous comment. However overloading like this requires that the original be defined first, so it could be load order if the initializer is getting loaded before the standard rails stack. Try moving the file to `lib/overrides.rb` I'm pretty sure lib files are loaded after the rest of the code. – Jeremy Nov 03 '10 at 10:28
  • Actually, if you look at the documentation you linked to, it's not in the rails 3.0 source. (Only up to version 2.3.8 is highlighted.) According to [this question](http://stackoverflow.com/questions/3689736/rails-3-alias-method-chain-still-used) it looks like alias_method_chain has been deprecated in Rails 3 in favor of using super. Conceptually though, this is probably still the right approach. – markquezada Nov 03 '10 at 19:44
  • Ah, right you are. Whoops, meant to use up to date code. Good to know alias_method_chain is deprecated, I actually hadn't heard that yet – Jeremy Nov 03 '10 at 22:40
  • Need to add one more line like `elsif template = load_path["#{template_path}.html"] \n return template` in order to handle non-I18n templates. You might want to add some more `elsif` branches from [ActionView::PathSet#find_template](http://apidock.com/rails/ActionView/PathSet/find_template). – arnep Jul 01 '13 at 08:25
  • I'm not sure I would recommend this at all in Rails 3 - it's very heavily tied to the rails internal implementation and liable to break between versions. This answer was written for rails 2, and as the currently accepted answer illustrates, there are much more elegant solutions available these days. – Jeremy Jul 04 '13 at 00:50
1

Here is another example of how to do it, inspired by Simon's code, but a bit shorter and a bit less hacky:

# application_controller.rb
class ApplicationController < ActionController::Base
  # ...

  # When the format is iphone have it also fallback on :html
  append_view_path ExtensionFallbackResolver.new("app/views", :iphone => :html)

  # ...
end

and somewhere in an autoload_path or explicitly required:

# extension_fallback_resolver.rb
class ExtensionFallbackResolver < ActionView::FileSystemResolver

  attr_reader :format_fallbacks

  # In controller do append_view_path ExtensionFallbackResolver.new("app/views", :iphone => :html)
  def initialize(path, format_fallbacks = {})
    super(path)
    @format_fallbacks = format_fallbacks
  end

  private

    def find_templates(name, prefix, partial, details)
      fallback_details = details.dup
      fallback_details[:formats] = Array(format_fallbacks[details[:formats].first])

      path = build_path(name, prefix, partial, details)
      query(path, EXTENSION_ORDER.map { |ext| fallback_details[ext] }, details[:formats])
    end

end

The above is still a hack because it is using a private API, but possibly less fragile as Simon's original proposal.

Note that you need to take care of the layout seperately. You will need to implement a method that chooses the layout based on the user agent or something similar. The will only take care of the fallback for the normal templates.

Jeroen van Dijk
  • 1,029
  • 10
  • 16
  • 1
    `uninitialized constant MyApplication::Application::ExtensionFallbackResolver` avec Rails 3.1 – Dorian Dec 05 '11 at 01:06
0

I need the same thing. I researched this including this stack overflow question (and the other similar one) as well as followed the rails thread (as mentioned in this question) at https://github.com/rails/rails/issues/3855 and followed its threads/gists/gems.

Heres what I ended up doing that works with Rails 3.1 and engines. This solution allows you to place the *.mobile.haml (or *.mobile.erb etc.) in the same location as your other view files with no need for 2 hierarchies (one for regular and one for mobile).

Engine and preparation Code

in my 'base' engine I added this in config/initializers/resolvers.rb:

    module Resolvers
      # this resolver graciously shared by jdelStrother at
      # https://github.com/rails/rails/issues/3855#issuecomment-5028260
      class MobileFallbackResolver < ::ActionView::FileSystemResolver
        def find_templates(name, prefix, partial, details)
          if details[:formats] == [:mobile]
            # Add a fallback for html, for the case where, eg, 'index.html.haml' exists, but not 'index.mobile.haml'
            details = details.dup
            details[:formats] = [:mobile, :html]
          end
          super
        end
      end
    end

    ActiveSupport.on_load(:action_controller) do
      tmp_view_paths = view_paths.dup # avoid endless loop as append_view_path modifies view_paths
      tmp_view_paths.each do |path|
        append_view_path(Resolvers::MobileFallbackResolver.new(path.to_s))
      end
    end

Then, in my 'base' engine's application controller I added a mobile? method:

    def mobile?
        request.user_agent && request.user_agent.downcase =~ /mobile|iphone|webos|android|blackberry|midp|cldc/ && request.user_agent.downcase !~ /ipad/
    end

And also this before_filter:

    before_filter :set_layout

    def set_layout
      request.format = :mobile if mobile?
    end

Finally, I added this to the config/initializers/mime_types.rb:

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

Usage

Now I can have (at my application level, or in an engine):

  • app/views/layouts/application.mobile.haml
  • and in any view a .mobile.haml instead of a .html.haml file.

I can even use a specific mobile layout if I set it in any controller: layout 'mobile'

which will use app/views/layouts/mobile.html.haml (or even mobile.mobile.haml).

Mike P.
  • 198
  • 1
  • 12
0

You can in this case for the format to html. By example you want always use the html in user show method

class UserController

  def show
    ..your_code..
    render :show, :format => :html
  end
end

In this case, if you request show on User controller you render all the time the html version.

If you want render JSON too by example you can made some test about your type like :

class UserController

  def show
    ..your_code..
    if [:mobile, :tablet, :html].include?(request.format)
      render :show, :format => :html
    else
      respond_with(@user)
    end
  end

end
shingara
  • 46,608
  • 11
  • 99
  • 105
  • That doesn't seem to achieve what he's looking for though. He wants to add :iphone formats by simply including an .iphone.erb template, which works, but then not have to explicitly render html every *other* request. Wouldn't this example force all responses to use the html templates? – Jeremy Oct 31 '10 at 11:02
  • I have no example of that and I don't know if it's really possible. My technique is a workaround – shingara Oct 31 '10 at 11:03
0

I made a monkey patch for that, but now, I use a better solution :

In application_controller.rb :

layout :which_layout

def which_layout
  mobile? ? 'mobile' : 'application'
end

With the mobile? method you can write.

So I have a different layout but all the same views, and in the mobile.html.erb layout, I use a different CSS file.

Dorian
  • 22,759
  • 8
  • 120
  • 116
-2

I solved this problem by using this before_filter in my ApplicationController:

def set_mobile_format
  request.formats.unshift(Mime::MOBILE) if mobile_client?
end

This puts the mobile format to the front of the list of acceptable formats. So, the Resolver prefers .mobile.erb templates, but will fall back to .html.erb if no mobile version is found.

Of course, for this to work you need to implement some kind of #mobile_client? function and put Mime::Type.register_alias "text/html", :mobile into your config/initializers/mime_types.rb

Jan
  • 950
  • 7
  • 14
  • No rails version specified for this fix. Tried in 3.0.1. Dosen't work request.formats is returning an array `[text/html]` with proper `Mime::Type.register_alias "text/html", :mobile` in place. no way to unshift that – kevzettler Apr 28 '12 at 03:56
  • This **works** in Rails 3.2.1 and 3.0.8.Please see for yourself by cloning and running https://github.com/janv/test_mobile. It does not work for layouts but I don't think that's a problem. – Jan Apr 29 '12 at 12:33