Search code examples
ruby-on-railsrubyruby-on-rails-7

Rails 7 respond_do Error ActionController::UnknownFormat


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 %>

Error Message: enter image description here

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


Solution

  • 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
    

    Headers

    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.

    Mime types

    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: ... }
    

    Controller and Form

    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}

    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.


    Turbo

    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
    

    Update

    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.