You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[6.x] Fire ElementCardPartsResolving for every element card - #20335
ElementCardHtmlResolving only fired from ElementHtml::elementCardHtml(). Cards rendered by the card component's slots (Matrix fields, relation fields, element indexes, CKEditor's nested entries) come from elementCardData(), which never fired it. So neither Craft 6 listeners nor Craft 5 Cp::EVENT_DEFINE_ELEMENT_CARD_HTML handlers (through the Yii adapter) could change those cards.
Both renderers now build the same ElementCardParts in one place and fire one parts-only event with them:
elementCardHtml() ─┐ ┌─ elementCardPartsHtml(parts)
├─ elementCardParts() → ElementCardParts ──┤
elementCardData() ─┘ fires ElementCardPartsResolving └─ card data array
(element, context, parts — no html)
Yii adapter (only when EVENT_DEFINE_ELEMENT_CARD_HTML has handlers):
parts → HTML → Craft 5 handlers → changed? → split back into parts
ElementCardHtmlResolving is renamed to ElementCardPartsResolving, with $element, $context and $parts, and no $html. Listeners edit $event->parts, and the change applies wherever the card is shown. There's one way to change a card, so listeners can't silently override each other.
Listeners get the same parts from both renderers. The selection checkbox is added when the card is rendered rather than stored in actionsHtml, and withThumb means the same thing in both.
ElementCardParts holds attributes, labelHtml, actionsHtml, contentHtml, footerHtml, thumbHtml and thumbAlignment.
The Yii adapter owns Craft 5 compatibility. It renders the parts for Cp::EVENT_DEFINE_ELEMENT_CARD_HTML handlers, and only when a handler changes the HTML does it split it back into parts. HTML that isn't a <craft-card> becomes the card's content, keeping the original attributes and footer unless the override already contains it. Markup outside the card root goes in the footer.
ElementCards.vue now passes cardAttributes.class through to <craft-card>, so classes listeners add show up on Matrix, relation and index cards.
Breaking:ElementCardHtmlResolving is renamed to ElementCardPartsResolving, and its $html property is replaced by $parts.
Notes
Craft 5 handlers' HTML is always split back into parts, including for elementCardHtml(). Splitting goes through Dom\HTMLDocument, so it can normalize markup (e.g. <circle/> becomes <circle></circle>). Attributes on internal wrappers such as .card-main aren't kept.
Cards rendered by the card component's slots, such as Matrix, relation
field, element index, and CKEditor cards, come from elementCardData(),
which never fired the event, so neither its listeners nor Craft 5
EVENT_DEFINE_ELEMENT_CARD_HTML handlers could change them.
Both card renderers now build an ElementCardParts and fire the same
event with it, so a listener that edits the parts changes the card
wherever it's shown. Setting the event's html still replaces the whole
card; for slot-rendered cards it's split back into parts. The Yii
adapter passes Craft 5 handlers the card's HTML, and only takes over the
card when a handler changed it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@riasvdv, @brandonkelly, any thoughts on this one?
I’m not 100% sure if it’s the best way to go, but we need the ElementCardHtmlResolving event firing for fields like Matrix (like it did on v5), and ideally we shouldn’t require devs to know if the card was rendered via ElementHtml::elementCardHtml() or ElementHtml::elementCardData()
…pter
ElementCardHtmlResolving is renamed to ElementCardPartsResolving and only
carries the card's parts. Both card renderers build the parts and fire the
event in one place, so listeners see the same parts everywhere.
The Yii adapter renders the parts for Craft 5 EVENT_DEFINE_ELEMENT_CARD_HTML
handlers and splits changed HTML back into parts, keeping the original footer
when the result isn't a card.
ElementCards.vue now passes server card classes through to <craft-card>.
Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3
Co-authored-by: Amp <amp@ampcode.com>
It keeps the main idea: both card renderers build the same parts and fire one event. What changes is where $html lives.
Core only has parts.ElementCardHtmlResolving is renamed to ElementCardPartsResolving, with $element, $context and $parts and no $html. That means there's only one way to change a card, so a listener that sets html can't silently override another listener's changes to parts. Craft 6 isn't out yet, so the rename only needs a changelog note.
The Yii adapter owns the HTML round-trip. It renders the parts for Cp::EVENT_DEFINE_ELEMENT_CARD_HTML handlers, and only when a handler changes the HTML does it split the result back into parts. The Dom\HTMLDocument parser moves out of ElementHtml into the legacy Cp helper. If a handler returns something that isn't a card, the original footer is kept, since that's where the field's hidden input is.
Listeners get the same parts from both renderers. The selection checkbox is added at render time instead of being stored in actionsHtml, and withThumb means the same thing in both renderers. The test now compares all of the parts, not just the header and content.
ElementCards.vue passes class through. Before this, classes in cardAttributes were dropped, so a listener adding a class had no effect on Matrix, relation or index cards. The Storybook render is pixel-identical before and after.
ElementCardParts is now a plain, non-final class without Arrayable, and headerHtml is renamed to labelHtml, since it holds what elementCardLabelHtml() returns.
One tradeoff: Craft 5 handlers' HTML is now always parsed back into parts, including for elementCardHtml(). So a handler that wraps the card in another element will end up with that wrapper as the card's content.
A handler that wraps the card already includes its footer, so the original
footer is only restored when the override lacks it. Markup a handler adds
next to the card root now goes in the footer instead of being dropped.
Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3
[6.x] Parts-only card event, with Craft 5 card HTML handled by the Yii adapter
brandonkelly
changed the title
[6.x] Fire ElementCardHtmlResolving for every element card
[6.x] Fire ElementCardPartsResolving for every element card
Nov 4, 2026
ElementCards passes server card classes through to <craft-card>, which put
the structural element class on the card as well as its <li>. Quick-edit
finds the row with closest('.element'), so it picked the non-focusable card
and couldn't restore focus after the editor closed. The element and error
classes now stay on the list item only.
Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3
Co-authored-by: Amp <amp@ampcode.com>
This PR unifies element-card customization across HTML and component-slot rendering through a shared parts event, with Craft 5 HTML compatibility handled by the Yii adapter.
Changes:
Replaces ElementCardHtmlResolving with ElementCardPartsResolving and shared card-part construction.
Converts legacy HTML overrides back into card parts.
Forwards server-provided card classes in Vue and adds regression coverage.
When a Craft 5 card HTML handler changed a card, the reparsed attributes
replaced the card's structured data group with flat data-* strings, so
Matrix lost data.editable and hid the card's Edit button. Only the
attributes a handler added, changed, or removed are now applied to the
original attributes.
ElementCards also reads server class lists through attrs(), so a PHP list
with gaps in its keys no longer renders as index classes.
Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3
Co-authored-by: Amp <amp@ampcode.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Craft 5 handler overrides can duplicate hidden inputs when markup is reserialized and discard buttons appended beside the existing card actions, causing duplicate submissions and lost controls.
No open issues 🎉
2 previously missed issues
In code that hasn't changed since last review
Match footer inputs structurally to prevent duplicate submissions
yii2-adapter/legacy/helpers/Cp.php:975
If a Craft 5 handler wraps the card and reserializes its hidden input, even just reordering attributes, this string comparison misses the existing input and appends the original footer again. A field such as myFieldName[] then submits the same element twice. Match existing footer inputs structurally in the parsed override rather than requiring identical HTML.
Preserve sibling nodes when unwrapping card actions
yii2-adapter/legacy/helpers/Cp.php:988
A Craft 5 handler that appends a button to .card-actions-container, beside the existing .card-actions div, loses that button here: querySelector() finds the existing div, so only its contents survive and its siblings are discarded. Previously the handler’s HTML was returned intact, so this also regresses elementCardHtml(). Please unwrap .card-actions while preserving the container’s other child nodes.
The original footer is now dropped when the override already has its inputs,
matched by name and value rather than identical HTML. Buttons a handler adds
beside .card-actions are kept when the actions are unwrapped.
Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3
Co-authored-by: Amp <amp@ampcode.com>
Re: Copilot's latest review: I agree with both findings, and both are fixed in 8dc2a68.
Matching footer inputs structurally. When an override isn't a card, the original footer is now dropped only if every input in it appears in the override with the same name and value. Re-serialized inputs, such as ones with their attributes reordered, no longer get the hidden input added a second time. The wrapper test now also re-serializes the input and checks there's exactly one myFieldName[].
Keeping siblings of the card actions..card-actions is unwrapped and its siblings in .card-actions-container are kept, so a button a handler adds next to it survives. There's a new adapter test for this.
Both tests fail without the fix. All 8 adapter card tests pass, and the adapter's PHPStan reports no errors.
A Craft 5 handler’s notice inserted alongside .card-content is moved into thumbHtml because every sibling is treated as a thumbnail. ElementCards.vue puts that fragment in the thumbnail slot, which CraftCard.render() hides on cover-image cards, so the notice disappears. Identify the actual thumbnail and retain other body markup in contentHtml.
Only the footer inputs an override already has are dropped now, so other
footer content survives. Within .card-main, only .thumb elements become the
thumbnail; other markup a handler adds beside the content stays in it.
Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3
Co-authored-by: Amp <amp@ampcode.com>
Re: the previously missed issue in Copilot's latest review ("Keep non-thumbnail card markup in contentHtml"): agreed, fixed in e01c07c. Within .card-main, only .thumb elements, which is what getThumbHtml() renders, now become the thumbnail. Other markup a handler adds before or after .card-content stays in contentHtml in the same position, so cover-image cards no longer hide it. A new adapter test covers this and fails without the fix.
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
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.
Description
ElementCardHtmlResolvingonly fired fromElementHtml::elementCardHtml(). Cards rendered by the card component's slots (Matrix fields, relation fields, element indexes, CKEditor's nested entries) come fromelementCardData(), which never fired it. So neither Craft 6 listeners nor Craft 5Cp::EVENT_DEFINE_ELEMENT_CARD_HTMLhandlers (through the Yii adapter) could change those cards.Both renderers now build the same
ElementCardPartsin one place and fire one parts-only event with them:ElementCardHtmlResolvingis renamed toElementCardPartsResolving, with$element,$contextand$parts, and no$html. Listeners edit$event->parts, and the change applies wherever the card is shown. There's one way to change a card, so listeners can't silently override each other.actionsHtml, andwithThumbmeans the same thing in both.ElementCardPartsholdsattributes,labelHtml,actionsHtml,contentHtml,footerHtml,thumbHtmlandthumbAlignment.Cp::EVENT_DEFINE_ELEMENT_CARD_HTMLhandlers, and only when a handler changes the HTML does it split it back into parts. HTML that isn't a<craft-card>becomes the card's content, keeping the original attributes and footer unless the override already contains it. Markup outside the card root goes in the footer.ElementCards.vuenow passescardAttributes.classthrough to<craft-card>, so classes listeners add show up on Matrix, relation and index cards.Breaking:
ElementCardHtmlResolvingis renamed toElementCardPartsResolving, and its$htmlproperty is replaced by$parts.Notes
elementCardHtml(). Splitting goes throughDom\HTMLDocument, so it can normalize markup (e.g.<circle/>becomes<circle></circle>). Attributes on internal wrappers such as.card-mainaren't kept.Related issues
🤖 Generated with Claude Code