Skip to content

Fix: preserve iframe preview when Livewire morphs the field - #52

Merged
awcodes merged 2 commits into
3.xfrom
fix/preserve-iframe-on-morph
Aug 8, 2026
Merged

Fix: preserve iframe preview when Livewire morphs the field#52
awcodes merged 2 commits into
3.xfrom
fix/preserve-iframe-on-morph

Conversation

@awcodes

@awcodes awcodes commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Hardened take on #38 by @hempsworth, whose diagnosis was correct.

Problem

The preview iframe goes blank after a Livewire round trip — most visibly when a brick modal is cancelled. Only reproduces with a browser extension that injects into the DOM; Grammarly is the reported case, confirmed by two people in #38.

Cause

Livewire's morph only honors wire:ignore on an element it has matched and is about to update. Grammarly injects <grammarly-extension> as a sibling of the iframe inside .mason-editor-wrapper, so the DOM children are [grammarly-extension, iframe] while the server HTML has [iframe]. Position-based matching compares the injected node against the iframe, mismatches, and removes/re-inserts. The iframe is collateral damage of a sibling mismatch, so its own wire:ignore is never consulted.

It then stays blank because mason.js cached the element once in a closure inside init() and attached the load listener there. init() never runs again, so the replacement iframe gets no listener and no content, and every sendMessageToIframe targets a detached node.

Changes

Three layers, in order of what does the work:

  1. wire:ignore moved to .mason-editor-wrapper (this is Fix: Preserve iframe preview when cancelling brick edit modal #38's change) — the whole subtree, including any injected nodes, is skipped during morphs.
  2. wire:key added to that wrapper — morph matches it by key with lookahead rather than by position, covering the case where something injects a sibling of the wrapper itself. This closes the gap that made the original one-liner "not bulletproof".
  3. The iframe binds from x-init on the element rather than being cached once in init(). Livewire re-runs Alpine init on morph-added nodes, so a replaced iframe rebinds and repopulates itself instead of leaving a stale reference. This is what makes the class of bug go away rather than this one instance — even if some future injector defeats 1 and 2, the preview recovers.

updatePreview() now guards on a null iframe (previously it would have thrown on iframe.name), and destroy() clears the bind flag so an element outliving its component can be rebound.

A second commit fixes an unrelated bug surfaced while verifying this: the wrapper rendered a literal class alongside the one from the extra-input attribute bag, producing two class attributes on one element. HTML parsing keeps the first and discards the rest, so user classes from extraInputAttributes() were being silently dropped there. mason-input-wrapper is dropped rather than merged — it belongs to the outer input wrapper, has never actually applied here, and adding it now would newly pull in overflow-hidden and the fullscreen h-full/rounded-none rules. Rendered classes are unchanged.

Verification

  • composer test green — 161 passed, Rector and Pint clean.
  • Rendered the field's HTML on the base commit and on this branch and diffed with IDs normalized: no node added or removed, no nesting change, applied classes byte-identical. Initial render and layout are unaffected.
  • npm run build re-run; a fresh build produces no diff against the committed resources/dist/mason.js.

Not verified in a browser — this repo has no Alpine dependency or playground app, so the runtime path is reasoned from Alpine's directive semantics (depth-first walk, x-data's init() running before children are initialized, x-load gating the subtree) rather than executed. Worth a manual pass with Grammarly installed, following steps 3-6 of #38.

Tradeoff

The wrapper carries $getExtraInputAttributeBag() and is now inside wire:ignore, so a reactive extraInputAttributes() closure would stop re-rendering on that element. A dedicated inner <div wire:ignore> around just the iframe would avoid this, but .mason-editor-wrapper is relative flex-1 with the iframe at min-h-full, so an extra DOM level breaks the percentage-height chain and the p-6 mobile/tablet padding.

Closes #38.

🤖 Generated with Claude Code

awcodes added 2 commits August 8, 2026 14:30
Livewire's morph only honors wire:ignore on an element it has matched and
is updating. When a browser extension (Grammarly is the reported case)
injects a node as a sibling of the preview iframe, position-based child
matching mismatches and the iframe is removed and re-inserted, so its own
wire:ignore never gets consulted. The Alpine component then holds a stale
reference to the detached element and the new iframe is never populated,
leaving the preview blank after any Livewire round trip (e.g. cancelling a
brick modal).

- Move wire:ignore to .mason-editor-wrapper so the whole subtree, including
  any injected nodes, is skipped during morphs.
- Add wire:key to that wrapper so morph matches it by key rather than
  position, in case a node is injected as a sibling of the wrapper itself.
- Bind the iframe from x-init on the element rather than caching it once in
  init(), so a replaced element rebinds and repopulates instead of leaving a
  stale reference behind.

Based on #38 by @hempsworth.
The wrapper rendered a literal class="mason-editor-wrapper" alongside the
class the extra-input attribute bag emits, producing two class attributes on
one element. HTML parsing keeps the first and discards the rest, so anything
the attribute bag contributed there — including user classes from
extraInputAttributes() — was silently dropped.

Fold the class into the attribute bag so it merges. mason-input-wrapper is
dropped rather than merged: it belongs to the outer input wrapper, has never
actually applied here, and adding it now would newly pull in overflow-hidden
and the fullscreen h-full/rounded-none rules. Rendered classes are unchanged.
@awcodes
awcodes merged commit 6afbbaa into 3.x Aug 8, 2026
5 checks passed
@awcodes
awcodes deleted the fix/preserve-iframe-on-morph branch August 8, 2026 18:48
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.

1 participant