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

Restoring GET-form state on restoration visits #894

Closed
tonywok opened this issue Mar 8, 2023 · 17 comments
Closed

Restoring GET-form state on restoration visits #894

tonywok opened this issue Mar 8, 2023 · 17 comments

Comments

@tonywok
Copy link

tonywok commented Mar 8, 2023

Hello there, thank you in advance (pun!) and for your work on this library.

I have a form in my application that uses a GET method. It is used for filtering, so I take advantage of turbo to advance the url via turbo-action and update a frame with the results using a targeted turbo_frame.

Upon clicking the back button, for the most part, this works as expected. The history pops and I see the previous url and the contents of the frame are the previous contents.

However, the form controls remain in the state at the time of form submission, as opposed to reverting back to the pre-submit state.

As I understand it, when the back button is pressed, a restoration visit occurs, which attempts to display the page from the turbo cache. I can confirm this is the area of my misunderstanding by disabling caching entirely via <meta name="turbo-cache-control" content="no-cache"> and observing the expected behavior. (In my case this was observing a radio button group revert to its previously checked value).

I see the documentation for turbo:before-cache. It seems that is designed to give you a chance to modify the DOM before a snapshot is taken. I tried to use this, but the trouble I ran into is that it also fires when doing regular turbo visits.

This all seems pretty intentional, so I'm pretty sure this isn't a bug, but I can't seem to find a way to get this to work as I'd expect without the no-cache hammer. If there's interest, I'd love to contribute back at least some documentation on how to approach this problem.

Happy to provide a minimal reproduction if that's helpful.


Update: I've made a repository that reproduces the observed behavior here

@night91
Copy link

night91 commented Mar 8, 2023

Hello.

What is the state of the form that didn't get restored?
Do you change the DOM using JavaScript before moving to the next page?

I got some similar issues before because JS changes to the next state before the snapshot. What I did was including the controls inside the frame, so for example the active tab is rendered with the rest. Although, sometimes you need to do JS changes that you need to play with using turbo:before-cache.

@tonywok
Copy link
Author

tonywok commented Mar 8, 2023

@night91 So I whittled down my production example down into a simple form that contains a single radio button group. No additional javascript besides a call to requestSubmit() to submit the form on radio button selection.

Consistent with what I was seeing before, the url updates, the content inside the frame updates, but upon clicking the back button in the browser, the radio button that triggered the submission remains checked. 🤔

It's as if the turbo page cache is being populated before the advance occurs. So then the restoration visit uses the page cached post form submission instead of pre form submission.

I'm trying to familiarize myself with the codebase to better understand the timing of these bits.

@night91
Copy link

night91 commented Mar 8, 2023

@tonywok the intended behavior I think is to populate the cache before the advance, so it takes a snapshot just before the advance. As the selected radio is checked before the advance, that is why the restoration visit have it checked in the cache.

I think the way is to use turbo:before-cache to reset the form, so the cache is taken with the correct radio selected.
You could also move the radio group inside the frame, intercept the selection, firing the requestSubmit() without checking the radio. The frame loaded need to come back with the radio checked.

I don't understand the issue you refer about: turbo:before-cache also fires when doing regular turbo visits.. Because in any application visit (unless using: <meta name="turbo-cache-control" content="no-cache">), it will fire turbo:before-cache event and take the snapshot.

@tonywok
Copy link
Author

tonywok commented Mar 8, 2023

@night91 Thanks so much for your commentary!

I think the way is to use turbo:before-cache to reset the form, so the cache is taken with the correct radio selected.

I saw some mention of form reset in the docs. To be clear, by reset you're referring to this, right? If I'm following, this would still leave us in a state where the params don't necessarily match the "form element's default values", right?

You could also move the radio group inside the frame, intercept the selection, firing the requestSubmit() without checking the radio. The frame loaded need to come back with the radio checked.

I will try this approach. Sounds very promising.

As for the comment about turbo:before-cache, I'll try out your suggestion first and see if I can rephrase w/ a deeper understanding 😅


Related, was discussing this with a friend and he pointed out how the page snapshot is taking special action on password and select inputs.

I've not confirmed this, but it is peculiar that I'm running into this behavior using radio buttons and check boxes 😅

@night91
Copy link

night91 commented Mar 8, 2023

@tonywok the reset is not necessary HTMLFormElement.reset(), that may work, but you can also perform some additional actions to actually reset the form, collapse some elements, hide a drop dawn, modal, etc. Disable cache is more for things as flash messages, sort like that, otherwise the restoration visit will show nothing there.

When I got a similar issue about tabs (links) loading frames, I solved it, moving the tabs inside the frame. The new active will come with the frame loaded. With radio buttons is tricky because you actually need to prevent the radio to be checked, otherwise the cache will be saved with the new state instead of the previous.

@tonywok
Copy link
Author

tonywok commented Mar 8, 2023

@night91 I'll try both approaches and report back -- thanks again (fwiw, I was going down the turbo:before-cache path, but was was finding it tricky where to stash the "previous" value 🤔)

Yeah, with a radio button, preventing it from being checked seems like it'd make it seem to the end-user that they didn't actually check it 😅

@night91
Copy link

night91 commented Mar 9, 2023

Yeah, with a radio button, preventing it from being checked seems like it'd make it seem to the end-user that they didn't actually check it 😅

Not really, because the frame will be rendered quickly with the radio checked.

@tonywok
Copy link
Author

tonywok commented Mar 9, 2023

@night91 So this was interesting. The select box behaves as I'd expect, but the radio does not.

Kapture 2023-03-08 at 19 52 59

Kapture 2023-03-08 at 19 52 09

It's like the radio button variant is one page snapshot behind 😅


You could also move the radio group inside the frame, intercept the selection, firing the requestSubmit() without checking the radio. The frame loaded need to come back with the radio checked.

So I've tried this, and I'm confused by what you mean by "without checking the radio". Wouldn't that result in not submitting the right value for the radio group 😅

@night91
Copy link

night91 commented Mar 9, 2023

@tonywok Reading the code they save the form inputs values before the snapshot. Imagine you have the form with inputs and the radio button group, you write something and change the radios, then you submit, the restoration visit show the form saved values. I think it should work the same for the select buy from what you show me, the select value is cached without the new value. I need to do some test later.

@tonywok
Copy link
Author

tonywok commented Mar 9, 2023

It seems text fields behave as radio buttons and checkboxes -- i.e being behind. I pinged the PR that introduced that clone behavior to see if they could chime in here :) Thanks all!

Happy to try my hand at the PR if we agree this needs fixing.

Kapture 2023-03-09 at 09 36 50

@saterus
Copy link

saterus commented Mar 9, 2023

It seems text fields behave as radio buttons and checkboxes -- i.e being behind.

That seems like a good way to phrase it. The restored input has the value that was set when the form was submitted, rather than when that page was actually rendered.

This seems like an issue of caching the live DOM properties for the inputs, instead of just the original HTML attributes of the tag. The user has changed the form field before submitting. This deliberately only updates the DOM property, not the tag attribute. When restoring the page, we really only want to restore the original form, rather than the form with the user's ephemeral property changes.

It looks like there's already special code for handling <select> and <input type="password"> elements.

clone() {
const clonedElement = this.element.cloneNode(true)
const selectElements = this.element.querySelectorAll("select")
const clonedSelectElements = clonedElement.querySelectorAll("select")
for (const [index, source] of selectElements.entries()) {
const clone = clonedSelectElements[index]
for (const option of clone.selectedOptions) option.selected = false
for (const option of source.selectedOptions) clone.options[option.index].selected = true
}
for (const clonedPasswordInput of clonedElement.querySelectorAll<HTMLInputElement>('input[type="password"]')) {
clonedPasswordInput.value = ""
}
return new PageSnapshot(clonedElement, this.headSnapshot)
}

The special behavior for <select> seems very close to what we'd want for all form elements. It's resetting all options to be unselected, but I think we even improve that.

  1. Clone the whole page
  2. Loop over all inputs
  3. Set the cloned node's value/checked/selected/etc property to match their original source tag's attribute.
  4. Cache the cloned node

@tleish
Copy link
Contributor

tleish commented Mar 9, 2023

From my understanding (and experience) turbo cache code already cached <input> and <textarea>. This specific code:

  • Adds support for also caching select (or multi-select).
  • Removes caching of the input passwords, which is a security concern.

All these combined mimic how some browsers (e.g. Chrome) implemented bfcache.

@tonywok - there are some possible solutions to work around the bfcaching of a specific form, but it first it would be helpful for you to share the code for the above gif examples?

Note: for more background behind turbo and bfcache, you may be interested in this thread: turbolinks/turbolinks#574

@tonywok
Copy link
Author

tonywok commented Mar 9, 2023

@tleish Thanks for the response. I'll push up an example repo over lunch.

@night91
Copy link

night91 commented Mar 9, 2023

I think saving the input values in the snapshot was the intention, in my tests the select also saves the selection. If you want to reset the form, you can do it in the event turbo:before-cache.

@tonywok You are getting "the behind" because you have checkbox 1 selected with value 1 visible. You select the checkbox 2, but the cache will save Checkbox 2 and value 1. When you go back, that is what you will see because that was what was saved.

I really don't know how to do it with the approach of the form, the click in the checkbox and updating the frame. Because you actually need to select the new checkbox to send the value.

@tonywok
Copy link
Author

tonywok commented Mar 9, 2023

@tleish I've made a vanilla minimal rails application that reproduces the behavior I was observing. It can be found here https://github.com/tonywok/turbo_form_restore

If you have time to run it and do, let me know if you run into any issue.

@tleish
Copy link
Contributor

tleish commented Mar 9, 2023

It appears there's a bug in turbo where select value cache is not restored in a restoration visits when inside of a turbo-frame. If you remove the turbo-frame, it restores the select from cache.

If you do not want bfcache applied to the form elements, you could create a separate controller (or the existing form controller) with the following:

connect() {
  this.element.reset();
}

This resets all form values to the value stored in the HTML, not the DOM.

@tonywok
Copy link
Author

tonywok commented Mar 11, 2023

Thanks for this, @tleish. That is exactly what I needed. Closing this -- thanks all!

@tonywok tonywok closed this as completed Mar 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

4 participants