thinksimple.pl

jsonp

June 17, 2009

I have an external application which does its job quite well. It was not written by me, and I feel it’s too complicated for me to mess with it, but lately we had an idea to display a couple of blog articles on the main page. It has been done loads of times, all you need is a simple API, Rails creates that API even in scaffold, I’ll just put an AJAX call on the main page and that’s all, right?

Well, no. There is something called Same Origin Policy (SOP) which only allows AJAX requests to proceed if the called URL is the same as website URL. Obviously, that will not be the case, because our application and the blog are two separate systems, and they are not running under the same URL. Bummer.

Okay then, how do Google Ads work? They’re not on the same domain, and they’re based on Javascript, so I should be able to do it the same way, right?

Um, kind of. It’s probably doable, but it’s messy. It means lots and lots <script> tags, and it’s ugly.

Another option is always messing in the controllers and getting XML from the blog, and trying to display it somehow – will take way too much time as I’d have to learn everything from the basics. Cost-benefit analysis says no.

What do we do then? There is a thing called JSONP. You can read in detail about it on this page, but basically it’s like saying

<script src="http://example.com/some_script.js"></script>

only different :)

Let’s just show some examples. I have a blog, and I would like to show my posts titles on some other site. My blog responds to this call:

// http://thinksimple.pl/entries/json.js?count=3
([
  {"link": "http://thinksimple.pl/entries/77-Different-submenu-depending-on-controller", 
  "title": "Different submenu depending on controller", 
  "id": 77}, 
  {"link": "http://thinksimple.pl/entries/74-RubyMine-1-1", 
   "title": "RubyMine 1.1", 
   "id": 74}, 
  {"link": "http://thinksimple.pl/entries/72-mysqldump", 
   "title": "mysqldump", 
   "id": 72}
])

(it really does, you can check it!)
That’s cool, but I want to get this data and display it nicely. It would be great if I could create some javascript function in the external app and feed it with the received data. Ok, let’s do it:

<div id="entries"></div>
<script type="text/javascript">
  // jQuery
  $.getJSON(blog_url + '/entries/json.js?count=3&jsoncallback=?', 
        function(data) {
          for (var i in data) {
            $('#entries').append('<p><a href="' 
              + data[i]['link'] + '">' 
              + data[i]['title'] + '</a>'
              + '</p>');
          }
  });
</script>

So I ask for http://thinksimple.pl/entries/json.js?count=3&jsoncallback=jsonp1245158265283 and I get

jsonp1245158265283([
  {"link": "http://thinksimple.pl/entries/77-Different-submenu-depending-on-controller", 
  "title": "Different submenu depending on controller", 
  "id": 77}, 
  {"link": "http://thinksimple.pl/entries/74-RubyMine-1-1", 
   "title": "RubyMine 1.1", 
   "id": 74}, 
  {"link": "http://thinksimple.pl/entries/72-mysqldump", 
   "title": "mysqldump", 
   "id": 72}
])

My blog calls the function that I’ve prepared in my other app! Great, now my problem is solved, no need to dig into an unknown application anytime soon :)

Different submenu depending on controller

June 10, 2009

I’ve been asked lately how to create different submenus for each of the controllers in your application. I thought I’d share my ideas.

The first idea is to create a partial with submenu code (or a couple of partials) and set the proper name in the controller. In every controller we define a method set_submenu_partial which sets a @partial variable. We can then use the variable in the layout file.

<% # layout %>
<%= render :partial => @partial %>
# controllers
ProjectsController < ApplicationController
  before_filter :set_submenu_partial

  def set_submenu_partial
    @partial = 'projects_partial'
  end
end

TasksController < ApplicationController
  before_filter :set_submenu_partial

  def set_submenu_partial
    @partial = 'tasks_partial'
  end
end

We can refactor the controller code to something like this:

ApplicationController < ActionController::Base
  def set_submenu_partial(partial_name)
    @partial = partial_name
  end
end

ProjectsController < ApplicationController
  before_filter {|c| c.set_submenu_partial('projects_partial')}
end

TasksController < ApplicationController
  before_filter {|c| c.set_submenu_partial('tasks_partial')}
end

But I’m not quite sure this one’s better. The before_filter looks kind of ugly. Also, if you only need two or three separate submenus and not exactly a different one for every controller, you can consider inheritance:

Submenu1Controller < ApplicationController
  before_filter :set_submenu_partial

  def set_submenu_partial
    @partial = 'submenu1_partial'
  end
end

Submenu2Controller < ApplicationController
  before_filter :set_submenu_partial

  def set_submenu_partial
    @partial = 'submenu2_partial'
  end
end

ProjectsController < Submenu1Controller
end

TasksController < Submenu2Controller
end

Another way, nice if you really need separate submenus for every single controller, is to create a _submenu.html.erb partial for each controller separately.
In layout:

<%= render :partial => 'submenu' %>

In each view folder for a controller create a _submenu.html.erb file to be rendered. Actually, you could always copy one submenu file into more view directories, but this sooo violates the DRY philosophy. You will be punished for that.

You know what, I don’t quite like the idea of setting view things in the controller. They should really be in the views where they belong. And it also can be done like Ryan did it in this Railscast (tip number 2), but then it requires copying the content_for part to all of your views, or if you’re smart, putting it in a partial and copying the render :partial part to all of your views. Not DRY either, but at least we keep MVC layers separate.

Ok so there are a few possibilities here. Of course this can be done to any part of your layout, not just submenus. And the method you should choose will vary each time you do this. Also, if you know of a better way to do this, please comment on this post.

Comments: 1