Re-attach DOM event listeners in the body of a turbo-frame

I have some buttons with click handlers that seem to only work on the very first page load. After the turbo-frame they’re a part of is replaced, the click handlers are no longer registered and I have inferred that I need to re-attach them.

I’ve tried to re-attach event listeners after turbo:load, turbo:render, and even turbo:submit-end all to no avail. It seems like the DOM element is indeed found but for some reason none of the event listeners attach. This is the case only after the turbo-frame has been replaced. And it only seems to happen when the visit is triggered by a form submission and redirect - normal links don’t seem to have this problem. Very strange! TIA for any help or advice.

EDIT: I have also noticed that the data-turbo="false" tag seems to be ignored entirely. I actually can’t get Turbo to ignore the form submission - no UJS is present, local: false, and I even added the <meta name="turbo-visit-control" content="reload"> tag and Turbo insists on ignoring all of these. Feels related.

You should definitely consider re-writing those handlers to respond to deferred events. In jQuery, this would take the form of:

$(document).on('click', '.some.selector#here', function(evt){
  // do something here

You can also write this sort of thing in “vanilla” JS, it just takes a few more lines of code. The idea is that the click bubbles up to the document, which then re-attaches it on the fly to the'.some.selector#here');.


1 Like

You have single handedly saved me hours of work. Thank you so much.

Hey @walterdavis do you happen to have a link handy on how to convert that code sample into vanilla JS? I can’t seem to find a good example in the addEventListener documentation.

Sure. This is ripped from a “use the entire table row as if it was a form label” where I clicked on a radio button if the click landed anywhere on the same row.

// polyfill for some browsers that don't do this natively
var matches;
(function(doc) {
  matches = 
  doc.matchesSelector ||
  doc.webkitMatchesSelector ||
  doc.mozMatchesSelector ||
  doc.oMatchesSelector ||

// set a delegated handler on a click, only respond if it's on a particular kind of tr
document.addEventListener('click', function(evt) {
  // ignore highlight swipes
  if ( window.getSelection().toString().length > 0 ) return true
  if (, '.row-as-label td' ) ) {
    const tgt ='tr').querySelector('td input');
    if( != tgt);
}, false);

Let me know if this helps.


1 Like

Thanks so much. Owe you.

1 Like

Glad you found a solution. StimulusJS controllers are also great at managing event handlers on elements that are updated after the initial page load. It’s literally what Stimulus was made for!

1 Like