Skip to content

Save element editor via a Turbo Stream#4069

Open
tvdeyen wants to merge 4 commits into
mainfrom
element-editor-turbo-stream
Open

Save element editor via a Turbo Stream#4069
tvdeyen wants to merge 4 commits into
mainfrom
element-editor-turbo-stream

Conversation

@tvdeyen

@tvdeyen tvdeyen commented Jul 16, 2026

Copy link
Copy Markdown
Member

What is this pull request for?

The element editor save form was the last admin form still driven by
@rails/ujs. It submitted with remote: true, the controller answered with
JSON, and the alchemy-element-editor custom element parsed the raw
XMLHttpRequest by hand to apply every effect of a save. This converts that
flow to Turbo so we can keep removing UJS from the admin.

The form already lives inside the alchemy_elements_window Turbo Frame, so
dropping remote: true is enough for Turbo to drive the submission. The
controller now renders a Turbo Stream for both the success and the 422
validation-failure case (verified against the installed turbo-rails: the
stream 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 on turbo:submit-end either the preview refresh (on
success) 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 form getter to its own direct-child form. It
previously 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 iframe load event rather than
the previewReady message. previewReady fires before the preview's images
load, 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

  • I have followed Pull Request guidelines
  • I have added a detailed description into each commit message
  • I have added tests to cover this change

@tvdeyen
tvdeyen requested a review from a team as a code owner July 16, 2026 20:06
@tvdeyen tvdeyen added this to the 8.4 milestone Jul 16, 2026
tvdeyen added 3 commits July 16, 2026 22:10
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
tvdeyen force-pushed the element-editor-turbo-stream branch from 00f2e58 to 27b507c Compare July 16, 2026 20:10
@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.10%. Comparing base (ce43b86) to head (27b507c).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Rebuilt assets after source or dependency changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants