views:

1503

answers:

4

I am implementing a dashboard as a relative Rails newbie (more of an infrastructure guy). The dashboard will consist of multiple pages, each page of which will contain multiple charts/tables/etc. For modularity, I want it to be as easy as possible to add new charts or change views of the data.

Say one page has 5 different charts. I could have the controller do 5 separate data lookups, hold all the relevant data in instance variables, and render 5 partials, each of which touch subsets of the data. But it seems more modular to have one "index" controller action whose render has a bunch of divs, and for each div there is another controller action which does the data lookup and has an associated view partial in charge of managing the view of that data within the div.

So if I'm showing the website dashboard page which has two graphs, website/index would use website/graph1 and website/graph2 to look up the data for each and then _graph1.html.erb and _graph2.html.erb would use the controller data to fill out divs "graph1" and "graph2", etc.

Is this the right design, and if so, what's the easiest way to accomplish this? I have an approximation using remote_function with :action => "graph1" to fill out divs, but I'm not 100% happy with it. I suspect I'm missing something easier that Rails will do for me.

A: 

I'm not aware of any special Rails tricks for achieving this without using AJAX in the way you've outlined.

The simplest way to get the modularity you seek is to put those portions of controller code into separate methods (e.g. set_up_graph1_data, set_up_graph2_data, etc.), which you simply call from your index action to set up the variables for the view.

You can put these methods into ApplicationController if you want them available to multiple controllers.

As a side note, early on, Rails did used to have a feature called 'components' which would allow you to do exactly what you're asking for here, without having to use AJAX. From your view, you could just render another controller action, inline. However, this feature was removed for performance and design philosophy reasons.

chrismear
Components are still available as a plugin.
Ian Terrell
A: 

You could achieve this by using

render_output = render :action => "graph2"

But Me personally would probably wrap the code in either a shared helper or write you own "lib" under the lib directory to reuse the code with a shared template. Remember that unless you change your route.rb file any public method defined is accessible by

/controller/action/:id

Also remember to turn off the layout for the function :layout => nil (or specify at the top of the controller layout "graph", :except => ["graph2"]

Cheers

Christian

christkv
+6  A: 

Version 1:

Simple method that I've actually used in production: iframes.

Most of the time you don't actually care if the page renders all at once and direct from the server, and indeed it's better for it to load staggered.

If you just drop an iframe src'd to the controller's show action, you have a very simple solution that doesn't require direct cross-controller interactions.

Pro:

  • dead easy
  • works with existing show actions etc
  • might even be faster, depending on savings w/ parallel vs sequential requests and memory load etc

Cons:

  • you can't always easily save the page together with whatever-it-is
  • iframes will break out of the host page's javascript namespace, so if they require that, you may need to give them their own minimalist layout; they also won't be able to affect the surrounding page outside their iframe
  • might be slower, depending on ping time etc
  • potential n+1 efficiency bug if you have many such modules on a page

Version 2:

Do the same thing using JS calls to replace a div with a partial, à la:

<div id="placeholder">
<%= update_page {|page| page['placeholder'].replace with some partial call here } %>

Same as above, except:

Pro:

  • doesn't lock it into an iframe, thus shares JS context etc
  • allows better handling of failure cases

Con:

  • requires JS and placeholder divs; a bit more complex

Version 3:

Call a whole bunch of partials. It gets complicated to do that once you're talking about things like dashboards where the individual modules have significant amounts of setup logic, however.

There are various ways to get around this by making those things into 'mixins' or the like, but IMO they're kinda kludgy.

ETA: The way to do it via mixins is to create what is essentially a library file that implements your module controllers' setup functions, include that wherever something that calls 'em is used, and call 'em.

However, this has drawbacks:

  • you have to know what top level controller actions will result in pages that include those modules (which you might not easily, if these are really widgety things that might appear all over, e.g. user preference dependent)
  • it doesn't really act as a full fledged controller
  • it still intermixes a lot of logic where your holding thing needs to know about the things it's holding
  • you can't easily have it be segregated into its own controller, 'cause it needs to be in a library-type file/mixin

It IS possible to call methods in one controller from another controller. However, it's a major pain in the ass, and a major kludge. The only time you should consider doing so is if a) they're both independently necessary controllers in their own rights, and b) it has to function entirely on the back end.

I've had to do this once - primarily because refactoring the reason for it was even MORE of a pain - and I promise you don't want to go there unless you have to.

Summary

The best method IMHO is the first if you have complex enough things that they require significant setup - a simple iframe which displays the module, passing a parameter to tell it to use an ultraminimalist layout (just CSS+JS headers) because it's not being displayed as its own page.

This allows you to keep the things totally independent, function more or less as if they were perfectly normal controllers of their own (other than the layout setting), preserve normal routes, etc.

If you DON'T need significant setup, then just use partials, and pass in whatever they need as a local variable. This will start to get fragile if you run into things like n+1 efficiency bugs, though...

Sai Emrys
+1  A: 

why don't you give Apotomo a try, that's stateful widgets for Rails:

A tutorial how to build a simple dashboard

Nick
that link is busted.
Bryan Oakley
It works for me. Leads to a sample application showing how to use Apotomo to build a dashboard.
obvio171
Strange, if you copy and paste the link, it works. The examples are outdated, though. Hit me on freenode#cells and ask for private assistance ;-)
Nick