I am having a problem with the turbo-rails gem. First I installed the latest version in my Rails 7 application. On my site, I have a select input which is wrapped in a form, with a partial below that form that shows the data. Now I want to apply a filter using the select and dynamically update the data using this turbo-rails package. My form html looks like this:
<div class="users">
<div class="filters">
<%= form_with url: '/users/load', method: :get, data: { turbo_frame: :form_response } do |form| %>
<%= render partial: "shared/select", locals: {
placeholder: 'Gender',
width: '90px',
options: #genders,
classes: 'filter',
name: 'gender',
} %>
<%= form.submit %>
<% end %>
</div>
<%= turbo_frame_tag :form_response do %>
<%= render partial: "users/user_list", locals: {
users: #users
} %>
<% end %>
</div>
In my routes, I created this get request which is forwared to a load method in my controller like this:
get '/users' => "users#index"
get '/users/load' => "users#load"
And then in my controller I have the 2 methods written like this:
class UsersController < ApplicationController
before_action :require_user
USERS_PER_PAGE = 15
def index
#genders = ["Male", "Female"]
#users = User
.limit(USERS_PER_PAGE)
.order(:creation_date).reverse_order
end
def load
#users = User
.limit(USERS_PER_PAGE)
.order(:creation_date).reverse_order
if params[:gender]
#users = #users.where(gender: params[:gender])
end
respond_to do |format|
format.html { render partial: 'users/user_list', locals: { users: #users } }
end
end
end
The problem is that when I go to this page, select a gender and hit the submit button, I get to see the user data with the correct genders, but I only see the partial loaded, so the rest of the page is gone. I can see in the network tab of developer tools in Chrome that the request headers is set to:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
I want to use the turbo-streams instead of the turbo frames, because I need to update more of these items on the same page. Why is it not rendering the content inside the page, instead of rendering the partial only?
How can this be fixed?
To answer your question, you're rendering a partial without turbo stream or turbo frame, so you're only getting a partial as response.
I think, a few examples will explain everything.
# config/routes.rb
resources :users
# app/controllers/users_controller.rb
def index
scope = User.order(created_at: :desc)
scope = scope.where(name: params[:search]) if params[:search]
#users = scope
end
"Preserve log" is quite useful when working with turbo frame and it redirects and clears the console:
https://developer.chrome.com/docs/devtools/console/reference/#persist
Turbo FRAME using GET request with HTML response
We are in index action and the form is submitting back to index.
# app/views/users/index.html.erb
# expect :users_index turbo frame in a response vvvvvvvvvvvvvvvvvvvvvvvvv
<%= form_with url: users_path, method: :get, data: { turbo_frame: :users_index } do |f| %>
<%= f.text_field :search %>
<%= f.submit %>
<% end %>
# turbo frame in a response needs to match turbo frame on the current page,
# since we're rendering the same page again, we have the matching frame,
# only content inside this frame is updated.
<%= turbo_frame_tag :users_index do %>
<%= render #users %>
<% end %>
# If you render some other page, you have to wrap it in
# `turbo_frame_tag :users_index`
If you want to update the url as well, so you don't lose the search on refresh:
<%= turbo_frame_tag :users_index, data: { turbo_action: :advance } do %>
<%= render #users %>
<% end %>
Turbo STREAM using GET request with TURBO_STREAM response
You have to set data-turbo-stream="true" to send a GET stream.
# app/views/users/index.html.erb
<%= form_with url: users_path, method: :get, data: { turbo_stream: true } do |f| %>
<%= f.text_field :search %>
<%= f.submit %>
<% end %>
<%= tag.div id: :users_index do %>
<%= render #users %>
<% end %>
Add turbo_stream format to respond to this request:
# app/views/users/index.turbo_stream.erb
# update content inside <div id="users_index">
<%= turbo_stream.update :users_index do %>
<%= render #users %>
<% end %>
# add another `turbo_stream` here if you'd like.
Turbo STREAM using POST request with TURBO_STREAM response
# config/routes.rb
resources :users do
# # add `search` action
# post :search, on: :collection
# i'll be lazy and post to :index
post :search, action: :index, on: :collection
end
POST form submissions are sent as TURBO_STREAM by default and it will render index.turbo_stream.erb.
# app/views/users/index.html.erb
<%= form_with url: search_users_path do |f| %>
<%= f.text_field :search %>
<%= f.submit %>
<% end %>
<%= tag.div id: :users_index do %>
<%= render #users %>
<% end %>
# app/views/users/index.turbo_stream.erb
<%= turbo_stream.update :users_index do %>
<%= render #users %>
<% end %>
Test set up
Just do a simple set up:
rails --version
# Rails 7.0.4
rails new turbo-test -j esbuild
cd turbo-test
bin/rails g scaffold User name
bin/rails db:migrate
open http://localhost:3000/users
bin/dev
# app/controllers/users_controller.rb
def create
#user = User.new(user_params)
respond_to do |format|
if #user.save
# Add this line:
format.turbo_stream { render turbo_stream: turbo_stream.prepend(:users, partial: "user", locals: { user: #user }) }
format.html { redirect_to user_url(#user), notice: "User was successfully created." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
# app/views/users/index.html.erb
# submit form
<%= render "form", user: User.new %>
# new user gets prepended here
<div id="users">
<%= render #users %>
</div>
Related
I have a standard create method in the controller responsible for creating a new Transaction record. The Transaction record has a mandatory transaction_type field which I hide in the view and automatically assigning it a value by passing it a params[:filter] so I have one _form for both withdrawal and deposit transactions, like below:
#index.html.erb
<%= link_to 'Add funds', new_transaction_path(filter: 'deposit') %>
<%= link_to 'Withdraw Funds', new_transaction_path(filter: 'withdrawal') %>
#new.html.erb
<%= render 'form', transaction: #transaction, transaction_type: params[:filter] %>
#_form.html.erb
<%= simple_form_for #transaction do |f| %>
<%= f.error_notification %>
<%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
<%= f.text_field :transaction_type, value: transaction_type, type: "hidden" %>
<%= f.input :amount, placeholder: 'Amount', label: false %>
<%= f.button :submit, 'Submit' %>
<% end %>
If for some reason the validation fails, to properly display the errors, the :new view will be rendered. Unfortunately, in this case, if the user fills out the entire form again (after first failed), the record will not be created because params[:filter] was not passed. Is there any way to pass original params[:filter] directly to the view?
#controller
# POST /transactions
def create
#transaction = wallet.transactions.new(transaction_params)
if #transaction.save
redirect_to :index, notice: 'Transaction was successfully created.'
else
render :new
end
end
While I understand the aspect of reusing the view code you really should consider creating separate routes and controllers and solving the code duplication issues by using inheritance and locals instead of by sneaking along a hidden parameter.
resources :deposits, :withdrawls, only: [:new, :create]
class TransactionsController < ApplicationController
helper_method :create_transaction_path
def new
#transaction = Transaction.new
render 'transactions/new'
end
def create
#transaction = Transaction.new(transaction_params) do |t|
t.transaction_type = transaction_type
end
if #transaction.save
yield #transaction if block_given?
success_response
else
yield #transaction if block_given?
failure_response
end
end
private
def transaction_type
controller_name.singularize
end
def create_transaction_path
polymorphic_path(controller_name)
end
def transaction_params
params.require(:transaction)
.permit(:foo, :bar, :baz)
end
def success_response
redirect_to transactions_path,
notice: 'Transaction was successfully created.'
end
def failure_response
render 'transactions/new'
end
end
class DepositsController < TransactionsController
# POST /deposits
def create
super do |transaction|
# do something just when making a deposit
end
end
end
class WithdrawlsController < TransactionsController
# POST /withdrawls
def create
super do |transaction|
# do something just when making a withdrawl
end
end
end
# app/views/transactions/_form.html.erb
<%= simple_form_for transaction, url: create_transaction_path do |f| %>
<%= f.error_notification %>
<%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
<%= f.input :amount %> # Do not use placeholders instead of labels
<%= f.button :submit, 'Submit' %>
<% end %>
<%= link_to 'Add funds', new_deposit_path %>
<%= link_to 'Withdraw Funds', new_withdrawl_path %>
Why?
Because it gives you endpoints that do a single job and it also gives you the obvious structure for your code when the requirements diverge as they most certainly will.
Stashing the value as a hidden field in the form is the right idea, but you're using two different parameter names for the same thing.
Your link_to call passes the transaction type as filter:
<%= link_to 'Add funds', new_transaction_path(filter: 'deposit') %>
In your form, you are putting it in a hidden field called :transaction_type. Thus, when the form submits the value now goes to your controller in params[:transaction_type]. One simple fix is to change the name of your hidden field:
<%= hidden_field_tag :filter, params[:filter] %>
I'm trying to add the helper _form of my Comment model inside Advertisement view. I call the helper <%= render 'comments/form', comment: #comment %>in the route /advertisement/:id
So here is my comments _form :
<%= form_with(model: comment, local: true, url: "/comments") do |form| %>
<% if comment && comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
<ul>
<% comment.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="form-group">
<%= form.label :content %>
<%= form.text_area :content, class:"form-field" %>
</div>
<%= form.hidden_field :advertisment_id, value:params[:id] %>
<div class="actions">
<%= form.submit "Envoyer", class:"btn btn-primary", url: 'comments' %>
</div>
<% end %>
I specify the url: "/comments" because I'm at /advertisment/:id and by default the action is aimed to here.
But when the post reached my comment controller, it can't read the params.
ActionController::ParameterMissing in CommentsController#create
The strange part is that I've access to the params :
{"utf8"=>"✓", "authenticity_token"=>"0+swOqHPEHN2Gwh0TO3iC7VPRz4ROLoBlkaMkOdnjjYxWHoDer7AwrgnQpu+9VHfSY90yMSRsNp8ojvPJxuzmQ==", "content"=>"Test", "advertisment_id"=>"1", "commit"=>"Envoyer"}
So here is the comment controller :
class CommentsController < ApplicationController
before_action :set_comment, only: [:show, :edit, :update, :destroy]
# [...]
# POST /comments
# POST /comments.json
def create
#comment = Comment.new(comment_params.merge(:user_id => #session_user.id))
respond_to do |format|
if #comment.save
format.html { redirect_to #comment, notice: 'Comment was successfully created.' }
format.json { render :show, status: :created, location: #comment }
else
format.html { render :new }
format.json { render json: #comment.errors, status: :unprocessable_entity }
end
end
end
# [...]
private
# Use callbacks to share common setup or constraints between actions.
def set_comment
#comment = Comment.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def comment_params
params.require(:comment).permit(:content, :advertisment_id)
end
end
Thanks a lot for your help
It probably because your #comment is nil so that form_with works like the old form_tag helper.
By using form_with, a field like <%= form.text_field :content %>
If your model is presented, it will generate <input type="text" name="comment[content]" />
If your model is not presented, it will generate <input type="text" name="content" />
In the second case, your strong params check will reject the naked params
To be able to solve it, simply assign a valid model to your render
<%= render 'comments/form', comment: Comment.new %>
According to the docs here, if you specific url option in form_with without the scope option, the params received will be missing prefixes in there name. Therefore
params.require(:comment).permit(:content, :advertisment_id)
will raise an error since the params does not include comment.
You should remove url option in your form_with or use url and scope options at the same time.
I am currently trying to add a row to a map but am getting a route error:
No route matches [POST] "/maps/1/rows/new"
If I do a rake routes I see that there is a route for this in there so I am a bit confused as to why.
new_map_row GET /maps/:map_id/rows/new(.:format) rows#new
This is the form that I am using to create this row.
<%= form_for #row, method: 'post', url: new_map_row_path do |form| %>
<div class="field">
<%= form.label :timeframe %>
<%= form.text_field :timeframe %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
Rows Controller
def create
#row = #map.rows.create(params[:row].permit(:timestamp))
#row.save
respond_to do |format|
if #row.save
format.html { redirect_to #row.map, notice: 'Row was successfully created.' }
else
format.html { render :new }
end
end
end
Rows Model
class Row < ApplicationRecord
belongs_to :map
end
You shouldn't post to the new action itself, that's what renders the form. The destination for the create phase, the follow-up to new, is actually the collection path with method POST:
<%= form_for #row, method: :post, url: map_rows_path do |form| %>
That's where the create action kicks in.
I'm trying to create a function with ajax to edit and update comments in a form.
my edit function is working without problems using ajax but when i try to update the comment, i get the error: CommentsController#update is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: []
comments_controller
def update
respond_to :js
authorize #comment, :update?
#comment.update(comment_params)
if #comment.save
flash[:notice] = 'Commentaar is succesvol toegevoegd.'
else
flash.now[:alert] = 'Commentaar is niet toegevoegd.'
end
end
def comment_params
params.require(:comment).permit(:text)
end
update.js.erb
$("#comment-ajax-<%= #comment.id %>").html("<%= j render #comment %>");
_comment.html.erb
<% if policy(comment).edit? %>
<%= link_to 'edit', [:edit, comment.fabmoment, comment], remote: true, 'data-type' => 'script' %>
<% end %>
comment form
<%= simple_form_for [fabmoment, comment] do |f| %>
<!-- Input -->
<%= f.input_field :text, rows: 4 %>
<%= f.label :text %>
<% end %>
The error message is telling you that the form is being submitted as 'text/html'. Try adding remote: true to the actual form instead of the link_to.
I have a controller that has the following index action :
def index
if request.post?
flash[:notice] = "Searching.."
redirect_to songs_path # where songs_path is this index page that i am on
end
end
in my application layout i have defined the flash section as such
<% if flash[:notice] %>
<div id='notice'><%= flash[:notice] %></div>
<% end %>
and on my pages_path i've got
<% form_for :search do |f| %>
<%= f.label :search_text %>
<%= f.text_field :search_text %>
<p>
<%= f.submit "Search" %>
</p>
<% end %>
The final result should be a search through a youtube api ( youtube-g) but now i would only wish to make that flash notification appear when i click on the "Search" button , and it doesn't .. any ideas why ?
index action ordinary is GET request. So, if in your routes.rb there is something like resources :searches then your code won't work.
try this:
def create
render :text => "Searching.."
end
because POST /searches will refer to create action
you should update your RESTful route to like this
resources :songs do
collection do
get 'search'
end
end
Then in your Controller
def search
# Perform Your search here like Youtube ..
flash[:notice] = "Searching.."
redirect_to songs_path # This should work now.
end