Save element editor via a Turbo Stream#4069
Open
tvdeyen wants to merge 4 commits into
Open
Conversation
The element editor save form was the last admin form still driven by @rails/ujs, responding with JSON that the ElementEditor custom element parsed by hand from the raw XHR. Now that the form already lives inside a Turbo Frame, dropping remote: true lets Turbo drive the submission and the controller answer with a Turbo Stream for both the success and the 422 validation-failure case. Everything that is markup is server-rendered: the growl, the per-ingredient validation errors, the element (and ancestor) preview text quotes, and the publish button. Only the genuinely client-side effects remain in JavaScript, now hung off Turbo's submit lifecycle events instead of ajax:complete: the dirty-state reset on submit-start and, on submit-end, the preview refresh or the error box toggle depending on the outcome.
The form getter matched any descendant form, so a wrapper element without ingredients of its own (e.g. a gallery) returned a nested child's form and attached its save handlers there. Saving the child then ran onSaveElement twice, refreshing and re-scrolling the preview twice. Scoping the getter to the element's own direct-child form, like the body getter already does, keeps each editor bound only to its own form.
The refresh promise resolved on the previewReady message, which the preview posts before its images have loaded. At that point the page is not yet tall enough to scroll, so focusing the just-saved element was a no-op and the preview stayed at the top. Resolving the promise on the iframe load event instead waits until the layout is final, so the scroll lands and sticks. The spinner still stops early on previewReady.
tvdeyen
force-pushed
the
element-editor-turbo-stream
branch
from
July 16, 2026 20:10
00f2e58 to
27b507c
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4069 +/- ##
=======================================
Coverage 98.10% 98.10%
=======================================
Files 348 348
Lines 9079 9083 +4
=======================================
+ Hits 8907 8911 +4
Misses 172 172 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Rebuilt assets after source or dependency changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this pull request for?
The element editor save form was the last admin form still driven by
@rails/ujs. It submitted withremote: true, the controller answered withJSON, and the
alchemy-element-editorcustom element parsed the rawXMLHttpRequestby hand to apply every effect of a save. This converts thatflow to Turbo so we can keep removing UJS from the admin.
The form already lives inside the
alchemy_elements_windowTurbo Frame, sodropping
remote: trueis enough for Turbo to drive the submission. Thecontroller now renders a Turbo Stream for both the success and the
422validation-failure case (verified against the installed
turbo-rails: thestream renderer intercepts the response before the success/failure split, so a
422 stream renders fine).
Everything that is markup is server-rendered — the growl, the per-ingredient
validation errors, the element and its ancestor preview text quotes, and the
publish button. The only effects that stay in JavaScript are the ones that are
genuinely client-side state, and they now hang off Turbo's own submit lifecycle
events rather than a custom stream action: the dirty-state reset on
turbo:submit-start, and onturbo:submit-endeither the preview refresh (onsuccess) or the error box toggle (on failure).
Notable changes (remove if none)
Two follow-up fixes are included because they surfaced while verifying the save
in the running app:
Scoping the element editor
formgetter to its own direct-child form. Itpreviously matched any descendant form, so a wrapper element without ingredients
of its own (a gallery) returned a nested child's form and attached its save
handlers there, refreshing the preview twice on a nested save.
Resolving the preview
refresh()promise on the iframeloadevent rather thanthe
previewReadymessage.previewReadyfires before the preview's imagesload, so the page is not yet tall enough to scroll and focusing the saved
element was a no-op that left the preview at the top.
Checklist