Skip to content

Instantly share code, notes, and snippets.

@vasilakisfil
Created October 24, 2017 16:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vasilakisfil/47b30c155e31df2cd8b5cd127861d6ab to your computer and use it in GitHub Desktop.
Save vasilakisfil/47b30c155e31df2cd8b5cd127861d6ab to your computer and use it in GitHub Desktop.
A module for creating UI controllers in Rails
module NestedControllers
CALLBACKS_OPTS = [:filter, :if, :unless, :kind].freeze
#adds the relative paths to controller so you can do `render 'subcontroller/something'`
#instead of `render 'parent_controller/subcontroller/something'`
#(solves 2)
def self.extended(base)
base.prepend_view_path("app/views/#{base.controller_path}/")
end
#creates a nested controller `{self}::{Name}Controller` that inherits from
#the controller that `self` inherits
def controller(name, options = {}, &block)
#save the code to an anonymized module
extended_m = Module.new
#create a new class that inherits parent and extends current module to support recursiveness
extended_superklass = Class.new(self.superclass).send(:extend, NestedControllers)
#create a new class that inherits the previously created class and sets that as a constant under parent controller
#ONLY THEN do we apply the developer's code in order to give
#the option to the developer to override any method defined by us or the parent controller
klass = self.const_set(
"#{name.to_s.camelize}Controller",
Class.new(extended_superklass, &block).send(:extend, extended_m)
)
#figure out the controller path
begin
name_path = self.controller_name
rescue NoMethodError
#if we get NoMethodError, this means that Rails hasn't set the class constant yet (like `ProfilesController::SubscriptionsController`)
#it happens when we have > 2 leves of nesting and we need to help Rails by passing a `controller_path` in options
name_path = nil
if options[:controller_path].nil?
raise 'You need to set a `controller_path` option in when nesting more than once'
end
end
#set the controller path (makes it easier to work with forms)
klass.send(:define_singleton_method, :controller_path) do
"#{(name_path || options[:controller_path])}/#{name.to_s}"
end
#set the views path (solves 1)
klass.prepend_view_path("app/views/#{(name_path || options[:controller_path])}/")
#add the parent's filters (solves 4)
unless options[:exclude_filters]
_inject_callbacks(klass)
end
end
#adds the {before|after}_filters defined in `self`, in `klass`
#(but not the filters from `self`'s ancestors since these run anyway because
#`klass` also inherits the same ancestors
#internally all callbacks in Rails are saved as `before` of `after`
def _inject_callbacks(klass)
callbacks_array = _process_action_callbacks.to_a.map{|c|
if superclass._process_action_callbacks.to_a.map{|s_c|
s_c.send(:instance_variable_get, "@filter")
}.include?(c.send(:instance_variable_get, "@filter"))
next
end
CALLBACKS_OPTS.inject({}){|memo, k|
memo[k] = c.send(:instance_variable_get, "@#{k}")
memo
}
}.compact
callbacks_array.each do |callback|
if callback[:filter].is_a? Symbol
klass.send("#{callback[:kind]}_action", callback[:filter], {
if: callback[:if], unless: callback[:unless]
})
else
klass.send("#{callback[:kind]}_action", callback[:filter], {
if: callback[:if], unless: callback[:unless]
}) do
klass.instance_exec(&callback[:filter])
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment