Skip to content

[POS] Add s-pos-list and the posListTemplate compiler (2026-10) - #4690

Draft
ajanth-u wants to merge 1 commit into
2026-10-rcfrom
au/pos-list-template-2026-10
Draft

[POS] Add s-pos-list and the posListTemplate compiler (2026-10)#4690
ajanth-u wants to merge 1 commit into
2026-10-rcfrom
au/pos-list-template-2026-10

Conversation

@ajanth-u

Copy link
Copy Markdown
Collaborator

What

Adds <s-pos-list> for POS API version 2026-10 and the posListTemplate tagged template that compiles its row markup. This is the templated candidate for GSD #51755; the legacy-parity alternative is #4661 (unmodified, held as contingency). Exactly one of the two should merge.

Companion PRs: World #2061117 (ui-api-design contract), #2061112 → #2061124 → #2061132 (pos-mobile host stack), Shopify/pos-e2e-ui-extension (sandbox screen, follows), World shopify-dev docs (follows).

Design

rows is plain data ({id, templateFor, ...fields}) and itemTemplates is a compiled AST produced once in the extension bundle by posListTemplate:

const itemTemplates = posListTemplate`
  <s-pos-list-item templateId="section-header" type="text">
    <s-heading>{{title}}</s-heading>
  </s-pos-list-item>
  <s-pos-list-item templateId="product">
    <s-stack direction="inline">
      <s-image src="{{image.src}}" alt="{{name}}" />
      <s-text>{{name}}</s-text>
      {{#if onSale}}<s-badge tone="success">Sale</s-badge>{{/if}}
      <s-switch bind:checked="active" />
    </s-stack>
  </s-pos-list-item>
`;
<s-pos-list rows={rows} itemTemplates={itemTemplates} onRowClick={(e) => e.detail.item.id} />

POS hydrates one template copy per FlashList-mounted row, so no extension code runs while scrolling and no functions cross the bridge (no Shopify/extensibility change needed).

Grammar: {{path}} in text; {{path}} inside an attribute (always a string); bind:prop="path" (the only typed binding); {{#if path}}…{{/if}} (no nesting, no else). Rejected at compile time with posListTemplate:-prefixed errors: on* attributes, ${} interpolation of any kind, #each/other expressions, nested/unbalanced #if, #if inside an attribute, text directly under <s-pos-list-item>, non-item roots, nested s-pos-list-item/s-pos-list, missing/duplicate templateId, unknown type, any other root attribute.

Changes

  • src/surfaces/point-of-sale/pos-list-template.ts — the compiler (htm-based; htm becomes a direct dependency).
  • src/surfaces/point-of-sale.ts — exports posListTemplate plus POSListTemplateTag, POSListTemplates, POSListItemTemplate, POSListTemplateNode, POSListTemplateProp, POSListTemplateSegment, POSListRow, POSListRowClickEvent.
  • components/POSList.d.ts (generated from pos-mobile meta.ts via pnpm build-extension-components:publish 2026-10) and the POSList blocks merged into components.d.ts. Only POSList additions are included; the pre-existing drift between the generator output and this repo's components.d.ts/components-shared.d.ts is left alone.
  • components/targets/StandardComponents.ts| 'POSList'.
  • components/POSList/examples/{default,incremental-loading}.jsx.
  • docs/.../2026-10/{generated_docs_data_v2.json,targets.json} regenerated with build-docs.mjs 2026-10.
  • .changeset/pos-list-template-component.md (minor).

Verification

  • yarn build, yarn type-check green; build/esm/surfaces/point-of-sale.mjs exports posListTemplate.
  • loom test packages/ui-extensions/src/surfaces/point-of-sale/tests/pos-list-template — 16 tests: exact AST output for text placeholders, {{#if}} blocks, bind:, attribute segments, valueless attributes, type="text"; and every rejection above with its literal message.
  • eslint clean on the changed files.

`<s-pos-list>` renders a virtualized list whose rows are plain data
(`rows`) and whose markup comes from item templates (`itemTemplates`)
compiled in the extension bundle by the `posListTemplate` tagged template.
POS hydrates one template copy per mounted row, so lists of thousands of
rows stay responsive and no extension code runs while scrolling.

Templates are static `<s-pos-list-item templateId="…" type="button|text">`
roots. Inside them `{{path}}` interpolates a row field as text,
`bind:prop="path"` passes a row field to a component prop with its own
type, and `{{#if path}}…{{/if}}` renders its content when the field is
truthy. Event handlers, interpolated values, nested `#if`, `#each`, text
directly under the root, and any other `{{…}}` expression are rejected at
compile time with a `posListTemplate:` prefixed error.

The component types are generated from POS Mobile's `meta.ts` and merged
into `components.d.ts`; `POSList` joins `StandardComponents`; the POS docs
data for 2026-10 is regenerated. `htm` (already a transitive dependency of
several surfaces) becomes a direct dependency for the parser.

Assisted-By: devx/058a962b-6514-4c8e-a49e-37b8e478fe36
@ajanth-u

Copy link
Copy Markdown
Collaborator Author

/snapit

@shopify-github-actions-access

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @ajanth-u! Your snapshots have been published to npm.

Test the snapshots by updating your package.json with the newly published versions:

"@shopify/ui-extensions": "0.0.0-snapshot-20260912150820",
"@shopify/ui-extensions-tester": "0.0.0-snapshot-20260912150820"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant