Hotwire Discussion

Search navigation with Turbo

I am trying out Turbo and have the setup below (simplified). Basically I have a list of cocktails which I would like to filter using a form.

<div data-controller="form">
  <%= search_form_for @q, :data => { turbo_frame: 'cocktail_list', form_target: 'form' } do |f| %>
    <!-- Search form with text search and js daterange picker -->
  <% end %>
</div>

<turbo-frame id="cocktail_list">
  <!-- Table for cocktails with sorting, pagination and link to cocktail -->
</turbo-frame>

I can’t quite wrap my head around how you can retain the form values when navigating to a specific cocktail and navigate back to the list? The list of cocktails is cached, but the form is reset. I have tried wrapping everything, including the form, in one single frame and then it works, but is this the right way of doing it or am I missing something?

On the top of my head, have you tried data-turbo-permanent option?

I have, but I believe that is only for elements that exists across pages, i.e. both on the page you are on and the one your are navigating back to. Wrapping everything in one frame does seem to work, however, then the inputs in the form looses focus since they get replaced with the html in the server response, which is why I am thinking that I am probably going about this the wrong way.

Hey @Michael_L! I actually put together an example repo for exactly this functionality in another thread. Here’s a link to the sample code.

To break down the important parts, first I have a search form that I have on the index page of my resource.

<%= form_tag('/search', method: :get, data: { controller: "search" }) do %>
  <%= text_field_tag :search, params[:search], data: { action: "input->search#findResults" } %>
<% end %>
<div id="books"></div>

Then I created a stimulus controller that submits the form using the following code.

// app/assets/javascripts/controllers/search_controller.js

import { Controller } from "stimulus"

export default class extends Controller {
  findResults(event) {
    this.element.requestSubmit();
  }
}

Next, I have a search controller that processes the request that the form dispatches.

# app/controllers/search_controller.rb
class SearchController < ApplicationController
  def search
    @books = Book.search_title(params[:search])

    respond_to do |format|
      format.turbo_stream
      format.html         { redirect_to root_url(search: params[:search]) }
    end
  end
end

Finally, I have a turbo-stream template that populates the search results.

<turbo-stream action="update" target="books">
  <template>
    <ul>
      <% @books.each do |book| %>
        <%= content_tag :li, book.title %>
      <% end %>
    </ul>
  </template>
</turbo-stream>

You can find the full discussion of why I’m using methods like requestSubmit() and how we could limit the rate of requests in the full post here.

Hope this helps!

1 Like

Thanks jacob, however I do not think that this addresses my problem. My problem is that when the user visits a link from my search results (with turbo-frame = “_top”) and then go back, my form is cleared, only the frame restores from cache. However, I have come up with a bit of a hack that seems to work okay so far. Basically I get the src of the turbo-frame, parse the url params and set the form values accordingly.

My view now looks like this:

<div data-controller="form">

  <%= search_form_for @q, :data => { turbo_frame: 'cocktail_list', form_target: 'form' } do |f| %>
    <!-- Search form with text search and js daterange picker -->
  <% end %>

  <turbo-frame id="cocktail_list" data-form-target="frame">
    <!-- Table for cocktails with sorting, pagination and link to cocktail -->
  </turbo-frame>

</div>

And my stimulus controller:

import { Controller } from "stimulus"

export default class extends Controller {
    static targets = [ "form", "frame" ]

    connect() {
        // Set frame parameter values if applicable
        let src = this.frameTarget.getAttribute("src")
        if(src) {
            let query = unescape(src.slice(src.indexOf('?') + 1))
            const searchParams = new URLSearchParams(query)

            for (let p of searchParams) {
                let formElement = this.formTarget.elements[p[0]]
                if(typeof(formElement) != 'undefined' && formElement != null) {
                    formElement.value = p[1]
                }
            }
        }
    }

    update(event) {
        this.formTarget.requestSubmit()
    }
}

Not exactly the prettiest solution, so would still love to hear how others have accomplished something like this. This post addresses the same issue but I have not been able to get data-turbo-permanent to work for this.

2 Likes

I’ve been spying on this topic for a little bit. I think maybe reframing the problem in terms of what would happen if JS was disabled seems to be a useful heuristic to figure out what the best course of action is in terms of using turbo-frames vs turbo-streams. Turbo is designed to be an enhancement on top of standard forms/web.

In this case, you have an index/search page that lists off results and you have a show page. How would it work if javascript/turbo wasn’t a part of the picture? You’d submit your query, it’d presumably add a query string param, e.g. ?query=mojito and return a list of results. Then in your results, you’d have a link to each matching item, e.g. cocktail_path(id). Ok now you navigate to the show page - how would you return to the search that you had previously? You’d have to pass the query string params through to the show page right? That’s one option, e.g. cocktails/id?search_query=mojito, you could then construct a back button to return to the search page and re-run that search again (& fill out the form). You could also persist the query somewhere when you execute the search and send the id through, e.g. cocktails/id?search_id=1234 and when you revisit the search page, you can replay the search by loading the model.

So that’s how that could work w/o any turbo enhancements. I think layering on a turbo-frame or turbo-stream becomes pretty easy after that… It’s a little tricky if you use the frame because the search page url won’t update (what if someone refreshes?) but that’s a normal consequence that you have to deal with if you use ajax for search results :slight_smile:

Hopefully that helps a little bit?

1 Like

Yup, this is excellent advice. “Party like it’s 1999.” Recall that the Basecamp (they’ll always be 37Signals to me) folk have been doing this as long as I have, which is to say, since before Web2.0. Time was, you built your page in HTML, with PHP or (god help you) CGI behind it, and anything else you added to make it look shinier, you added with the certain knowledge that a meaningful percentage of your audience would not see it, due to browser deficiencies or well-founded paranoia. Layer your JS on top, like the fondant on a wedding cake. But make sure it tastes good without that icing, first.

Walter

1 Like