Fix: preserve iframe preview when Livewire morphs the field - #52
Merged
Conversation
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.
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.
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:ignoreon 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 ownwire:ignoreis never consulted.It then stays blank because
mason.jscached the element once in a closure insideinit()and attached theloadlistener there.init()never runs again, so the replacement iframe gets no listener and no content, and everysendMessageToIframetargets a detached node.Changes
Three layers, in order of what does the work:
wire:ignoremoved 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.wire:keyadded 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".x-initon the element rather than being cached once ininit(). 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 oniframe.name), anddestroy()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
classalongside the one from the extra-input attribute bag, producing twoclassattributes on one element. HTML parsing keeps the first and discards the rest, so user classes fromextraInputAttributes()were being silently dropped there.mason-input-wrapperis dropped rather than merged — it belongs to the outer input wrapper, has never actually applied here, and adding it now would newly pull inoverflow-hiddenand the fullscreenh-full/rounded-nonerules. Rendered classes are unchanged.Verification
composer testgreen — 161 passed, Rector and Pint clean.npm run buildre-run; a fresh build produces no diff against the committedresources/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'sinit()running before children are initialized,x-loadgating 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 insidewire:ignore, so a reactiveextraInputAttributes()closure would stop re-rendering on that element. A dedicated inner<div wire:ignore>around just the iframe would avoid this, but.mason-editor-wrapperisrelative flex-1with the iframe atmin-h-full, so an extra DOM level breaks the percentage-height chain and thep-6mobile/tablet padding.Closes #38.
🤖 Generated with Claude Code