I am trying to do a Ajax request in rails 7 when the form is submitted (when the button is pressed) it renders JavaScript
In my Stocks Controller I have:
class StocksController < ApplicationController
def search
if params[:stock].present?
@stock = Stock.new_lookup(params[:stock])
if @stock
respond_to do |format|
format.js {render partial: 'users/result'}
end
else
flash[:alert] = "Please enter a valid symbol to search"
redirect_to my_portfolio_path
end
else
flash[:alert] = "Please enter a symbol to search"
redirect_to my_portfolio_path
end
end
end
My Form I have:
<div class="search-area">
<h3>Search Stocks</h3>
<%= form_tag search_stock_path, method: :get, remote: true do %>
<div class="form-group row">
<div class="col-sm-9 noRightPad">
<%= text_field_tag :stock, params[:stock], placeholder: "Stock ticker symbol",
autofocus: true, class: "form-control form-control-lg" %>
</div>
<div class="col-sm-3 noLeftPad">
<%= button_tag type: :submit, class: "btn btn-success" do %>
<%= fa_icon "search 2x" %>
<% end %>
</div>
</div>
<% end %>
Thank you for your time.
Update Controller Updated
class StocksController < ApplicationController
respond_to :js
def search
if params[:stock].present?
@stock = Stock.new_lookup(params[:stock])
if @stock
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.update(
"results",
partial: "users/result" # render any partial and remove js code.
)
end
end
else
flash[:alert] = "Please enter a valid symbol to search"
redirect_to my_portfolio_path
end
else
flash[:alert] = "Please enter a symbol to search"
redirect_to my_portfolio_path
end
end
end
Form Tag
<%= form_tag search_stock_path, method: :get, data: {turbo_stream: true}, remote: true do %>
I have researched the 406 Not acceptable error in console but it says to add respond_to which I have done and still the same issue
I'll try to demystify the magic here, I've seen this question come up a lot.
Setup:
rails new rails_formats -c tailwind
cd rails_formats
bin/rails g scaffold stock name
bin/rails db:migrate
open http://localhost:3000/stocks/new
bin/dev
Rails abstracts this out of the away so you don't have to deal with this. There are two headers that you just have to know:
Content-Type
This is what you send to the server with your request, and what you send back with the response. For example, you can send multipart form data and get json as a response (when you need to upload images to your api server).
Accept
This is what you want to get as a response. This is the one that determines what format block rails will run.
It's what goes into Accept and Content-Type headers. Rails has a class to handle it:
https://api.rubyonrails.org/classes/Mime/Type.html
Mime::Type.register "text/vnd.hyper-stream.html", :hyper
# if you send this in Accept ^ header, then run this ^ format block
ActiveSupport.on_load(:action_controller) do
ActionController::Renderers.add :hyper do |html, options|
# set response type if rendering ^ `render hyper: ..`
self.content_type = Mime[:hyper] if media_type.nil?
html
end
end
# now you have your own format
format.hyper { render hyper: ... }
You know what headers to use, here is how to use them:
<!-- app/views/stocks/_form.html.erb -->
<!-- disable Turbo for now so it doesn't interfere -->
<script type="module"> Turbo.session.drive = false </script>
<!-- make your own remote form -->
<div id="remote_response"></div>
<%= form_with model: stock, html: {onsubmit: "remote(event)"} do |form| %>
<%= form.submit %>
<% end %>
*Use event listeners and event delegation in real apps.
<script charset="utf-8">
function remote(event) {
event.preventDefault();
const form = event.target;
fetch(form.action, {
// headers: { "Accept": "text/html" },
// headers: { "Accept": "text/vnd.turbo-stream.html" },
headers: { "Accept": "application/json" },
method: form.method,
body: new FormData(form),
})
.then(response => response.text())
.then(text => {
document.querySelector("#remote_response").innerHTML = text;
})
}
</script>
def create
puts "# CONTENT TYPE | #{request.content_type}" # what you sent
puts "# ACCEPT | #{request.accept}" # what you want
# Change Accept header in `fetch` to choose which format block to run
respond_to do |format|
format.html { render html: "Responded with html" }
format.json { render json: "Responded with json" }
format.js { render js: "console.log('railsujs')" }
format.turbo_stream { render turbo_stream: "Responded with turbo stream" }
end
puts "# RESPONSE TYPE | #{response.content_type}" # what you get
end
remote: true
(or local: false
if using form_with
) will add data-remote="true"
to the form tag and that's it. Something on the frontend has to know what to do with it. That something is RailsUJS which was replaced by Turbo in rails 7.
$ bin/importmap pin @rails/ujs
Just pick Turbo or RailsUJS:
// app/javascript/application.js
// import "@hotwired/turbo-rails"
// import "controllers"
import Rails from "@rails/ujs";
Rails.start();
<%= form_with model: stock, local: false do |form| %>
...
Now RailsUJS will do what we did with remote()
function and set Accept
to text/javascript
so that format.js
block would run. It will also handle the response and execute the code.
Streams and frames and broadcasts take a little bit to get your head around it. Just start with turbo_stream
I found it much easier to understand at first.
No set up needed, all forms submit "remotely" as TURBO_STREAM format aka "Accept": "text/vnd.turbo-stream.html, text/html, application/xhtml+xml"
. This means you can respond with format.html
or format.turbo_stream
.
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.update(
"id_of_the_element_to_update",
partial: "users/result" # render any partial and remove js code.
)
end
end
GET streams
Add data-turbo-stream="true"
to forms with method: :get
and links.
form_tag "/", method: :get, data: {turbo_stream: true} do
https://turbo.hotwired.dev/handbook/streams#streaming-from-http-responses
Bootstrap
You can't import "bootstrap" unless you have a pin for it:
import * as bootstrap from "bootstrap"
// ^^^^^^^^^
// browser doesn't know how to get that
Add a pin (and get styles from cdn):
bin/importmap pin bootstrap
If importmaps add too much hassle use cssbundling-rails
. Rails has this built in:
rails new my_app -c bootstrap
But there are lots of other answers on how to install bootstrap in rails 7.
Debug
Working with javascript, you have to look at your browser console.