I'm curious to know how this is done. Lets say I have a simple Product model and on one page wanted to click a link and add a product form by AJAX. I than pop up other product forms, finish the first one and submit it and do the same to the others.
Here is the code I will use.
On the index page you can add a product form by the link, create it and see it in a list.
products/index.html.erb
<h1>Products</h1>
<%= link_to "Product", new_product_path, :remote => true %>
<div id="product_form">
<%= render 'form' %>
</div>
<ul id="products">
<%= render :partial => #products.reverse %>
</ul>
products/_form.html.erb
<%= form_for(#product, :remote => true) do |f| %>
<%= f.text_field :name %>
<%= f.text_field :price %>
<%= f.submit %>
<% end %>
products/_product.html.erb
<%= content_tag_for(:li, product) do %>
<p><%= product.name</p>
<p><%= product.price %></p>
<% end %>
ProductsController
def index
#products = Product.all
#product = Product.new
end
def create
#product = Product.new(params[:product])
respond_to do |format|
if #product.save
format.html { redirect_to products_url }
format.js
else
format.html { render action: "index" }
format.js
end
end
end
When it gets created it should show the product in the _product partial.
products/create.js.erb
$('#products').prepend('<%= escape_javascript(render(#product)) %>');
The link when clicked will make the product form appear in the <div id="product_form">
products/new.js.erb
$("#product-form").html("<%= escape_javascript(render(:partial => 'products/form', locals: { product: #product })) %>");
Now this generates one product form but I want to know the code logic behind rendering other product forms on the same page. How would this be done?
Usually I do this with a second object representing the collection of products. This can be an activerecord if it fits your business logic (something like a ProductCategory, or a ShoppingCart) or a simple ActiveModel "Products" with a save method that would save each of its related products.
Active Presenter can give you more details of this mechanism but I wouldn't use that gem since it's activity is quite low.
Related
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>
One Article has_many Images. When creating a new Article, Users can add 2 images max.
In my controller I run "build" for images only twice, but when I submit the form that has 3 image fields, it succeeds. Is there any need to run "build" at all? It seems pointless in this scenario, is there another way to better ensure only 2 images are accepted?
articles_controller.rb
def new
#article = Article.new
2.times { #article.images.build }
end
Note the "2.times" here.
def create
#article = Article.new(place_params)
#article.user = current_user
respond_to do |format|
if #review.save
params[:images][:image_file].each do |image_params|
#image = #article.images.create(image_file: image_params, user: current_user)
end
end
end
end
_form.html.erb
<%= form_with(model: article, url: create_article_path(#article), local: true) do |form| %>
<div class="field">
<%= form.label :title %>
<%= form.text_area :title %>
</div>
<%= form.fields_for :images, #image do |image| %>
<div class="field">
<%= image.label :image_file_1, "Image 1" %>
<%= photo.file_field :image_file, name: "images[image_file][]", id: :article_images_image_file_1 %>
</div>
<div class="field">
<%= image.label :image_file_2, "Image 2" %>
<%= photo.file_field :image_file, name: "images[image_file][]", id: :article_images_image_file_2 %>
</div>
<div class="field">
<%= image.label :image_file_3, "Image 3" %>
<%= photo.file_field :image_file, name: "images[image_file][]", id: :article_images_image_file_3 %>
</div>
<% end %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
SUCCESS (But why?)
In short -- Your build statement is prepping the view to have 2 child objects. But you're manually creating them, so you're rendering the build statement as useless. You don't have to do it this way, you can declare nested attributes in the model, then whitelist in the controller, then auto-add them in the view. (see code example below)
Build itself does change how many objects are instantiated, but you're overriding that.
You are also manually saving the images, which you do not have to do. There's a bit of rails magic that saves all the children for you, if you've built them properly.
CodeView
1 The Model
app/models/article.rb
class Article < ApplicationRecord
has_many :images
validates :images, length: {maximum: 2}
accepts_nested_attributes_for :images
end
2 bits of note here. Firstly, in your validation, only allow 2 object, if you try to save a third, it will fail. Secondly, accepting the attribute in the model allows you to create safe params in the controller, thus alleviating your need to manually create. (unless of course, you really want to)
2 The View
<%= form_with(model: article, url: article_path(#article), local: true) do |form| %>
<div class="field">
<%= form.label :title %>
<%= form.text_area :title %>
</div>
<%= form.fields_for :images do |image_form| %>
<div class="field">
<%= image_form.label "image_file_#{image_form.index + 1}" %>
<%= image_form.file_field :image_file %>
<%= image_form.hidden_field :user_id, value: current_user.id %>
</div>
<% end %>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
The change here is a) I added the user directly to the form b.) because you are accepting attribute in the model and we'll whitelist the attribute in the controller, you don't need to pass an object to the field_for -- :images will do just fine. And because you will say to build it twice in your controller, you'll have 2 image objects in the form. Additionally, because you wanted a label of Image 1 and Image 2, with fields_for you automatically get access to the index of the object (just like you'd have with any array) by calling object.index.
3 The Controller - New Action
app/models/article.rb
Your action works perfectly well, keep it just as it is.
def new
#article = Article.new
2.times { #article.images.build }
end
4 The Controller - Strong Params
def article_params
params.require(:article).permit(:title, :body, images_attributes: [:id, :article_id,:user_id, :image_file])
end
Whitelisting your params altogether will save time and it's easier to read than permitting them in each controller, though you CAN do that if you need to, for instance if params are allowed in certain actions but not in others.
5 The Controller - Create Action
def create
#article = Article.new(article_params)
respond_to do |format|
if #article.save
format.html { redirect_to #article, notice: 'Article was successfully created.' }
format.json { render :show, status: :created, location: #article }
else
format.html { render :new }
format.json { render json: #article.errors, status: :unprocessable_entity }
end
end
end
This will probably look similar if not identical to the default create action in a scaffold, and this is all you need. The child image objects will not be created unless the parent can be created, so you don't have to worry about adding them in an if save.
I have a form and a div for showing the message which is sent by form. I have to refresh the page after sending a message by form to see the messages in my div. However, I want to see the messages in div without refreshing the page, like a chatroom. I tried to use Ajax but I couldn't make it.
Here is my form:
<%= form_for #message, remote:true do |f| %>
<%= f.text_area :body, class: "form-control", placeholder: "Enter Message" %>
<%= f.submit "Send", class: 'btn btn-primary btn-lg' %>
<% end %>
And here is my controller
class MessagesController < ApplicationController
def index
#messagesAll = Message.all
#message = Message.new
end
def new
#message = Message.all
end
def create
#message = Message.new(params.require(:message).permit(:user, :body))
if #message.save
respond_to do |format|
format.js {}
format.html {redirect_to messages_url}
end
else
render 'new'
end
end
end
And here is my div:
<div class="panel-body" id="messages-div">
<ul class="media-list">
<% #messagesAll.each do |post| %>
<li class="media">
<%= post.body %>
</li>
<% end %>
</ul>
</div>
Could anyone help me? Thank you.
Assuming you have a div with an id of messages that you're rendering the messages in e.g.
<div id="messages">
<%= render #messages %>
</div>
On your create action rails will look for a create.js.erb file. So what you want is this in your controller:
respond_to do |format|
format.js {}
format.html {redirect_to messages_url}
end
Then have a create.js.erb file:
$('#messages').append('<%= j render #message %>');
Edit: In your specific case you would need to target the ul. So your create.js.erb would be
$('#messages-div ul.media-list').append('<li class="media"><%= j #message.body %></li>');
j is just a shortcut for escape_javascript. Docs
That should get you started, but you'll want to do something else via ajax if the message isn't saved.
I've been told that I should not create my Quiz object before my quiz is completed; A user could go to the quiz page, not complete it, and there would be an 'unused' quiz sitting on the database. I can see the logic of that.
I CAN'T see how my quiz is supposed to work without being passed a #quiz object. Here's my QuizzesController, which, when the quiz is needed, gets routed to the 'new' action:
class QuizzesController < ApplicationController
def index
end
def new
#user = current_user
#quiz = Quiz.create(user_id: current_user.id)
end
def create
#results = Quiz.where(user_id: current_user.id).last
redirect_to results_path
end
end
At the moment, you can see that I'm coding the actions as simply as possible. Later, in the 'new' action, I'll add a test to see if the current_user has done the quiz and, if so, redirect to results_path.
Here is my form partial which is rendered as part of quizzes/new.html.erb:
<%= form_for(#quiz) do |f| %>
<p>
<%= f.check_box(:answer1) %>
<%= f.check_box(:answer2) %>
<%= f.check_box(:answer3) %>
<%= f.check_box(:answer4) %>
<%= f.check_box(:answer5) %>
<%= f.check_box(:answer6) %>
<%= f.check_box(:answer7) %>
<%= f.check_box(:answer8) %>
</p>
<p>
<%= f.submit("Get my results!") %>
</p>
<% end %>
Once again, the quiz is very simple while I figure out what's going on.
But I'd like to know, if the #quiz object is not created in the 'new' action, what would I pass into form_for to build the form?
You can instantiate a Quiz object without saving it to the database:
def new
#user = current_user
#quiz = Quiz.new(user_id: current_user.id)
end
The generally used sequence of requests/actions is the following:
The new action just initializes the model's instance with default values, and renders the record with empty fields, usually in a edit view.
def new
#quiz = Quiz.new(user_id: current_user.id)
render :edit
end
create action create the record, and after the create action you should render either the view of the newly created record by redirection to show action with the same view, or to redirect to a new action, in case you are creating a sequence of the same instances of a model.
def create
#quiz = Quiz.create(params)
render :show # or redirect_to :new
end
edit action is to prepare edit fields, is it renders edit view with filled-in fields.
def edit
#quiz = Quiz.where(id: params[:id]).first
end
update action updates the record with values set in edit view, then it renders the show view on the current record.
def update
#quiz = Quiz.update(params)
render :show
end
show action just shows the model's found out with stored in the DB values, then it renders show view with filled-in fields.
def show
#quiz = Quiz.where(id: params[:id]).first
end
So in your show.erb view you get rendering the newly built, or found out instance of Quiz:
<%= form_for #quiz, url: {action: "create"} do |f| %>
<p>
<%= f.check_box(:answer1) %>
<%# ... %>
</p>
<p>
<%= f.submit "Create Quiz" %>
</p>
<% end %>
But I prefer simple-form gem:
<%= simple_form_for #quiz do |f| %>
<%= f.input :answer1, as: :boolean, checked_value: true, unchecked_value: false %>
<%= f.button :submit %>
<% end %>
When I add a model in my index action, the create action is invoked that adds an instance of the model to the database. This is the following create action:
tracks_controller.rb
def create
#track = Track.new(params[:track])
if #track.save
redirect_to(root_url) //Want to change this!
else
#tracks = Track.all
render :action=>"index"
end
end
Where you can see that I am redirected to my root url (where I want to be) everytime create is invoked. However, how can I carry this out without refreshing the page? Since tracks are being played, I do not want the page to be refreshed whenever something is added to the database.
If I change this line to render :action=>"index", then I receive the following error in my index.html.erb file
undefined methodeach' for nil:NilClass`
15: <p>Database is empty!</p>
16: <%else%>
17: <br>
18: <% #tracks.each do |track| %>
19: <div id="list_container">
20: <ul>
21: <li class="list_container">
How do I go about achieving this?
form
<%= form_for #track, remote: true %>
form fields
<%= f.submit %>
<% end %>
app/views/tracks/create.js.erb
<% if #track.valid? %>
$(".tracks").prepend('<%= j(render(#track)) %>'); // make sure you have _track.html.erb
$("ID OR CLASS OF YOUR FORM")[0].reset(); // this will clear your form inputs
<% else %>
alert('Something Went Wrong');
<% end %>
app/views/tracks/index.html.erb
<div class="tracks">
<% #tracks.each do |track| %>
<%= render :partial => 'track' %>
<% end %>
</div>
app/views/tracks/_track.html.erb
some code here to show track:
<div>track.id</div>
<div>track.name</div>
index.html.erb needs the #tracks variable. Therefore, you have to set it before rendering the page.
For example:
def create
#track = Track.create(params[:track])
#tracks = Track.all
render :action => "index"
end
The simplest, quickest way to get it is to add :remote => true to you form_for!
If you are posting track from a form then make the remote to true
like
<%=form_for #track, :remote => true do |f| %>
your input fields
<%end %>
index.html.erb
<div id="track_list">
<%= render :partial => 'tracks_record' %>
</div>
_tracks_record.html.erb
<% #tracks.each do |track|%>
your code
<% end %>
in controller
def create
#track = Track.create(params[:track])
#tracks = Track.all
respond_to do |format|
format.js
end
end
Create one js.erb file for create.js.erb
<% if #track.valid? %>
('#track_list').html('<%= escape_javascript( render :partial => "tracks_record" ) %>');
<% else %>
alert('Could not save');
<% end %>
This will refresh the data and populate the new entries without refreshing the page.
Unfortunately I can't comment on Debadatt's suggestion but I found it much better than the top post. My only notes are to change this to
def create
#track = Track.create(params[:track])
#tracks = Track.all
respond_to do |format|
format.js
end
end
to this:
def create
#track = Track.create(params[:track])
#tracks = Track.all
respond_to do |format|
format.js { render layout: false, content_type: 'text/javascript' }
end
end
And this:
('#track_list').html('<%= escape_javascript( render :partial => "tracks_record" ) %>');
to this:
$('#track_list').html('<%= escape_javascript( render :partial => "tracks_record" ) %>');
Great suggestion thanks Debadatt