feat(text-editor): add clear method to clear content imperatively#4144
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughA new imperative ChangesText editor clear API
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Documentation has been published to https://lundalogik.github.io/lime-elements/versions/PR-4144/ |
bdb0591 to
d23be29
Compare
d23be29 to
df4fe99
Compare
civing
left a comment
There was a problem hiding this comment.
🤖 AI-assisted review — generated with Claude (Claude Code) and shared by @perfryking. Findings are suggestions to verify, not an automated gate.
Nice, focused PR — the clear() framing (over a general setValue) is exactly right, and keeping the debounce untouched while adding an imperative escape hatch is the clean way to solve this. I read the implementation against updateView/watchValue/handleTransaction and verified the two things I'd normally worry about are already handled: updateView('') re-syncs lastEmittedValue to empty (so a later identical edit isn't wrongly deduped) and resets suppressChangeEvent. A few suggestions below, none of them blockers.
Should Fix
1. Add a test that pins the actual differentiator. The three new e2e tests prove clear() empties a diverged document, but none show that the value-prop path can't — which is the whole reason this method exists. Without it, a future change to the prop path could pass all these tests while the real collapse silently regresses. Starting from value: '' is what makes the reassignment a genuine no-op:
test('clear empties content when re-assigning value="" cannot', async () => {
const { root, waitForChanges, setProps, editor, typeText } =
await createEditor({ value: '' });
typeText('hello');
await vi.waitFor(() => expect(editor.textContent).toBe('hello'));
// '' -> '' is skipped by change detection, so @Watch never fires
// and the prop path cannot clear — the gap clear() exists for.
await setProps({ value: '' });
await waitForChanges();
expect(editor.textContent).toBe('hello'); // prop path did NOT clear
await root.clear();
await vi.waitFor(() => expect(editor.textContent).toBe(''));
});2. Consider a try/finally around suppressChangeEvent in updateView. It's pre-existing, but clear() adds a new awaitable, consumer-facing path into updateView, so it's now reachable: if parseAsHTML rejects, suppressChangeEvent stays true and silently kills all future change events for that instance. A finally { this.suppressChangeEvent = false; } closes that off cheaply.
3. Document the no-emit consequence. Agreed that clear() shouldn't emit — that matches how programmatic value assignment behaves on native inputs and on controlled components generally. But the JSDoc states the fact without the implication. One line makes it a contract instead of a surprise: "Does not emit a change event. If you mirror content on change (drafts, validation, dirty state), reset your own copy when calling clear()." Same for the silent no-op in readonly mode (no adapter rendered).
Minor
- The emptiness check serializes the whole doc just for a boolean (
currentContent === ''). The converters already expose the canonical emptiness predicate (textContent.trim() === '' && !hasImageNode && !hasCustomElementNode) — cheaper on large docs, same result. The check is correct as-is, just wasteful. - The spec
clear › it resolvesasserts almost nothing — in the spec envthis.viewis falsy so it hits the early return, proving only that an async fn resolves. The readonly variant is the meaningful one; consider dropping the first or swapping it for an "already empty → no-op, no change emitted" case. - Probably follow-up, not this PR: if a
valuechange interleaves duringawait clear()'supdateView, twoupdateViewcalls can race onsuppressChangeEvent/lastEmittedValue. Pre-existing and needs a specific interleaving; thetry/finallyabove narrows it.
Backward-compat
Confirmed purely additive — @beta, no collision with HTMLElement/FormComponent, and the api.md regen is correct. Don't add a release tag to clear (that'd break the flushPendingChanges precedent and trip api-extractor).
Assigning an empty `value` prop only clears the editor when the value changes. Because the editor debounces its `change` event, a consumer's bound value can lag the live document, so assigning `''` when the prop was already `''` is skipped by change detection and leaves the content untouched (for example, clearing the editor immediately after a send). clear empties the editor regardless of the prop's change detection, discarding any pending debounced change. It does not emit a `change` event.
updateView set suppressChangeEvent to true, awaited parseAsHTML, and only reset it once all the work below had finished. If parseAsHTML rejected (or anything in between threw), the flag stayed true and every subsequent change event was silently suppressed for that editor instance. Reset the flag in a finally block so a failed update can no longer wedge the editor into a state where it never emits change again.
df4fe99 to
0f21317
Compare
|
@civing Thanks, genuinely useful pass. Done in 0b80ee3 (folded into the clear commit) and 0f21317. Should Fix
Minor (declining, with reasoning)
Backward-compat: confirmed, no release tag added to |
|
🎉 This PR is included in version 39.36.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
|
Some follow-up thoughts on The core issue is that the editor pretends to be controlled but isn't. It implements Most of the fiddly state in the adapter is there to paper over that: Two things about
The alternative is a synchronous The two events divide the work. Bind Two changes, which have to land together:
The send-box case then works straight through the prop: <limel-text-editor value={this.draft} onInput={(e) => (this.draft = e.detail)} />
// on send:
this.converseRequest.emit(this.draft); // already current, no flushPendingChanges()
this.draft = ''; // "hello" -> "" fires the watch and clears, no clear()
None of this is urgent, but the direction matters: @john-traas you mentioned you wanted to do some improvements to the text editor if I understood you correctly. Is some of this part of that, or did you have other changes in mind? |
|
@jgroth Nice analysis! Completely agree that it would be better long term to fix the core issue with the text-editor masquerading as a controlled component |
|
@jgroth yes, it is one major area where we need significant improvement. My first goal is to improve test coverage to ensure that we don't break any current functionality while doing any re-writes. |
@john-traas Nice to hear! You might be interested in some of the recent testing work I've been doing in lime-elements:
The first two are mostly just groundwork, but the last of those three is a first shot at writing tests that run the real components, in a real web page served by a real web server, in a real browser, actually clicking around and opening menus and stuff, and not just asserting that classes got applied or removed and attributes updated, but actually taking screenshots and comparing to a baseline from |
Problem
limel-text-editoris a controlled component (it has avalueprop), but assigningvalueonly updates the editor when Stencil detects the prop changed. Because the editor debounces itschangeevent, a consumer's bound value can legitimately lag the live document. That produces a case the prop can't express: clearing the editor by assigning''when the previously rendered prop was already''.The clearest example is a chat-style "send box":
''to clear.''. Since the previously rendered value was already'', the prop goes'' → ''— no change — so the editor never re-renders,@Watch('value')never fires, and the document keeps the sent text on screen.The editor receives no signal at all in this case, so it cannot be fixed reactively from inside the component.
flushPendingChanges()(added in #4128) solved the symmetric read problem for this same use case; this adds the write counterpart.Change
Add an imperative
clear()method tolimel-text-editor(and its ProseMirror adapter) that empties the editor's document regardless of the prop's change detection:changeso it can't fire afterward and resurrect old content;changeevent (the caller cleared it, so no echo / re-entrancy).Consumers that previously needed timing workarounds to clear (e.g. deferring by a frame) can call
await editor.clear()instead.Backwards compatible (additive method only).
Tests
clearresolves, including readonly mode where no adapter is rendered.clearempties content thevalueprop never caught up to; discards the pending change so no stalechangeis emitted; does not emit achangeevent.Summary by CodeRabbit
New Features
clear()method to immediately remove all editor content, including when updates are pending/debounced.clear()is designed to avoid emitting the editor’schangeevent.Bug Fixes
Tests
clear()in default andreadonlymodes, including discarding pending typed input and preventing stale change payloads.