ref: https://gist.github.com/ahoward/5752290
web apps nearly always end up needing a plethora of little javascript helper methods: you know, auto-complete forms, populating defaults, validations, etc. these aren't API calls properly, just little tidbits of functionality needed to make the views work.
there is always a question of which controller to hang these methods off of. do you make a global helper controller for all this stuff? do you hang them off the controller in question? how to do share the backend of javascript helper methods across controllers?
step one
include the RPC module in your ApplicationController
class ApplicationController < ::ActionController::Base
include RPC
end
step two
in controllers, declare rpc helper methods
class PostsController < ApplicationController
rpc(:defaults) do |params|
post = Post.new(params)
post.set_defaults!
post.as_json
end
end
step three
setup js to be able to call rpc methods relative to the current controller
// in file: app/views/layouts/application.html.erb
//
// notice the *relative* url rpc endpoint
//
// <script>
jQuery(function(){
var rpc = function(){
// parse args
//
var args = Array.prototype.slice.call(arguments);
var ajax;
var method;
if(args.length == 1 && typeof(args[0]) == 'object'){
ajax = args.shift();
} else {
ajax = {};
for(var i = 0; i < args.length; i++){
var arg = args[i];
switch (typeof(arg)) {
case 'object':
ajax['data'] = arg;
break;
case 'function':
ajax['success'] = arg;
break;
case 'string':
method = arg;
break;
};
}
};
// normalize ajax options
//
ajax['url'] = ajax['url'] || rpc['url'];
method = method || ajax['method'];
delete ajax['method'];
ajax['data'] = ajax['data'] || {};
ajax['data']['method'] = method;
ajax['async'] = true;
ajax['type'] = 'GET';
ajax['dataType'] = 'json';
ajax['cache'] = false;
ajax['contentType'] = 'application/json; charset = utf-8';
var result = ajax;
// ajax request - using callback or returning the result sync style
//
if(!ajax['success']){
ajax['async'] = false;
ajax['success'] = function(response){ result = response };
};
jQuery.ajax(ajax);
return(result);
};
rpc['url'] = <%= raw url_for(:action => :rpc).to_json %>;
window.rpc = rpc;
});
// </script>
step four
enable the rpc endpoints in config/routes.rb
resources :posts do
collection do
get 'rpc'
end
end
step five
now you can use relative js rpc helpers at will in your views
var title = $('#title');
var slug = $('#slug');
title.change(function(){
var value = title.val();
var data = {'title' : value};
var success = function(defaults){
slug.val(defaults['slug']);
};
rpc('defaults', data, success);
});
this organization keeps your code clean by avoiding a proliferation of controllers and prevents the need to litter clean resourceful controllers with loads of fugly js helper actions
here is the code in it's entirety
# file : ./lib/rpc.rb
module RPC
Actions =
Hash.new{|hash, controller| hash[controller] = Hash.new}
module ClassMethods
def rpc(method = nil, &block)
case
when method.nil? && block.nil?
Actions
else
controller = self
method = method.to_s
Actions[controller][method] = block
end
end
end
module InstanceMethods
def rpc
method = params.delete(:method) || params.delete(:call)
if method
controllers = self.class.ancestors.select{|controller| controller <= ::ApplicationController}
controllers.each do |controller|
action = Actions[controller][method]
if action
result = action.call(params)
render(:json => result.to_json)
return
end
end
render(:nothing => true, :status => :not_found, :layout => false)
else
render(:json => RPC.index)
end
end
end
def RPC.index
Hash.new.tap do |methods|
Actions.each do |controller, actions|
controller = controller.name.underscore
methods[controller] = []
actions.each do |method, block|
methods[controller].push(method)
end
end
end
end
def RPC.included(other)
super
ensure
other.send(:extend, ClassMethods)
other.send(:include, InstanceMethods)
end
end