Rails AJAX link_to not refreshed controller action - ruby-on-rails

In my Rails 5 app, Doctor/Admin have to create a Caregiver with linked Patient (new object CaregiverPatient represents that). During this process, inside the registrants_controller#new, Doctor/Admin search for a Patient (which is done by Ransack gem) and from the result he/she push Add Patient button to add Patient to initialized CaregiverPatient object from caregiver_patient#new and display #patient.full_name inside the form.
In order not to lose already filled form fields, everything must be done asynchronously. I've tried to do so with AJAX, below my code:
registrant_controller.rb
def new
#registrant = Registrant.new
#patient = Registrant.find(session[:patient_id_to_add_caregiver]) if session[:patient_id_to_add_caregiver]
end
registrants/_new_form.html.erb
<%= form_for #registrant do |f| %>
<%= f.fields_for :registration do |registration| %>
<% if #patient %>
<div id="add-patient"></div>
<% else %>
<%= required_text_field_group registration, #registrant, :registrant_id, "Patient Added", {}, { disabled: true } %>
<% end %>
<% end %>
# some other logic (...)
# patient search result
<% #registrants do |registrant| %>
<%= link_to 'Add Patient', new_registrant_link_path(registrant), remote: true %>
<% end %>
Add Patient button triggers caregiver_patients#new action:
#caregiver_patient_controller.rb
class CaregiverPatientsController < ApplicationController
# new_registrant_link_path
def new
session[:patient_id_to_add_caregiver] = params[:registrant_id].to_i
respond_to do |format|
format.html { redirect_to request.referer }
format.js { render action: 'patient_to_caregiver', notice: 'Patient Added' }
end
end
end
AJAX request is made:
#views/caregiver_patients/patient_to_caregiver.js.erb
$("#add-patient").html("<%= escape_javascript(render :partial => 'registrants/add_patient') %>");
views/registrants/_add_patient.html.erb
Patient Added: <%= #patient.full_name %>
But surprisingly gets an error:
ActionView::Template::Error (undefined method `full_name' for nil:NilClass):
1: Patient Added: <%= #patient.full_name %>
Which probably means that registrants_controller#new was not refreshed under the hood somehow. What have I missed?

Related

How to edit a single attribute inline with Turbo Frame and Trubo Stream with validation feedback?

Creating In-Place-Editing of a single attribute of a model using Turbo Frames (not using a gem such as Best_In_Place as it requires jQuery and is not working well with Rails 7) This implemenation is using ONLY turboframes.
To accomplish this I followed this tutorial: https://nts.strzibny.name/single-attribute-in-place-editing-turbo/ (written in January 2022)
The tutorial does not match Ruby 3.2.0, Rails 7.0.4 perfectly and needs a one variable adjustment on the show page to work.
Unfortunately, there is no validation feedback currently in this tutorials method as the turbo_frame form implemented does not have it included.
Question: how to properly add validation feedback and routing of errors? (preferably a turbo_frames only solution)
Summary of tutorial:
create new app and scaffold one model: User name:string
changes to UsersController (a new action on the controller to edit a single attribute, and adding edit_name to before_action list)
before_action :set_user, only: %i[ show edit edit_name update destroy ]
# GET /users/1/edit_name
def edit_name
end
add to routes.rb (a new route for editing a single specific attribute)
resources :users do
member do
get 'edit_name'
end
end
create view/users/edit_name.html.erb (a new view page to support editing a specific attribute, (here a name)).
<%= turbo_frame_tag "name_#{user.id}" do %>
<%= form_with model: #user, url: user_path(#user) do |form| %>
<%= form.text_field :name %>
<%= form.submit "Save" %>
<% end %>
<% end %>
additions on _user.html.erb file (the link to the created turbo frame form edit_name.html.erb)
<%= turbo_frame_tag "name_#{user.id}" do %>
Name: <%= link_to #user.name, edit_name_user_path(#user) %>
<% end %>
Upon starter the app server I get errors about #user being nil:Class.
In order to get the tutorial to work I have to change the _user.html.erb file to use a local variable for user in the link.
edited again  _user.html.erb (changing instance variable #user to local variable user)
<%= turbo_frame_tag "name_#{user.id}" do %>
Name: <%= link_to user.name, edit_name_user_path(user) %>
<% end %>
With this change, the tutorial works, allowing single attribute in place editing through turbo frames! But no model validation feedback is implemented.
Below, I attempt to deal with validation, first adding validation to models/user.rb
class User < ApplicationRecord
validates :name, presence: true
validates :name, comparison: { other_than: "Jason" }
end
PROPOSED SOLUTION:
CREATE a new turbo_stream file for editing errors that pop up (it has an error in the turbo_frame tag that it is targeting, it needs to be able to target any parent turboframe where the single attribute edit was initiated)
<%= turbo_stream.replace"name_#{#user.id}" do %>
<%= form_with model: #user, url: user_path(#user) do |form| %>
<% if #user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(#user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% #user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<% if #user.errors[:name].any? %>
<%= form.label :name, style: "display: block" %> <%= form.text_field :name %>
<% end %>
<% if #user.errors[:active].any? %>
<%= form.label :active, style: "display: block" %> <%= form.check_box :active %>
<% end %>
<%= form.submit "Save" %>
<% end %>
<% end %>
and edit the UsersController.rb update method to deal with turbo stream errors
# PATCH/PUT /users/1 or /users/1.json
def update
respond_to do |format|
if #user.update(user_params)
format.html { redirect_to user_url(#user), notice: "User was successfully updated." }
format.json { render :show, status: :ok, location: #user }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: #user.errors, status: :unprocessable_entity }
format.turbo_stream do
if #user.errors[:name].any?
#user.name = nil #so that it does not repopulate the form with the bad data
if #user.errors[:active].any?
#user.active = nil
end
render :edit_errors, status: :unprocessable_entity
end
end
end
end
This all works except for after entering a succesful edit on the form produced after an invalid entry, it renders the show for that entry only, rather than all of them.
What would be a 'dry'er method of doing all of this? (and how do I target updating just the one frame from the turbo stream so that only the one field gets updated after success on validation)?
Philosophically, is any of this worth it now compared to just using jQuery and the Gem Best_In_Place??? Seems like the number of changes are piling up and the code will get ugly if supporting such functionality across multiple attributes?
Since the initial issue is resolved, I'll just add some other ways you can do this. It's gonna be a little more work to do this yourself and you won't have all the functionality that some gem could give you. On the other hand, it's a lot less code and you have full control over everything. Besides, if you just need to have this one field to be editable, installing a gem and jquery is too much overhead.
Setup:
# rails v7.0.4.2
$ rails new hello_edit_in_place -c tailwind
$ cd hello_edit_in_place
$ bin/rails g scaffold User email first_name last_name --skip-timestamps
$ bin/rails db:migrate
$ bin/rails runner "User.create(email: 'admin#localhost', first_name: 'super', last_name: 'admin')"
$ open http://localhost:3000/users
$ bin/dev
class User < ApplicationRecord
validates :email, presence: true, length: {minimum: 3}
end
Turbo Frame
I'll just modify the default form and won't touch the controller as a quick example:
# app/views/users/_form.html.erb
# NOTE: this lets you render this partial and pass a local `:attribute` or
# get attribute from url params.
<% if attribute ||= params[:attribute] %>
<%= turbo_frame_tag dom_id(user, attribute) do %>
# NOTE: send `attrtibute` back in case of validation error, so this page
# can be rendered again with params[:attribute] set.
# V
<%= form_with model: user, url: user_path(user, attribute:) do |f| %>
<%= f.text_field attribute %>
# NOTE: show validation errors
<%= safe_join user.errors.full_messages_for(attribute), tag.br %>
<%= f.submit "save" %>
<% end %>
<% end %>
<% else %>
# original form here
<% end %>
# app/views/users/_user.html.erb
# NOTE: there is no need to have the whole set up for each individual
# attribute
<% user.attribute_names.reject{|a| a =~ /^(id|something_else)$/}.each do |attribute| %>
<%= tag.div attribute, class: "mt-4 block mb-1 font-medium" %> # tag.div - so that i can keep rb syntax highlight for stackoverflow
<%= turbo_frame_tag dom_id(user, attribute) do %>
<%= link_to edit_user_path(user, attribute:) do %>
<%= user.public_send(attribute).presence || "—".html_safe %>
<% end %>
<% end %>
<% end %>
That's it, every attribute is rendered, is editable and email shows validation errors. Also because all turbo_frame_tags have a unique id, everything works with multiple users on the index page.
Turbo Stream
You can also use turbo_stream to have more flexibility and make it even more dynamic, but it's a bit more of a set up. Also, add ability to edit full name in place, with first_name and last_name fields together:
# config/routes.rb
# NOTE: to not mess with default actions, add new routes
resources :users do
member do
get "edit_attribute/:attribute", action: :edit_attribute, as: :edit_attribute
patch "update_attribute/:attribute", action: :update_attribute, as: :update_attribute
end
end
# app/views/users/_user.html.erb
# Renders user attributes.
# Required locals: user.
<%= render "attribute", user:, attribute: :email %>
<%= render "attribute", user:, attribute: :name %>
# app/views/users/_attribute.html.erb
# Renders editable attribute.
# Required locals: attribute, user.
<%= tag.div id: dom_id(user, attribute) do %>
<%= tag.div attribute, class: "mt-4 block mb-1 font-medium" %>
# NOTE: to make a GET turbo_stream request vvvvvvvvvvvvvvvvvvvvvvvvvv
<%= link_to edit_attribute_user_path(user, attribute:), data: {turbo_stream: true} do %>
# far from perfect, but gotta start somewhere
<% if user.attribute_names.include? attribute.to_s %>
<%= user.public_send(attribute) %>
<% else %>
# if user doesn't have provided attribute, try to render a partial
<%= render attribute.to_s, user: %>
<% end %>
<% end %>
<% end %>
# app/views/users/_name.html.erb
# Renders custom editable attribute value.
# Required locals: user.
<%= user.first_name %>
<%= user.last_name %>
# app/views/users/_edit_attribute.html.erb
# Renders editable attribute form.
# Required locals: attribute, user.
<%= form_with model: user, url: update_attribute_user_path(user, attribute:) do |f| %>
<% if user.attribute_names.include? attribute.to_s %>
<%= f.text_field attribute %>
<% else %>
# NOTE: same as before but with `_fields` suffix,
# so this requires `name_fields` partial.
<%= render "#{attribute}_fields", f: %>
<% end %>
<%= f.submit "save" %>
<% end %>
# app/views/users/_name_fields.html.erb
# Renders custom attribute form fields.
# Requires locals:
# f - form builder.
<%= f.text_field :first_name %>
<%= f.text_field :last_name %>
# app/controllers/users_controller.rb
# GET /users/:id/edit_attribute/:attribute
def edit_attribute
attribute = params[:attribute]
respond_to do |format|
format.turbo_stream do
# render form
render turbo_stream: turbo_stream.update(
helpers.dom_id(user, attribute),
partial: "edit_attribute",
locals: {user:, attribute:}
)
end
end
end
# PATCH /users/:id/update_attribute/:attribute
def update_attribute
attribute = params[:attribute]
attribute_id = helpers.dom_id(user, attribute)
respond_to do |format|
if user.update(user_params)
format.turbo_stream do
# render updated attribute
render turbo_stream: turbo_stream.replace(
attribute_id,
partial: "attribute",
locals: {user:, attribute:}
)
end
else
format.turbo_stream do
# render errors
render turbo_stream: turbo_stream.append(
attribute_id,
html: (
helpers.tag.div id: "#{attribute_id}_errors" do
# FIXME: doesn't render `first_name` `last_name` errors
helpers.safe_join user.errors.full_messages_for(attribute), helpers.tag.br
end
)
)
end
end
end
end
private
def user
#user ||= User.find(params[:id])
end

Rails partial not refreshing with create action

I realize there are dozens of similar questions here on this topic, I have looked through many of them and tried to duplicate the solutions and nothing has worked for me.
I have a list of tasks displayed in the index view as a partial, when the user adds a task I would like list to show the newest task (as well as all the others already there).
Here is the create controller:
def create
#task = current_user.tasks.build(task_params)
if #task.save
flash[:success] = "Task added"
respond_to do |format|
format.js
end
else
flash[:error] = "Task not added"
render 'new'
end
The _new partial:
<%= form_for(#task, method: :post, remote: true) do |f| %>
<%= f.label :comments %>
<%= f.text_field :comments, id: "task-comments" %>
<%= f.submit "Insert" %>
<% end %>
Index.html.erb
<div id="new-task-form">
<%= render 'new' %>
</div>
<div id="current-tasks">
<%= render partial: 'show_list', locals: {tasks: #tasks} %>
</div>
create.js.erb
console.log("create.js.erb called");
document.getElementById("task-comments").value = "";
document.getElementById("current-tasks").html('<%= j(render partial: "show_list") %>');
I have been able to get the console to log the message and even been able to clear the input box but the partial does not render, I have tried this in a few different ways and the script always seems to stop executing when it gets to the escape javascript line.
There are no JS errors, as stated above, the console logs the message when the "submit" button is pressed. There is a warning [Violation] Forced reflow while executing JavaScript took 114ms but I don't think that is relevant to this issue.
Here is the partial that I am trying to render _show_list.html.erb
<% unless #tasks.nil? %>
<ul>
<% tasks.each do |tsk| %>
<li><%= tsk.comments %></li>
<% end %>
</ul>
<% end %>
def create
#task = current_user.tasks.build(task_params)
if #task.save
flash[:success] = "Task added"
#respond_to do |format|
#format.js
#end
else
flash[:error] = "Task not added"
render 'new'
end
end
It's always good practice to use local variable in partial
1) => Index.html.erb
<div id="new-task-form">
<%= render 'new' %>
</div>
<div id="current-tasks">
<%= render partial: 'show_list', locals: {tasks: #tasks} %>
</div>
2) => If you are using jquery (create.js.erb)
console.log("create.js.erb called");
$("#task-comments").val('');
$("#current-tasks").html('<%= j render "show_list", tasks: #tasks) %>');
3) => _show_list.html.erb(Use tasks instead of #tasks)
<% unless tasks.blank? %>
<ul>
<% tasks.each do |tsk| %>
<li><%= tsk.comments %></li>
<% end %>
</ul>
<% end %>
ok you have some problem here with your validation on the show_list partial.
you have this line
<% unless #tasks.nil? %>
try to use something more readable like
<% if #tasks.present? %>
and on your controller create action, you don't fill the #tasks variable, so that's the problem in reality, add on you controller the load for all the tasks after saving the new one.
if #task.save
#tasks = Task.all
flash[:success] = "Task added"
and that will make it work, but I recommend to load just the created one and add just one column to the table and not update the complete list again if you have all the others on the client already.

Getting the ability to post in a model under a different controller Rails

I decided to make a clone of Facebook in Rails. First I'm working on getting status updates working. I got it setup as the StatusUpdate model that is called by the Pages controller to render on the index page.
The issue I'm having is that if I use form_for(#status_update) I get:
undefined method to_key' for
<StatusUpdate::ActiveRecord_Relation:0x00000000049d3448>
Did you mean? to_set to_ary
If I use form_with(model: #status_update):
undefined method to_model' for
<StatusUpdate::ActiveRecord_Relation:0x000000000471cd80>
Did you mean? to_xml
If I use form_with(model: status_update):
undefined local variable or method status_update' for
<#<Class:0x0000000005801678>:0x0000000002ec8ec8>
Did you mean? #status_update
My action:
def create
#status_update = StatusUpdate.new(status_update_params)
respond_to do |format|
if #status_update.save
format.html { redirect_to root_path, notice: 'Status successfully posted!' }
else
format.html { render :new }
end
end
and erb view:
<%= form_with(model: status_update) do |sp| %>
<div class="form-group">
<%= sp.label :status_update %>
<%= sp.text_area :status_update, class: 'form-control', rows: 15, placeholder: 'Content' %>
</div>
<div class="form-group">
<%= sp.submit 'Submit', class: 'btn btn-primary' %>
</div>
<% end %>
I think you are missing the initialisation step. You have to first initialise the model object in new action of the controller.
def new
#status_update = StatusUpdate.new
end
and then use it in form.
form_with(model: #status_update)
The argument to form_with must be a single model instance. Not a whole collection.
class Pages
def index
#status_updates = StatusUpdate.all
#new_status_update = StatusUpdate.new
end
end
---
# app/views/pages/index.html.erb
<%= form_with(model: #new_status_update) %>
# ...
<% end %>
<%= #status_updates.each do |s| %>
# ...
<% end %>
Which is why you need to pay attention to pluralization when naming variables!
Another way to solve this is by using a condition:
# app/views/status_updates/form.html.erb
<%= form_with(model: local_assigns(:status_update) || StatusUpdate.new) %>
...
<% end %>
Which lets you use the form as a partial even without a StatusUpdate instance:
# app/views/pages/index.html.erb
<%= render partial: 'status_updates/form' %>
<%= #status_updates.each do |s| %>
# ...
<% end %>

.each loop returns []. Form / controller issue

I have RoR 4.2.0beta. (Although it s irrelevant as this is a beginer problem).
My form does not insert in the database the "propuneres" that I am creating trough it. And as a result they do not show in the index page when I get redirected to it. They show up when I create them through the console.
class PropuneresController < ApplicationController
before_action :prop_params
def new
#user = User.find(params[:user_id])
#propunere = #user.propuneres.build
end
def create
#user= User.find(params[:user_id])
#propunere = #user.propuneres.new(params[:prop_params])
#propunere.save
if #propunere.empty?
render 'new'
else
redirect_to user_propuneres_path
end
end
def index
#user = User.find(params[:user_id])
#propunere = #user.propuneres(params[:prop_params])
end
private
def prop_params
params.require(:propunere).permit(:titlu, :body)
end
end
new.html.erb
<h2> Propunere Nouă </h2>
<%= form_for #propunere do |f| %>
<ul>
<% #propunere.errors.full_messages.each do |error| %>
<li><%= error %></li>
<% end %>
</ul>
<p>
<%= f.label :titlu %><br />
<%= f.text_field :titlu %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
index.html.erb
<h2> Propuneri: </h2>
<% #propunere.each do |p| %>
<%= p.titlu %>
<%= p.body %>
<% end %>
Not sure if its relevant, but you have code
#propunere.save
if #propunere.empty?
render 'new'
else
redirect_to user_propuneres_path
end
Object #prorunere will never be empty, since you have
#propunere = #user.propuneres.new, which assigneds user_id to your #propunere object and
render 'new' will never be rendered, therefore you wont see any validation errors and never find out why your record wasnt created
Also since you have that piece of code, and dont see errors, this is what most like broke your code
#user.propuneres.new(params[:prop_params]) - you should use your permitted params, so it'd look like
#propunere = #user.propuneres.new(prop_params)
I've cloned your repo, here's the problem: in new.html.erb you had
&lt%= form_for #propunere, url: new_user_propunere_path(#user, #propunere), html: { method: :get } do |f| %&gt
Both the url and the method are wrong. user_propuneres_path will give you the correct url for the create action and the correct method is :post, not :get. This is why you never reached the create action.
You also need to change from #propunere = #user.propuneres.new(params[:propunere]) to #propunere = #user.propuneres.new(prop_params), otherwise you'll get a ActiveModel::ForbiddenAttributesError exception.
You can see all the routes in the app by running rake routes in the terminal.
I don't think you need the params in your index action:
#propunere = #user.propuneres
and it would be more logical to write it in plural since you have many of them.
Edit:
As Avdept suggested your create action should look like this:
def create
#user = User.find(params[:user_id])
#propunere = #user.propuneres.new(prop_params)
respond_to do |format|
if #propunere.save
format.html { redirect_to user_propuneres_path(#user), notice: 'Your propunere has been saved' }
else
format.html { render :new }
end
end
end
Do you have any validations for the Propunere model? Maybe the model is invalid. You can use
the create! method instead of create for testing, because it will throw an exception if the object cannot be saved. Also try puts #propunere.inspect before persisting it and check that the contents of the object are ok, the output will be shown in the development log.

Rails, STI and 'becomes' - f.object.errors not showing in view

My question is: why doesn't .becomes pass errors over to the new object? Isn't this the expected behaviour?
I have the following single table inheritance classes in a rails app:
class Document < ActiveRecord::Base
validates :title, :presence => true
end
class LegalDocument < Document
end
class MarketingDocument < Document
end
I want to use the same controller and set of views to edit both LegalDocuments and MarketingDocuments, so I am using DocumentsController < ApplicationController with the following edit and update actions:
def edit
#document = Document.find(params[:id])
end
def update
#document = Document.find(params[:id])
if #document.update_attributes(params[:document])
redirect_to documents_path, :notice => "#{t(:Document)} was successfully updated."
else
render :action => "edit"
end
end
and the following in my edit view:
<%= form_for #document.becomes(Document) do |f| %>
<% if f.object.errors.present? %>
<div class="error_message">
<h4><%= pluralize(f.object.errors.count, 'error') %> occurred</h4>
</div>
<% end %>
<div>
<%= f.label :title %>
<%= f.text_field :title, :class => "inputText" %>
</div>
<%= f.submit %>
<% end %>
If title is filled in, the documents update correctly.
If title is left blank, I am returned to the edit view BUT the error is not shown.
From debugging, I know it's not showing because f.object.errors is nil. However, from debugging, I also know #document.errors is NOT nil, as expected.
My question is: why doesn't .becomes pass errors over to the new object? Isn't this the expected behaviour?
Yes, I noticed that too.
Just change f.object.errors.present? by #document.errors.any? ( or #document.errors.present?).
If you really want to use f.object.errors.present?, write becomes in the controller (both edit and update actions) instead of in the view:
def edit
#document = Document.find(params[:id]).becomes(Document)
end
def update
#document = Document.find(params[:id]).becomes(Document)
# ....
end
And then in the view:
<%= form_for #document do |f| %>
<% if f.object.errors.present? %>
<p>Errrorsss....</p>
<% end %>
#.....
It happens because the url of the form is build according to #document.becomes(Document) (=> PUT document/:id) but #document is created according to its "true" class (a subclass of Document).
If you have pry (highly recommended), write:
def update
#document = Document.find(params[:id])
binding.pry
# ...
end
And then inspect #document. You will see that #document is an instance of LegalDocument or the other subclass even though you called #document.becomes(Document) in your form.
So in final f.object and #document are not the same.
This explains why you can't see f.object.errors when validation fails.
Edit
The 'best way' to deal with STI and form is NOT to use becomes:
<= form_for #document, url: { controller: 'documents', action: 'update' }, as: :document do |f| %>
<% if #document.errors.any? %>
# or if f.object.errors.any?
# handle validation errors
<% end %>
# your form...
<% end %>
This enables you:
to have only one controller (documents_controller)
to have only one resource (resources :documents)
it keeps trace of your subclasses: a LegalDocument will be store as a LegalDocument. No conversion: You don't have to store its class before the conversion to Document and then reassign it later.
Plus, your subclass is available in your form, so you can (let's imagine) build a select for the type.
your controller looks cleaner: #document = Document.find params[:id] nothing more. Just like a classic resource.
If you want to share this form across different actions(typically edit and new):
<%= form_for #document, url: { controller: 'media_files', action: action }, as: :media_file do |f| %>%>
# edit.html.erb
<%= render 'form', action: 'update' %>
# new.html.erb
<%= render 'form', action: 'create' %>
Pretty much it is a bug and it should work as you initially expected. The following patch to address the issue looks like it was pulled back in October
https://github.com/lazyatom/rails/commit/73cb0f98289923c8fa0287bf1cc8857664078d43

Resources