# `orderly-product-grid`

`orderly-product-grid` owns product search state. It accepts a `SearchQuery`, calls `SearchService.Search`, renders products, supports sorting, and uses continuation tokens for manual or automatic paging.

`orderly-product-rail` and `orderly-collection-page` delegate product fetching to this component.

## Basic Markup

```html
<orderly-product-grid
  base-url="https://service.orderly.shop"
  paging="dynamic"
  keywords="sneakers | støvler | sandaler"
  tags="sko"
  order-by="CreatedTime desc">
</orderly-product-grid>
```

See [Search Syntax](../search-syntax.md) before writing category or rail queries. `keywords` maps to `SearchQuery.Query`; whitespace narrows the backend query and `|` is the OR operator. Multiple `tags` narrow results AND-style, so use tags only for shared filters and use `|` query text for alternatives.

## Typed Query

```ts
import { create } from "@bufbuild/protobuf";
import { SearchQuerySchema } from "@orderlyshop/core-client";

grid.query = create(SearchQuerySchema, {
  Query: "sneakers | støvler | sandaler",
  OrderBy: ["CreatedTime desc"]
});
```

## Default Query Scope

Use `defaultQuery` for shop-wide or component-local constraints:

```ts
grid.defaultQuery = defaultQuery;
```

Global default query:

```ts
configureShop({ defaultShop: { defaultQuery } });
```

The final request is:

1. Global default query.
2. Component `defaultQuery`, when supplied.
3. Grid `query` or declarative query markup.
4. Continuation token, when loading the next page.

## Attributes

- `base-url`: Backend URL used when no `client` property is assigned.
- `paging`: `button`, `manual`, `dynamic`, or `infinite`.
- `sort-label`: Sort control label.
- `clear-search-label`: Clear-search button label for custom context templates.
- `error-label`: Error text.
- `loading-label`: Loading text.
- `results-label`: Result count suffix for custom context templates.
- `searching-label`: Active query prefix for custom context templates.
- `keywords`: Maps to `SearchQuery.Query`.
- `tags`: Comma-separated values mapped to `SearchQuery.Tags`.
- `hidden-query`: Maps to `SearchQuery.HiddenQuery`.
- `order-by`: Comma-separated values mapped to `SearchQuery.OrderBy`.
- `store-id`: Maps to `SearchQuery.StoreId`.
- `featured`: Boolean mapped to `SearchQuery.Featured`.

## Properties

- `client: OrderlyClient | undefined`: Explicit client instance.
- `query: SearchQuery | undefined`: Explicit search query. Takes precedence over declarative query markup.
- `defaultQuery: SearchQuery | undefined`: Component-level default query.
- `shopQuery: SearchQuery | undefined`: Backward-compatible alias for `defaultQuery`.
- `sortOptions: SortOption[]`: Sort options rendered by the grid.
- `basketController: BasketController | undefined`: Shared basket state.
- `paging: "manual" | "button" | "infinite" | "dynamic"`: Paging behavior.
- `products: SearchObject[]`: Current products.
- `continuationToken: ContinuationToken | undefined`: Current continuation token.
- `hasMore: boolean`: Whether more results can be loaded.
- `state: ProductGridState`: Loading/error/result snapshot.
- `productTileTagName: string`: Product tile tag name for custom prefixes.

## Methods

- `refresh(): Promise<void>`: Loads the first live page. If SSR fallback products are present, they remain visible while the live request is pending.
- `loadNextPage(): Promise<void>`: Loads the next page when a continuation token is available.
- `setSort(orderBy: string[]): Promise<void>`: Applies sort and refreshes.
- `clearSearch(): Promise<void>`: Clears `SearchQuery.Query` and refreshes. The default layout does not render a clear-search button; custom context templates can call this with `data-orderly-action="clear-search"`.

## Paging

- `paging="dynamic"` and `paging="infinite"` use an intersection observer sentinel and do not render a load-more button.
- `paging="button"` renders a load-more control when a continuation token exists.
- `paging="manual"` never renders a button; call `loadNextPage()` yourself.

## SSR Fallback Products

When the grid contains semantic fallback products from static hydration or server-side rendering, it reads them into the initial visible state and hides the raw fallback markup. Those products are only a visual fallback: when a `client`, `base-url`, or package-owned parent client is available, the grid still performs the first live `SearchService.Search()` after hydration. The live response replaces the fallback products and owns the continuation token used by `loadNextPage()`.

If no live search target is configured, the fallback products remain visible so static markup still renders.

## Sort Options

```ts
grid.sortOptions = [
  { label: "Nyeste", orderBy: ["CreatedTime desc"] },
  { label: "Pris: lav til høj", orderBy: ["Price asc"] },
  { label: "Pris: høj til lav", orderBy: ["Price desc"] }
];
```

## Templates

### `layout`

Replaces the grid structure. Include:

- `data-orderly-slot="context"`
- `data-orderly-slot="sort"`
- `data-orderly-bind="status"`
- `data-orderly-slot="items"`
- `data-orderly-action="load-more"` if button paging is used.
- `data-orderly-sentinel` if dynamic paging is used.

```html
<template data-orderly-template="layout">
  <section class="catalog">
    <div data-orderly-slot="context"></div>
    <div data-orderly-slot="sort"></div>
    <p data-orderly-bind="status"></p>
    <ol data-orderly-slot="items"></ol>
    <button type="button" data-orderly-action="load-more">Flere varer</button>
    <span data-orderly-sentinel></span>
  </section>
</template>
```

### `product`

Replaces each product item. The grid binds the `SearchObject` into an inner `orderly-product-tile` when present.

```html
<template data-orderly-template="product">
  <li class="catalog-item">
    <orderly-product-tile></orderly-product-tile>
  </li>
</template>
```

### Other Templates

- `context`
- `sort`
- `loading`
- `empty`

## Events

- `orderly-results`: Fired after successful search. Detail is `ProductGridState`.
- `orderly-error`: Fired when search fails.
- Product tile events bubble through the grid:
  - `orderly-product-selected`
  - `orderly-add-to-basket`
  - `orderly-remove-from-basket`

## Styling

Default class names:

- `orderly-product-grid`
- `orderly-product-grid__toolbar`
- `orderly-product-grid__context`
- `orderly-product-grid__sort`
- `orderly-product-grid__status`
- `orderly-product-grid__items`
- `orderly-product-grid__item`
- `orderly-product-grid__load-more`
- `orderly-product-grid__sentinel`
- `orderly-product-grid__loading-item`

Useful CSS variables:

- `--orderly-product-grid-gap`
- `--orderly-product-grid-item-min-width`

The default grid uses responsive full-width `auto-fit` tracks, spaces cards with `--orderly-product-grid-gap`, and lets each product tile stretch to fill its resolved column width. That keeps rows visually flush at awkward intermediate breakpoints instead of leaving extra internal padding inside each track. Host applications can override the variables, style the default classes directly, or replace templates entirely.
