Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Making a Single Page Search with Hotwire
Tony Rowan
Tony Rowan

Posted on

     

Making a Single Page Search with Hotwire

Cover photo byTim Meyer onUnsplash

Turbo (part of theHotwire suite) is a supercharged version of Turbolinks. If you're used to immediately turning off Turbolinks in any new project, you might be surprised to learn that you don't need React or Vue to build a rich and interactive web app.

What We're Building

We're going to be adding a single page search to a simple Single Page App (SPA) that allows users to view information about some Movies.

Demo of the end result

Starting Point

The starting point is heavily based on 'Using Hotwire with Rails for a SPA like experience' by Mike Wilson. If you want to follow along with this post you can use the starting pointhere on Github or follow along through that post. You should have a simple app where you can select a movie without reloading the page, with only a very simple rails controller.

Demo of starting point

classMoviesController<ApplicationControllerdefindex@movies=Movie.allenddefshow@movie=Movie.find(params[:id])endend
Enter fullscreen modeExit fullscreen mode

Adding Search

The first step is to add search support to the controller.

classMoviesController<ApplicationControllerdefindex@movies=movies@query=queryenddefshow@movie=Movie.find(params[:id])endprivatedefmoviesifqueryMovie.where("title ILIKE ?","%#{query}%")elseMovie.allendenddefqueryparams[:query]endend
Enter fullscreen modeExit fullscreen mode

We can verify this is working by simply adding thequery parameter to the URL and visiting the page again. Only the movies matching the query you made are present in the list.

Not very user friendly though. Let's add a search form to the top of the list of movies, within theturbo-frame-tag named:index - the same turbo-frame with the list of movies.

<%=form_with(url:movies_path,method: :get)do|f|%><%=f.text_field(:query,value:@query,placeholder:"Search")%><%end%>
Enter fullscreen modeExit fullscreen mode

Now when you make a search and hit enter, the list of movies is filtered. You'll also notice that, if you have a movie selected, it stays selected. Searching doesn't cause a full page refresh, only the list of movies updates.

This happens because each turbo-frame creates its own navigational context. By default, all links and form submissions from within a given turbo-frame only effect that turbo-frame. Under the hood, turbo is hijacking the submission event, making an AJAX call for the form submission and interpreting the result.

When the submission is successful turbo reads the response, looking for aturbo-frame-tag that has the same name as the turbo frame that caused the submission. If it finds a match, it swaps out the contents.

In our case, the form submission returns the same page so it is pretty clear that there will be a matching turbo-frame - the:index frame.

I said this was the default behaviour. By adding aturbo-target attribute to a form or link turbo will instead look for, and swap the contents in, the turbo frame with the name you passed - this is how clicking the movies changes the:details turbo frame and not the:index frame.

Come Alive with Stimulus

This is pretty good, but what if the userdidn't have to press enter when they were done searching? We can search as the user types and we can do this with very little Javascript thanks toStimulus - another part of the Hotwire suite.

We can watch for input changes on the text field of the search form and call an action on a Stimulus controller to submit the form for the user. This can be achieved with a small amount of markup on the form and the text field.

<%=form_with(url:movies_path,method: :get,data:{controller:"submit-form",submit_form_target:"form",})do|f|%><%=f.text_field(:query,value:@query,placeholder:"Search",data:{action:"input->submit-form#submit"})%><%end%>
Enter fullscreen modeExit fullscreen mode

On the text field we've addeddata-action="input->submit-form#submit". The value assigned to thedata-action attribute encodes which event to listen for and which controller and action (method) to invoke when the event happens. The generic markup isevent->controller-name#action. In our example,event isinput,controller issubmit-form and ouraction issubmit.

Theinput event name comes from theJS DOM events, and we want to listen to any changes in the input. The controller namesubmit-form is just the name we gave the controller we want to invoke here - it is expected to be defined in a file namedsubmit_form_controller.js. Its name in Javascript land will then beSubmitFormController. Thesubmit action is just a public method defined on that controller.

On the form element we've addeddata-controller="submit-form" anddata-submit-form-target="form". The former simply tells Stimulus to attach a new instance of theSubmitFormController to the form element, allowing us to invoke its actions from the form element or any of its children. The latter attribute makes the form element available from the controller as a 'target' calledform - from the controller we will be able to access the element by callingthis.formTarget.

TheSubmitFormController is very simple.

import{Controller}from"stimulus";exportdefaultclassextendsController{statictargets=["form"];submit(){clearTimeout(this.timeout);this.timeout=setTimeout(()=>{this._submit();},300);}_submit(){this.formTarget.requestSubmit();}}
Enter fullscreen modeExit fullscreen mode

When thesubmit action is called it submits the target, which is assumed to be a form. The action itself is debounced. This stops us from generating too many simultaneous requests which makes the submissions overlap.

Thethis.formTarget.requestSubmit(); is very important, if we usedsubmit() rather thanrequestSubmit(), the event that Turbo hooks into the intercept the form submission would not happen and you would have a normal form submission - with a full page refresh.

A Small Problem

This works really well - until the first time the form is submitted - then the focus in the text field is lost and we can't keep typing. That's definitely a bug!

What's happening here is that when the response is returned, we're replacing the whole contents of the:index turbo frame - including the search form itself. To avoid this we can move the search form outside of the turbo frame, and then addturbo-target: :index to the form element. This will mean when the form is submitted, turbo will replace the contents of the:index turbo-frame and leave the search field completely untouched.

Conclusion

And there we have it. We've added a single page search to an app and all we needed was a small amount of markup and very small amount of Javascript. That's the power of Turbo.

The resulting code can be found onGithub.

Top comments(1)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
bestfriend101 profile image
BestFriend101
  • Joined

I actually just finishedcolby.so/posts/live-search-with-ra... a few hours ago after weeks of confusion and dread. Curious what you think of that approach vs. Hotwire?

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Full stack engineer tinkering with projects big and small. I've built apps, games and web apps.
  • Location
    St. Helen's, United Kingdom
  • Education
    University of Manchester
  • Pronouns
    he/him
  • Work
    Ruby Engineer @ char.gy
  • Joined

More fromTony Rowan

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp