s-form save bar disappears when removing table rows – dirty state lost on DOM unmount (app-home UI extension)
Goal
I’m building a tabular JSON editor for a metaobject field – the user can add/remove rows and columns, edit cell values, and save the result as JSON back to the metafield. I need the <s-form> save bar to remain visible after a user removes a row or column from the table so they can save the deletion.
Environment
- App-home UI extension (non-iframe, Preact-based,
s-*web components) - Shopify CLI 3.x, API version 2026-04
- Components:
s-page,s-form,s-table,s-text-field,s-button
What happens
- Page loads a metaobject’s JSON metafield and renders it as a table. Each cell is an
<s-text-field>inside an<s-form onSubmit={...} onReset={...}>. - User edits a cell value – save bar appears correctly.
- User clicks “Remove row” – the row is removed from state, Preact unmounts the corresponding
<s-table-row>and its child<s-text-field>elements. - Result: Save bar disappears. The form no longer considers itself dirty because the tracked input nodes are gone from the DOM.
- Expected: Save bar stays visible – data has changed (a row was deleted) and needs saving.
What I’ve tried
- Keeping a permanent
<s-text-field>and toggling itsvalueprop – this does trigger the save bar, but<s-text-field>has nohiddenattribute/property, so it’s always visible to the user. - Programmatic
dispatchEvent(input/change) on<s-text-field>– did not trigger the save bar. - Native
<input type="hidden">inside the<s-form>– not rendered by the extension runtime (onlys-*components are rendered). - Soft-delete pattern (mark rows as deleted, hide with CSS) –
styleattributes and CSS do not appear to work ons-*web components in the extension sandbox. The elements remain visible.
What I’m asking
Is there a way to either:
- Visually hide an
s-*component while keeping it mounted in the DOM (so<s-form>retains dirty tracking)? - Programmatically mark an
<s-form>as dirty without a visible input?
I’m specifically looking for a solution within the app-home UI extension (non-iframe). I’m aware of the shopify.saveBar API but that requires the iframe-based extension surface.
Minimal reproduction
const removeRow = (index: number) => {
// This causes Preact to unmount the <s-text-field> nodes
// and <s-form> loses dirty state
setRows((prev) => prev.filter((_, i) => i !== index));
};
// Inside <s-form>:
{rows.map((row, rowIndex) => (
<s-table-row key={rowIndex}>
<s-table-cell>
<s-text-field
name={`row-${rowIndex}-col`}
value={row.value}
onInput={...}
/>
</s-table-cell>
<s-table-cell>
<s-button onClick={() => removeRow(rowIndex)}>Remove</s-button>
</s-table-cell>
</s-table-row>
))}