How does rails pass objects to the view? - ruby-on-rails

If an action looks like:
def show
#post = Post.find(params[:id])
end
I can then do:
<%= #post.title %>
How does it pass the object to the view?
Since my action has only one line, what programming technique or pattern is used to take the #post object and pass it to the view (template) page?
I know it assumes the view will be the same name as the action, but how does it do this?

for example like this:
we will transfer MyController variables to MyView variables
class MyController
def my_action
#my_variable = 100
end
end
class MyView
def process_view_with_variables(variables)
puts variables.inspect # => [{"#my_variable"=>100}]
variables.each do |var|
var.each {|name,value| self.instance_variable_set(name,value)}
end
puts self.instance_variables # => #my_variable
# .. view_rendering here
end
end
# create new view and controller instances
my_controller = MyController.new
my_view = MyView.new
# call my_action method in MyController instance (which will initialized some variables)
my_controller.my_action
# let know about instance variables in our controller
puts my_controller.instance_variables.inspect # => ["#my_variable"]
# simple array, for store variables (this is like a proxy)
controller_variables = []
# transfer instance variables from controller to proxe
my_controller.instance_variables.each do |variable|
controller_variables << {variable => my_controller.instance_variable_get(variable)}
end
# let know which instance variables bow in proxy array
puts controller_variables.inspect # => [{"#my_variable"=>100}]
# call method process_view_with_variables which will transfer variables from proxy to view
my_view.process_view_with_variables(controller_variables) # => [{"#my_variable"=>100}]#

First you need to look at the binding class. The book "Metaprogramming Ruby" (highly recommended BTW) sums them up nicely: "A Binding is a whole scope packaged as an object. The idea is that you can create a Binding to capture the local scope and carry it around."
Then a look at the ERB class should answer your question. This example is straight from the docs:
require 'erb'
x = 42
template = ERB.new <<-EOF
The value of x is: <%= x %>
EOF
puts template.result(binding)
I hope that helps.

Related

Copy a controller class instance variables

