Triggering Turbo Frame with JS

This is a bit out of topic, but I would like to mention it anyway.

Because of out of order responses, I would suggest to use Stimulus and manually issue and abort fetch requests. I.e. if we need to send a new fetch request and the previous one did not finish yet, then just abort the previous one. This fixes out of order search results.

You can see it in my Stimulus search example here: https://github.com/mrhead/stimulus-search/blob/main/app/javascript/controllers/search_controller.js

Back to the original problem. Yesterday I thought that requestSubmit() is the right answer. However, as it was pointed out by @walterdavis, it’s not supported by Safari.

So I tried to manually fire the submit event:

this.element.closest("form").dispatchEvent(new CustomEvent("submit", { bubbles: true }))

This works in Safari and Chrome, but not in Firefox which just submits the form without Turbo :man_shrugging:.

So if anyone is using the custom event approach, then I would suggest to test it in Firefox.

In the meantime, I’m going to look for a solution which work in all mainstream browsers :).

I’ve decided to use form-request-submit-polyfill which is pretty lightweight and works well for me.

2 Likes

And coming full circle, if you dig into how it works, it creates a hidden submit button and “clicks” it!

Walter

3 Likes

So… I might have found something.

This might be a private API, but we can use Turbo’s Navigator class to submit forms.

Here is an example:

import { Controller } from "stimulus"
import { navigator } from "@hotwired/turbo"

export default class extends Controller {
  submit() {
    navigator.submitForm(this.element.closest("form"))
  }
}

The issue is that you need to use the npm package and as far as I know you can’t use this if you use turbo_include_tags from turbo-rails.

You can see it in this experimental pull request: https://github.com/mrhead/todos/pull/58/files

2 Likes

For what it’s worth, I’ve kept rails-ujs to submit forms for now. I’m using it like this:

Rails.fire(form, "submit")

Sure enough, for a form (without a submit button) that’s in a Turbo Frame this does trigger turbo requests.

Now there has been some discussion about keeping rails-ujs, but I’m not sure if this particular function should be on the list (@dhh?). And I’m not even sure if I’m abusing stuff or if Rails.fire is indeed a sure-fire way of getting this to work.

2 Likes

requestSubmit works!

simmilar to your solution, in mine I’m using target

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = [form]
  submit() {
    formTarget.requestSubmit();
  }
}


<%= form_with model: todo, url: toggle_todo_path(todo), method: :post, data: {target: "checkbox.form"} do |form| %>
  <%= form.check_box :completed, data: { controller: "checkbox", action: "checkbox#submit" } %>
<% end %>

(something like this, I’m writing from top of my head, but you get the picture :slight_smile: )

So, can anybody confirm whether requestSubmit() also works for Turbo streams too?

That would solve @danjac’s problem:

one problem I have is sending updates generated from HTML media element events, so using a form submission is not really an option. Of course I use fetch(); but then I’m handling low level HTML changes in Stimulus. It works, but it could be a lot more elegant if I could use streams.

UPDATE:

I can now confirm that requestSubmit() successfully works with Turbo streams as well.

I’m interested in this also. However, I think it was established that requestSubmit isn’t supported in Safari

1 Like

There seems to be a polyfill for this @MrHubble which should do the trick for now.

Alternatively, you could use a GET response and dynamically update the src of the turbo-frame with a GET query. No polyfill needed.

import { Controller } from "stimulus"

export default class extends Controller {
  static targets = ['results'];
  static values = {url: String}

  findResults(event) {
    this.resultsTarget.src = this.urlValue.replace(/%s/g, encodeURIComponent(event.target.value))
  }
}
<div data-controller="search" data-search-url-value="books?q=%s">
  Search:
  <input type="search" data-action="search#findResults">
  <turbo-frame id="search-results" data-search-target="results"></turbo-frame>
</div>
6 Likes

I like this approach a lot. I realized you could use src in this manner a while after I wrote my original response, but I think this may be better. My only concern would be that it kind of gets into the guts of how frames work.

@jacobdaddario wrote

My only concern would be that it kind of gets into the guts of how frames work.

I’m not sure I understand this, do you think dynamically setting src on a turbo-frame is getting to the guts?

1 Like

Would you happen to know how to run a call back after the form submission response is received?

In my particular case, I have something like this. from within my controller, I want to highlight the results which matches the stimulus indexValue. I was thinking that this could easily be done with a callback after the form submission is made and the results are rendered. Any ideas on how this could actually be done?

<div data-controller="somecontroller" data-somecontroller-index-value="1"    />
  turbo_frame_tag "results"
    <form etc />
    <li> results etc. </li>
    <li> results etc. </li>
  end

@BKSpurgeon have you tried hooking a callback up to the turbo:submit-end event?

@dan thank you for your response.

The issue with turbo:submit-end is that I wish to act on the results after they are added to the dom. I was unable to get turbo:submit-end to do this?

<div data-controller="slideshow" data-slideshow-index-value="1"    />
  turbo_frame_tag "results"
    <form />  
    <li> results etc. </li>    
  end
  1. Form is submitted via javascript (i.e. within turbo_frame_tag)
  2. Controller is hit and results are returned.
  3. We want to perform some action on the results that are returned. i.e. a callback after the form submission and after everything’s rendered

I was unable to get turbo:submit-end to work on the rendered response of the form submission. Off the top of your head: is this the expected behaviour?

highlightCurrentSlide(event){  	
    this.slideTargets.forEach((element) => {         
     // the slideTargets get updated on the form submission
    //  the highlightCurrentSlide method seems to run before the
    // the results of the form submission are rendered.    
    }, this)
  }

Any advice would be much appreciated. chrs!

Hmm…turbo:submit-end only fires after your controller has responded. So I guess the issue here is the gap between getting that response and the HTML actually being put in the DOM. I wouldn’t expect that gap to be significant, though.

A couple of ideas (I haven’t tested any of them, mind):

The event exposes the FetchResponse via event.detail.fetchResponse (I think). FetchResponse has a responseHTML method that returns a promise. You could make your highlightCurrentSlide method async and on the first line await event.detail.fetchResponse.responseHTML().

Or (more of a long shot) I’d maybe try wrapping highlightCurrentSlide in a window.requestAnimationFrame callback.

Or, failing any of that, there’s probably a solution using MutationObserver and/or maybe giving those slideTargets their own controller (so you could just use connect).

1 Like

I think FrameElement.loaded could help you

In Stimulus controller:

// get frame    
const frame = document.getElementById("frame_id");
// submit form
this.formTarget.dispatchEvent(new CustomEvent('submit', { bubbles: true }));
// listen for frame rendering
frame.loaded.then(function (success) {
  // some actions after frame rendered
}, function (error) {});
3 Likes

That’s pretty cool! :clap:

I was getting Fetch API cannot load [url here] due to access control checks.

errors in Safari with the polyfill.

But this works in Safari and Brave.

This actually seemed to do the trick for me in Safari / Firefox / Chrome. Curious if this is the best approach?