docs: make the inline SVG icon example idempotent - #820
Conversation
The example added in #794 prepended an <svg> unconditionally: icon: function (opt, $itemElement) { $itemElement.prepend('<svg class="context-menu-icon" ...>...</svg>'); return 'context-menu-icon-inline'; } A callback `icon` is invoked twice before the menu is ever visible - once from op.create() and again from the op.update() that op.show() runs on every show - and once more on every subsequent open or update. The prepend has no guard, so the item already carries two icons on its first display and gains another with every open. The returned 'context-menu-icon-inline' class reads as a done-marker, but nothing ever checked it. Guard on the injected element instead, which is what makes the callback safe to re-run. Two related doc fixes: * The example put `context-menu-icon` on the child <svg>. That is the plugin's own item-level class (classNames.icon), which carries an absolutely positioned ::before from the base-context-menu-icon mixin. Use a class of your own instead. * items.md never said the callback re-runs, which is what allowed the example to be written this way. Say so, and point at the guard pattern. The canonical example there needs no guard because $itemElement.html() replaces content rather than adding to it. Reported by Codex review on #794, after the PR had already been merged.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 664c295161
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| The callback is invoked every time the menu is shown or updated, not only when | ||
| it is first built, so that the icon can reflect current state. Write it to be | ||
| idempotent: replacing the item's content (as in the example below) is safe, |
There was a problem hiding this comment.
Qualify dynamic icon-class state handling
When an icon callback returns a different class string as state changes, the icon does not reliably reflect only the current state: op.update() removes item._icon, which is the creation-time result, but never assigns the new iconResult back to item._icon, so classes returned by successive opens accumulate on the item. This new guidance therefore encourages a broken dynamic-class pattern unless the callback reuses one class or manages stale classes itself; document that limitation or update the implementation to replace the previous result.
Useful? React with 👍 / 👎.
Addresses the Codex review point on the docs change in this PR: the new
guidance said a callback `icon` re-runs "so that the icon can reflect current
state", but a callback whose class tracks that state did not actually work.
op.update() removed `item._icon` before re-invoking the callback, then applied
the result without ever storing it. `item._icon` therefore stayed at whatever
op.create() had produced, so only that very first class was ever removed and
every class returned after it stayed on the item:
create -> _icon='state-one', item has state-one
update -> removeClass('state-one'), addClass('state-two') _icon unchanged
update -> removeClass('state-one') no-op, addClass('state-three')
item now carries state-two AND state-three
Assign the result back to `item._icon` so the next update removes the class the
previous one applied.
Rather than documenting the limitation, which would enshrine it, this makes the
documented contract true. The alternative was to soften the docs and leave a
callback `icon` unable to express changing state at all, which is the whole
reason it is re-evaluated.
Backwards compatibility: a callback returning a constant string - the common
case by far - is unaffected, since removing and re-adding the same class is a
no-op either way. Only a callback returning *different* strings changes, and
only by dropping stale classes, which is the bug. A callback returning an
element rather than a string is unaffected: `removeClass()` ignores a non-string
argument, exactly as it did before. `item._icon` is internal and read only in
op.create() and op.update().
Also documents that a returned class is swapped on each call, while anything the
callback does to $itemElement itself is not, so only that part needs to be
idempotent.
|
Thanks, this one was right and it landed on my own change rather than just the pre-existing code, so I've fixed it rather than documented it. The guidance I added said a callback Reproduced it first ( The test failed with Fixed rather than documented. Writing down "manage stale classes yourself" would enshrine the wart, and the alternative was to soften the docs and leave a callback On backwards compatibility, since that is the bar here:
Unit 139/139 and acceptance 38/38 green locally, and the docs now state that a returned class is swapped on each call while anything the callback does to Two things found while tracing this, both left out as out of scope for this PR:
|
Addresses the Codex review point on #794 that was never picked up before that PR merged: #794 (review)
Problem
The example added in #794 prepends an
<svg>unconditionally:A callback
iconis invoked twice before the menu is ever visible and once more on every open after that:src/jquery.contextMenu.js:1955op.create()calls the callback, then prependssrc/jquery.contextMenu.js:1493op.show()callsop.update()on every showsrc/jquery.contextMenu.js:2195op.update()calls the callback againSo the item carries two icons on its very first display, and gains another with every subsequent open. The returned
context-menu-icon-inlineclass reads as a done-marker, but nothing ever checked it.Fix
Guard on the injected element, which is what makes the callback safe to re-run:
Two related doc fixes came out of it:
context-menu-iconon the child<svg>. That is the plugin's own item-level class (classNames.icon), which carries an absolutely positioned::beforevia thebase-context-menu-iconmixin. Changed to a class of the reader's own.items.mdnever said the callback re-runs, which is what allowed the example to be written this way in the first place. It says so now and points at the guard pattern. The canonical example there needs no guard because$itemElement.html()replaces content rather than adding to it.Why docs-only, and no plugin change
The re-invocation is deliberate and documented behaviour: it is what lets a callback
iconreflect current state, the same way function-baseddisabled/visible/namedo. The contract is "your callback may run repeatedly", and the canonicalitems.mdexample already honours it via.html(). #794 simply broke that contract.Making the plugin de-duplicate prepended content would be a behaviour change affecting an undocumented return shape (returning an element rather than a class string), for no benefit over fixing the example. This is a bad example, not a bad plugin.
Verification
npm run docs:buildis clean, and the new cross-reference resolves:items.htmlemitshref="customize#using-your-own-svg-icons-without-a-build-step"andcustomize.htmlcarriesid="using-your-own-svg-icons-without-a-build-step".No source or test changes, so nothing in the matrix is affected.