I want to write a simple erb template generator to parse stored erb templates from the views using Generator module. I call the Generator from the rails controller to generate it's singleton instances and pass it the WallController by self pointer.
require 'generator'
class WallController < ApplicationController
def index
header = File.read 'app/views/application-header.html'.freeze
#instances = {header: header}
# Load view generators
Generator.generate_instances self
end
end
The first thing Generator.generate_instances actually attempts to do is to copy the WallController instance variables (hence the self pointer) to perform correct parsing of the erb templates. Then it generates methods returning erb resulted text.
require 'erb'
module Generator
def self.generate_instances environment
# Mimic class environment
if environment.superclass == ApplicationController
environment.instance_variables.each do |v|
puts "Copy instance variable '#{v}' from #{environment.name} to #{self.name}"
value = environment.instance_variable_get(v)
self.send :instance_variable_set, v, value
end
end
# Parse the ERB templates
templates = #instances
return 0 if !templates.is_a?(Hash) or templates.empty?
templates.keys.each.with_index do |key, index|
define_singleton_method key do
ERB.new(templates.values[index]).result
end
end
end
end
Usage of Generator interface will look like this:
<%=== Generator.header %>
I am new to rails but I have found out that rails controller's included files are limited to a single static structure. I didn't manage either to overwrite class Object or class Class singleton methods which could be helpful.
However, after running the above example the instance variables of WallController return the WallController class address in stead of values defined by WallController.index.
undefined method `empty?' for #<WallController:0x000000000a1f90>
Is there a correct way to distribute a rails controller instance variables among other controllers? If not, why are regular instance copy not working?
If I had to write it in ruby, that would be easy:
module Y
def self.generate_environment environment
environment.instance_variables.each do |v|
puts "Copy #{v} from #{environment.name} to #{self.name}"
value = environment.instance_variable_get v
self.instance_variable_set(v, value)
end if environment.class == Class
puts "Retrived string: #{#hello}"
end
end
class X
def self.index
#hello = 'Hello, World!'
Y.generate_environment self
end
end
X.index
This problem may be solved with viewcomponent, which allows for standard ruby code for the view controller. Also solves the problem of dividing the view code to smaller reusable components in reasonable speed.
To use the viewcomponent gem first include it to your Gemfile.
gem 'view_component', require: 'view_component/engine'
After updating your gems with bundle install, you will also need to restart your server if it's running, to apply the new gem.
Then generating the component is similar in usage to other rails generators. The first argument is a component name and the second is a component argument.
rails generate component Header site_id
Now I focus on files generated in app/component directory, view and controller code. This will simply be the controller to create the header snippet of the view.
Inside of app/component/header_component.rb can be encapsulated all the code from WallController related to the header view.
class HeaderComponent < ViewComponent::Base
def initialize(site_id:)
puts "Rendering header component for site: #{site_id}"
# Load site elements
#site = Site.find site_id
#menu_items = []
Site.all.each.with_index do |site, index|
#menu_items.push site.title => site.link
end
end
end
Similarly, put all the header view erb code to the app/component/header.html.erb.
The finished component can be generated from the view using rails render:
<%= render HeaderComponent.new(site_id: 1) %>

How can I access a method in a controller that is defined in a model?

I'm pretty new to rails, and the project I'm working on requires me to access an existing method. However, it's written in a model, and I'm not sure how I can get it to run in the controller my API needs to hit
I've tried routing to the method in the model, but learned I can't do that. From what I've gathered, this is sort of the way it will work, right?
model.rb
def method_i_need
//code
end
controller.rb
def method_to_call_other_method
//code
end
At the risk of stealing #sergio's points...
If your method is defined inside Model.rb, then both the following will work in your controller:
def method_to_call_other_method
Model.first.method_i_need
end
def method_to_call_other_method
Model.find(params[:id]).method_i_need
end
As the commentor said, you just need an instance of your model (Model.first or Model.find(params[:id])) and to then call the method you defined in your model, on the instance of the model. And the params[:id] is obviously dependent on what params you're getting through.
Any instance of a class will have the public instance methods available to be called on the instance object. It's very common to instantiate model class instances within a controller action.
Here's an example elaborating on previous answer and comments how you can do this in Rails.
class Person < ActiveRecord::Base
def say_hello
language == 'DE' ? 'Guten Tag' : 'Hello'
end
end
class PersonsController < ApplicationController
def random_person
#random_person = Person.find(Person.pluck(:id).sample)
# you can now call #random_person.say_hello
end
def person_greetings
# this examples assumes we only have 2 languages, EN and DE
languages = Person.pluck(:language).uniq.sort
#greetings = languages.each_with_object({}) do |language, hash|
hash[language] = Person.new(language: language).say_hello
end
end
end
# #greetings should return the following hash
=> {
"DE" => "Guten Tag",
"EN" => "Hello"
}
Likewise, class methods can also be called directly when needed inside a controller action method for example in model you may have a class method defined like this inside the Person model.
def self.languages
pluck(:language).uniq.sort
end
This method can be called from any controller or other classes where appropriate for example:
def languages
#people_count = Person.count # active record method to get number of people in database
#languages = Person.languages
end
Where you might use this inside of a controller action's view
<div>
There are <%= #people_count %> total people in the system.
Of them, <%= #languages.count %> languages are spoken.
Those include the following:
<ol>
<% #languages.each do |language| %>
<li><%= language %></li>
</ol>
</div>

Ruby on Rails - Creating and Using a Custom Method

I am rather new to Rails, and would greatly appreciate any bit of help. I have created the following method:
def name_fix
name = self.split
mod_name = []
name.each do |n|
n.split("")
if n[0]
n.upcase
else
n.downcase
end
mod_name.push(n)
end
mod_name.join
end
I would like to use this method in my Controller as such:
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name].name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
How can I go about accomplishing this? Will this method reside within my Model or Controller? Previously, I've run into an undefined method error.
Note: I'm sure that there is a way to better write my code. I am grateful for help with that as well.
#app/models/patient.rb
class Patient < ActiveRecord::Base
protected
def name=(value)
mod_name = []
value.split.each do |n|
n.split("")
type = n[0] ? "up" : "down"
n.send("#{type}case")
mod_name.push(n)
end
#name = mod_name.join
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient = Patient.new patient_params
#patient.save ? redirect_to(patients_path) : render(:new)
end
private
def patient_params
params.require(:patient).permit(:name)
end
end
What you're doing is trying to override the setter method, which can be done using the above code. Much more efficient and out of the way.
I have created the following method
Since you're new, let me explain something else.
It is important to note where you're using this method.
You've currently put it in the model, which means you'll have to call it to manipulate some attribute / functionality of any object created with said model.
--
Models - in Rails - build the objects which populate your app. Ruby is an object orientated language, which means that every element of your program should revolve around data objects in some degree.
As you can see above, the method of building objects in your system is really about invoking classes. These classes contain methods which can be called, either at class level (IE invoking the class through the method), or at instance level (IE calling a method on an already invoked object).
This is where you get "class" methods (Model.method) and "instance" methods (#model.method) from:
#app/models/patient.rb
class Patient < ActiveRecord::Base
def explode
#this is an instance method
puts "Instance Explode"
end
def self.explode
#this is a class method
puts "Exploded"
end
end
Thus you can call the following:
#patient = Patient.find params[:id]
#patient.explode #-> "Instance explode"
Patient.explode #-> "Exploded"
--
This is important because it gives you a strict framework of where you should, and shouldn't use methods in your models.
It explains why you have controllers & helpers, and allows you to formulate the best way to structure your application as to get the most out of the least code.
For example...
Your use of #patient.name = params[:params][:name].name_fix is incorrect.
It's wrong because you're calling the instance method .name_fix on a piece of data totally unrelated to your model. If you wanted to use .name_fix in a general sense like this, you'd probably use a helper:
#app/helpers/patients_helper.rb
class PatientsHelper
def name_fix value
# stuff here
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient.name = name_fix params[:patient][:name]
end
end
Since you're using the method to populate the .name attribute of your model, it makes sense to override the name= setter. This will not only provide added functionality, but is much smoother and efficient than any other way.
Methods that are called directly are best put in the Controller (or in ApplicationController if you think more than one controller might want to use it).
These are methods like
# app/controllers/my_controller.rb
def foo(bar)
# do something here
end
def create
id = params[:id]
value = foo(id)
end
If you want a chained method that acts as a property method of whatever you're calling it on. Those are characteristic of how Models work - you have your main model and you call attributes or methods on the instance of that model.
# app/models/my_model.rb
def full_name
first_name + " " + last_name
end
# app/controller/my_controller.rb
def create
id = params[:id]
model = MyModel.find(id)
full_name = model.full_name
end
In your case, you want to call name_fix ON whatever is returned by params[:params][:name], which is (I'm guessing) a String.
You have two options
Modify the String class to define a method named name_fix. I highly recommend against this. It's call "monkeypatching" and shouldn't be done without good reason. Just letting you know you can do it in some cases.
Use a direct method in your controller or ApplicationController like the first example above.
#patient.name = name_fix(params[:params][:name])
Edit: As for your request about a better way to write your code... that's difficult to teach or convey in one answer. I'd say read some open source projects out there to see how people write Ruby and some common idioms used to clean up the code. To get you started, here's how I'd re-write your code
def create
#patient = Patient.new(params[:patient])
# 1. Be descriptive with your method names. `name_fix` is vague
# 2. Why is `:name` nested under another `[:params]` hash?
#patient.name = capitalize_name(params[:name])
if #patient.save
# 1. I think `patient_path` has to be singular
# 2. It needs a `Patient` object to know how to construct the URL
# e.g. `/patients/:id`
redirect_to patient_path(#patient)
else
render :new
end
end
def capitalize_name(full_name)
# Example: julio jones
#
# 1. `split` produces an array => ["julio", "jones"]
# 2. `map` applies a function (`capitalize`) to each element
# => ["Julio", "Jones"]
# 3. `join(" ")` rejoins it => "Julio Jones"
full_name.split.map(&:capitalize).join(" ")
end
Assuming your goal with the name_fix method is just to capitalize the first letter of each name, you could just pass name as an argument and store it as a private method on the Controller:
# app/controllers/patient_controller.rb
private
def name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do
#patient.name = name_fix(params[:params][:name])
in the create method.
OR, you could store this method in the model:
# app/models/patient.rb
def self.name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do this instead, in the controller:
#patient.name = Patient.name_fix(params[:params][:name])
I would also suggest renaming your name_fix method to something like capitalize_name.
update your create method as below
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name]
#patient = #patient.name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
It should work.

make variables accessible to multiple actions in controller

I have a set of boolean symbols, inside a controller action
These are currently in 1 action, in the following format
def my_action
setup_stages = [:data_entry_completed, :data_validation_completed]
setup_stages.each do |stage|
do stuff
end
end
I've noticed that I need to make use of these symbols in another action but do not want to replicate them. Is there a way to make this list accessible to multiple actions in the controller so that I can iterate through them without having the list twice?
Just define them as constant:
class MyController < AplicationController
SETUP_STAGES = [:data_entry_completed, :data_validation_completed]
I would personally define it as an instance variable:
class MyClass
def initialize
#setup_stages = [:data_entry_completed, :data_validation_completed]
end
def do_action
#setup_stages.each do |stage|
# do stuff
end
end
def show_stages
puts #setup_stages.to_s
end
end
x = MyClass.new
x.do_action
x.show_stages
A constant is also a good way of defining this but should not be altered, so if for whatever reason you want to add other options to the array dynamically you would be able to do this with an instance variable.

Rails Presenter block method

EDIT:
I got many responses with different approaches for solving the problem, thanks a lot!
Sadly, none of them worked until now.
To easily understand and reproduce the failure, I created a small Rails repo on GitHub with a Rspec suite.
One of the specs is passing (where the presenter is initialized in the view).
One of the specs is failing (where the presenter is initialized in the controller).
How make them both pass ?
ORIGINAL QUESTION BELOW:
This is my Presenter:
class UserPresenter
def initialize(user, vc)
#user = user
#vc = vc
end
def linkify()
#
# HERE IS THE PROBLEM
#
vc.link_to("foo") do
yield
end
end
end
This is my Controller:
I initialize my Presenter in the controller, passing the view context of the controller with the presented model.
class UserController
def show
#user = User.find(#.....
#presenter = UserPresenter.new(#user, view_context)
end
end
In my Slim template, I call my Presenter to put the content in a link:
=#presenter.linkify do
p "123"
My problem is, I can't pass the block from the view to my linkify method.
In the with comment marked above code, the passed block is the whole view content, instead of the p 123.
When I initialize my Presenter in the view via: #presenter = UserPresenter.new(#user, self), it works as expected.
How I can make the linkify method uses the provided block, without initializing the presenter in the view ?
Because if you are going to use the yield command, you mustn't specify the &block, since now you are effectively receiving a block as a parameter using normal parameter syntax.
class UserPresenter
def initialize(user, vc)
#user = user
#vc = vc
end
def linkify() # <-- Remove &block
vc.link_to("foo") do
yield
end
end
end
# ...
# Somewhere else, assuming you have access to #presenter which is an instance of
# UserPresenter
# ...
def show
#presenter.linkify do
# ...
# do my view stuff here
# ...
end
end
show()
# Now, if your "View" is nothing but a block that needs to get passed in
# then you'd do this...
def show(&block)
#presenter.linkify do
block.call()
end
end
# This would be used this way:
show(lambda {
# ...
# View stuff here
# ..
})
As specified in lacrosse's answer. The wrong view context is the root of this cause. I tried to make a work around for your situation. And, this is how ended up doing it:
I created a helper method in ApplicationHelper:
module ApplicationHelper
def link(presenter)
presenter.linkify(self) do
yield
end
end
end
changed linkify() to:
def linkify(vc)
vc.link_to("foo") do
yield
end
end
which means, no need to have vc in presenter's class constructer, or you can update the vc in link method defined in the helper(your choice).
views are now looks something like this:
presenter_from_view.html.slim:
-#presenter = UserPresenter.new(#user, self)
=link #presenter do
p 123
presenter_from_controller.html.slim:
=link #presenter do
p 123
I agree, maybe this is not how you wanted your solution to be done. But, I couldn't get any cleaner work around for this. However, here you don't have to worry about passing self in views wherever you use link #presenter do..(which may become too much for writing code when you use linkify in multiple views I guess).
P.S.: Your all specs are passing now. And, if you need the modified code then I can push it to your repository in a separate branch. Let me know.
From Slim's documentation on Helpers and Capturing:
module Helpers
def headline(&block)
if defined?(::Rails)
# In Rails we have to use capture!
"<h1>#{capture(&block)}</h1>"
else
# If we are using Slim without a framework (Plain Tilt),
# this works directly.
"<h1>#{yield}</h1>"
end
end
end
Can you try using capture as follows?
def linkify(&block)
result = capture(&block)
vc.link_to("foo") do
result
end
end
The wrong view context is causing this issue. Just change UserPresenter#initialize to not accept view context, initialize presenter in the controller and pass the correct view context from the view instead, like so:
= #presenter.linkify(self) do
p "123"
What error are you getting? Just looking at the code...
In this method
def linkify()
#
# HERE IS THE PROBLEM
#
vc.link_to("foo") do
yield
end
end
where is vc defined?
I think you mean #vc which is the instance variable you're initializing.
Also as a side note... the empty () in linkify() are redundant in a ruby method with no variables. You can eliminate them.
Also you may want to take a look at the cells gem. As you're basically mirroring this behavior in your presenters and IMO cells is a cleaner way of accomplishing this in rails.
I think I figured out WHY it's not working. When you pass in the view_context in the controller, it's rendering the view_context once when you pass it in to the presenter, and then you again when you actually render the view.
def initialize(user, vc)
#user = user
#vc = vc
end
# When called from the view:
#presenter = UserPresenter.new(#user, self)
# You're passing the view directly in "as is" and renders as expected.
# However, when you pass it in from the controller:
#presenter = UserPresent.new(#user, view_context)
# you're essentially rendering the view_context in the controller, and then again
# once it renders at the end of your action. That's why you're getting this:
# "<p>123</p><p>123</p>"
You'll either need to send self in from the view each time you call linkify, or you can use an application helper method that will always have the view context.

Resources