Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Events: Accessing the link or form element in dispatched events #99

Closed
pfeiffer opened this issue Jan 10, 2021 · 11 comments · Fixed by #695 · May be fixed by #750
Closed

Events: Accessing the link or form element in dispatched events #99

pfeiffer opened this issue Jan 10, 2021 · 11 comments · Fixed by #695 · May be fixed by #750

Comments

@pfeiffer
Copy link
Contributor

Most events fired by Turbo fire on document, irrespective of if the navigation was triggered programatically or through a link click or form submission. The event.details for these events does not contain a reference to the original interactive event (click / form) nor a reference to the original element in case a navigation was fired interactively.

I'd be very handy if these events (eg. turbo:before-fetch-request) would provide details with a reference to the link or form element that triggered the navigation. This would allow customisation based on the element's attributes, as well as updating the element during the lifecycle of the visit.

A few common examples that are very hard to achieve today:

  • Add a loading class to the link or form or adding a spinner while the turbo request is in-flight (listening to turbo:before-fetch-request)

  • Customize the fetch options based the element attributes, eg. <a href=".." data-prefer-modal> ..</a> could customize the Accept headers to allow the backend to return a specific variant.

  • Handle network and server-side errors by adding an error class to the form in case the network fetch failed

  • Customize cache behavior based on the action

A suggestion could be to provide originalEvent in event.details if a visit was triggered interactively, eg:

// visit triggered via click on a link
event.details.originalEvent // => the original event, with event.target pointing to the link

// visit triggered via Turbo.visit(...)
event.details.originalEvent // => null

It could be there are ways to do this today that I've missed. If not, what do you think? I'll be happy to wrap up a PR supporting this, as I believe it would be very useful for customizing the behavior when doing navigations.

@seanpdoyle
Copy link
Contributor

One way that's currently possible involves listening for turbo:click events fired on links that trigger turbo navigations, or turbo:submit-start and turbo:submit-end. Between those life cycles, you should be able to extract data- attributes and toggle state CSS classes based on the progress through the life cycles.

@pfeiffer
Copy link
Contributor Author

One way that's currently possible involves listening for turbo:click events fired on links that trigger turbo navigations, or turbo:submit-start and turbo:submit-end. Between those life cycles, you should be able to extract data- attributes and toggle state CSS classes based on the progress through the life cycles.

Well - not really.

While one can listen to turbo:click and turbo:submit-start to eg. add a loading class to the element, there's no guarantee that a fetch request is actually performed as other listeners can cancel the event independently leading to no request being made. It's also not possible to remove the class again after a completed visit.

Besides that, as the turbo:click and turbo:submit-start event fires before a visit is performed (and therefore cancelable), this means that there's no way of configuring the fetch request, eg. headers based on the element.

@pfeiffer
Copy link
Contributor Author

pfeiffer commented Jan 10, 2021

What I miss the most is the the ability to customize the fetchOptions using hints given as data attributes.

For example, a link could hint that it would accept a modal as content like so:

<a href="/boosts/new" class="btn" data-rel="modal">Create boost</a>
document.addEventListener("turbo:before-fetch-request", (event) => {
  const { details: { originalEvent, fetchOptions } } = event;

  if (originalEvent?.target && originalEvent.target.dataset.rel == "modal") {
    fetchOptions.headers["Accept"] = ...
  }
});
# app/views/boosts/new.html+modal.erb
<dialog>
  ... 
</dialog>

@MirazMac
Copy link

Hello, any updates regarding this? I badly need to access the form element on submission, for example for Google Recaptcha v3. To pause the submit request and wait for the token and then resume. But unfortunately the turbo:before-fetch-request event doesn't contain any reference to the target form element. And the turbo:submit-start event doesn't support pausing either or I didn't find anything in the docs regarding this.

@MirazMac
Copy link

MirazMac commented Sep 8, 2021

#367 I'm trying to listen for turbo:before-fetch-request in a form element, but unfortunately it's not picking up anything. However, if I bind the event listener to document it works but the event.target is the document itself.

@hhartsell
Copy link

I just encountered the same issue. Having a reference to the original element that was interactively clicked would be super helpful.

@dhh
Copy link
Member

dhh commented Jun 19, 2022

Would be happy to see a PR for originalEvent. Please do investigate.

@kendagriff
Copy link

I don't have a deep understanding of this, but it appears that access to the target that initiated the event was the original intent of Turbo:

export function dispatch(eventName: string, { target, cancelable, detail }: Partial<DispatchOptions> = {}) {
  const event = new CustomEvent(eventName, {
    cancelable,
    bubbles: true,
    detail,
  })

  if (target && (target as Element).isConnected) {
    target.dispatchEvent(event)
  } else {
    document.documentElement.dispatchEvent(event)
  }

  return event
}

See 3c726f7. A prerequisite being that the target was not removed from the DOM first.

I just confirmed in our own Turbo app that clicking a link, while listening for the event via document.addEventListener, did in fact yield the event.target of the link.

@domchristie
Copy link
Contributor

Would be happy to see a PR for originalEvent. Please do investigate.

@dhh in a similar vein to #367, would it be preferable to dispatch turbo:before-visit, turbo:visit etc on the initiating element? (rather than include the original event in detail) e.g.

const link = document.getElementbyId('link')
addEventListener('turbo:visit', (event) => event.target === link)
link.click()

@seanpdoyle
Copy link
Contributor

Would be happy to see a PR for originalEvent. Please do investigate.

@dhh in a similar vein to #367, would it be preferable to dispatch turbo:before-visit, turbo:visit etc on the initiating element? (rather than include the original event in detail)

So long as it continues to bubble up to the document.documentElement, I'm in favor of dispatching it closer to its source.

@domchristie
Copy link
Contributor

@seanpdoyle #695 should do this. The default dispatch sets bubbles: true, and my manual tests confirm that window.addEventListener('turbo:visit') / document.addEventListener('turbo:visit') still work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
7 participants