I'm making a breadcrumb module for my Ruby on Rails application, but I wanted a specific syntax - which I thought was good looking and more intuitive for Rails developers.
Here's the deal:
class WelcomeController < ApplicationController
breadcrumb_for :index, :text => 'Home', :href => -> { root_path }
def index
end
end
See, it's neat.
You can safely ignore the everything else but that proc - what I assign to the :href key.
I use instance_eval so that when the proc is evaluated it has access to the root_path helper.
And it worked. The example above is okay. BUT then I wanted to use an instance variable and that didn't work.
Like this:
class WelcomeController < ApplicationController
breadcrumb_for :index, :text => 'Home', :href => -> { #path }
def index
#path = root_path
end
end
Now, in that proc context #path is nil.
What should I do so I can access the instance variables from the block ?
Below is all the code of my module. Note that when I "process" the blocks and use instance_eval (aka call my module's #breadcrumb) the action should already be evaluated so the instance variable #path should already exist.
module Breadcrumb
extend ActiveSupport::Concern
included do
cattr_accessor(:_breadcrumb) { [] }
helper_method :breadcrumb
def self.breadcrumb_for(*args)
options = args.pop
_breadcrumb.push([args, options])
end
end
def breadcrumb
#breadcrumb ||= self._breadcrumb.map do |item|
puts item
if item[0].include?(params[:action]) || item[0][0] == '*'
text, href = item[1].values_at(:text, :href)
if text.respond_to?(:call)
text = instance_eval(&text)
end
if href.respond_to?(:call)
href = instance_eval(&href)
end
[text, href]
end
end
end
end
Oh no. I'm ashamed to say but it was my mistake. The code above works just fine, I was using different variable names in my application, not shown in the excerpt I used in the question.
Thanks anyway, I'll left it here for reference.
Related
I'm trying to pass a variable from my controller to a custom activeadmin page but I can't seem to figure it out.
I basically have a form that uploads a file and it parses it. If it reaches an error, it throws one and redirects to the custom page.
class ToolController < ApiController
def import
begin
Schedule.Parse(data)
rescue MissingDependencyError => e
#dependencies = "test"
redirect_to admin_import_path({}.merge(flash_error: "Missing Dependencies", dependency_error: true, :locals => { :m => e.object }))
end
end
class MissingDependencyError < StandardError
attr_reader :object
def initialize(object)
#object = object
end
end
ActiveAdmin.register_page "Import" do |lab|
menu false
content do
#dependencies
end
end
#dependencies comes back as nil -> why?
I can pass it through the params hash but that's not the right way.
Instance variables are not available after a redirect_to ... the redirect_to creates a new controller instance and all the instance variables of the previous controller object are gone.
Instead of the params hash, you can use the sessions hash
session[:dependencies] = "test"
and
content do
session[:dependencies]
end
My show action:
def show
# Multiple keywords
if current_user.admin?
#integration = Integration.find(params[:id])
else
#integration = current_user.integrations.find(params[:id])
end
#q = #integration.profiles.search(search_params)
#profiles = #q.result.where(found: true).select("profiles.*").group("profiles.id, profiles.email").includes(:integration_profiles).order("CAST( translate(meta_data -> '#{params[:sort_by]}', ',', '') AS INT) DESC NULLS LAST").page(params[:page]).per_page(20)
#profiles = #profiles.limit(params[:limit]) if params[:limit]
end
There can be many different filters taking place in here whether with Ransacker, with the params[:limit] or others. At the end I have a subset of profiles.
Now I want to tag all these profiles that are a result of the search query.
Profiles model:
def self.tagging_profiles
#Some code
end
I'd like to create an action within the same controller as the show that will execute the self.tagging_profiles function on the #profiles from the show action given those profiles have been filtered down.
def tagging
#profiles.tagging_profiles
end
I want the user to be able to make a search query, have profiles in the view then if satisfied tag all of them, so there would be a need of a form
UPDATE:
This is how I got around it, don't know how clean it is but here:
def show
# Multiple keywords
if current_user.admin?
#integration = Integration.find(params[:id])
else
#integration = current_user.integrations.find(params[:id])
end
#q = #integration.profiles.search(search_params)
#profiles = #q.result.where(found: true).select("profiles.*").group("profiles.id, profiles.email").includes(:integration_profiles).order("CAST( translate(meta_data -> '#{params[:sort_by]}', ',', '') AS INT) DESC NULLS LAST").page(params[:page]).per_page(20)
#profiles = #profiles.limit(params[:limit]) if params[:limit]
tag_profiles(params[:tag_names]) if params[:tag_names]
end
private
def tag_profiles(names)
#profiles.tagging_profiles
end
In my view, I created a form calling to self:
<%= form_tag(params.merge( :controller => "integrations", :action => "show" ), method: :get) do %>
<%= text_field_tag :tag_names %>
<%= submit_tag "Search", class: "btn btn-default"%>
<% end %>
Is this the best way to do it?
Rails public controller actions correspond always to a http request. But here there is just no need for 2 http requests. A simple solution would be just creating to private controllers methods filter_profiles(params) and tag_profiles(profiles) and just call them sequentially.
You can also extract this problem entirely to a ServiceObject, like this:
class ProfileTagger
attr_reader :search_params
def initialize(search_params)
#search_params = search_params
end
def perform
search
tag
end
def tag
#tag found profiles
end
def search
#profiles = #do the search
end
end
As processing 30,000 records is a time consuming operation, it would make sence to perform it outside of the rails request in background. This structure will allow you to delegate this operation to a sidekiq or delayed_job worker with ease
Instance Variables
If you want to "share" variable data between controller actions, you'll want to look at the role #instance variables play.
An instance of a class means that when you send a request, you'll have access to the #instance variable as long as you're within that instance of the class, I.E:
#app/controllers/your_controller.rb
Class YourController < ApplicationController
before_action :create_your_var
def your_controller
puts #var
end
private
def create_your_var
#var = "Hello World"
end
end
This means if you wish to use the data within your controller, I would just set #instance variables, which you will then be able to access with as many different actions as you wish
--
Instance Methods
The difference will be through how you call those actions -
#app/controllers/your_controller.rb
Class YourController < ApplicationController
def action
#-> your request resolves here
method #-> calls the relevant instance method
end
private
def method
#-> this can be called within the instance of the class
end
end
I am trying to refactor my Rails helpers and move breadcrumbs and navigation menu logic into separate classes. But in these classes I don't have access to params, cookies hashes etc. I think that passing params on and on between different classes is a bad idea. How can I avoid that?
For example I have:
module NavigationHelper
def nav_item(name, path, inactive = false)
NavItem.new(params, name, path, inactive ).render
end
class NavItem
include ActionView::Helpers
include Haml::Helpers
def initialize(params, name, path, inactive )
init_haml_helpers
#params = params
#name = name
#path = path
#inactive = inactive
end
def render
capture_haml do
haml_tag :li, item_class do
haml_concat link_to #name, #path
end
end
end
def item_class
klass = {class: 'active'} if active?
klass = {class: 'inactive'} if #inactive
klass
end
# Class of the current page
def active?
slug = #path.gsub /\//, ''
#params[:page] == slug || #params[:category] == slug
end
end
end
I don't think Rails provide any mechanism to access Params out of ActionPack. The way you have done is seems correct to me. You have to pass on params, cookies atleast once to initialize your classes.
Doing some integration work with another site I've got the unusual requirement of needing to create the layout at runtime.
At the moment I'm having to resort to something like this:
def new
body = render_to_string 'new', :layout => false
page = add_layout(body, db.load_template)
render :text => page
end
This is a bit awkward, I'd rather do something like:
def new
...
render 'new', :layout => db.load_template
end
Is there a cleaner way to do this? Perhaps it's possible to register new layouts at runtime and use the normal syntax?
Ha! I encountered a project that will solve just that. Check out panoramic. It stores rails views in the database instead of the filesystem.
You can extend ActionController::Base (or ApplicationController) with a module and alias_method_chain to make this work.
module Foo
alias_method_chain :render, :dblayout
def render_with_dblayout options = nil, extra_options = {}, &block
if options.include? :dblayout
...
else
render_without_dblayout options, extra_options { yield }
end
end
end
ActionController::Base.send(:include, Foo)
I've been drying up one of our controllers in our rails 2.3 app, and I've run up against a problem using an instance variable assigned in a helper_method. Originally, the situation was like this:
home_controller.rb:
class HomeController < ActionController::Base
def index
end
def popular
#popular_questions = PopularQuestion.paginate :page => params[:page],
<some complex query>
end
end
home_helper.rb:
module HomeHelper
def render_popular_questions
#popular_questions = PopularQuestion.paginate :page => 1,
<some complex query>
render :partial => 'popular'
end
end
home/index.html.haml
-cached do
.popular=render_popular_questions
home/popular.html.haml
=render :partial => 'popular'
home/_popular.html.haml
-if #popular_questions.length > 0
<show stuff>
hitting either / or /popular showed the appropriate box of popular questions.
Now, since the query was pretty much duplicated, and since paginate will use the correct page by default, I refactored this as:
home_controller.rb:
class HomeController < ActionController::Base
helper_method :get_popular_questions
def index
end
def popular
get_popular_questions
end
private
def get_popular_questions
#popular_questions = PopularQuestion.paginate :page => params[:page],
<some complex query>
end
end
home_helper.rb:
module HomeHelper
def render_popular_questions
get_popular_questions
render :partial => 'popular'
end
end
now when I go to /, I get
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.length
being raised in line 1 of home/_popular.html.haml
It seems that variables set from within helper_methods called from within helpers aren't accessible to the template. Have I made a mistake somewhere? If not, how do I use an instance variable assigned in a helper_method from a helper?
Pass them as parameters and local-variables:
home_controller.rb:
class HomeController < ActionController::Base
helper_method :get_popular_questions
def index
end
def popular
#popular_questions = get_popular_questions
end
private
def get_popular_questions
# remember that the final statement of a method is also the return-value
PopularQuestion.paginate :page => params[:page],
<some complex query>
end
end
home_helper.rb:
module HomeHelper
def render_popular_questions
questions = get_popular_questions
render :partial => 'popular', :locals => {:questions => questions}
end
end
now in your partial, use "questions" instead of "#popular_questions"
Just make sure that the main template for "popular" also need to populate this local variable too.