Consuming Query Fields
A block declares a query-type schema field; Frontend Core resolves it through Orchestr and hands the result to the component as a prop. That prop carries more than just the entities to render: it also exposes the available filters and sortings, the user's current selection, and the URL identity needed to navigate when something changes. This page covers reading that interactive state and updating it.
For declaring the schema field itself (type: 'query', singleEntity, components, links), see Schema fields → query. For how the same shapes are produced server-side, see Queries and Filters.
What's on the resolved prop
A query field (without singleEntity) resolves to a ClientEntitySet. The display side (entities, nested links) is documented under schema fields; the parts relevant for interactive UI are:
key is what comes back in sorting when selected.availableFilters ids mapped to selected values.current (1-based page), offset (0-based entity offset), limit, total, pages, next, previous.buildQueryUrl and they are used implicitly.Reading current state
Treat the prop as undefined-tolerant: it is undefined while the query is loading or has errored. Wrap reads in computeds so they recompute when the URL changes:
<script setup lang="ts">
import type { ClientEntitySet } from '@laioutr-core/orchestr/types';
import { computed } from 'vue';
const { products } = defineProps<{ products: ClientEntitySet | undefined }>();
// ---cut---
const filters = computed(() => products?.availableFilters ?? []);
const sortings = computed(() => products?.availableSortings ?? []);
const activeFilters = computed(() => products?.filter ?? {});
const activeSorting = computed(() => products?.sorting);
const total = computed(() => products?.pagination.total ?? 0);
const currentPage = computed(() => products?.pagination.current ?? 1);
</script>
activeFilters and activeSorting reflect what the user already picked; use them to render selected state in the UI (highlighted chips, checked checkboxes, the current option in a sort dropdown).
Updating state
State changes go through the URL. The flow is always: build a new URL with buildQueryUrl, then navigate with the router. The router push triggers a re-fetch with the new params, and the prop re-resolves with the new state.
Pass the entity set itself to buildQueryUrl; it carries the URL identity, so you do not construct a QueryUrlIdentity manually:
<script setup lang="ts">
import type { ClientEntitySet, QueryWireRequestFilter } from '@laioutr-core/orchestr/types';
import { buildQueryUrl, useRouter } from '#imports';
const { products } = defineProps<{ products: ClientEntitySet | undefined }>();
// ---cut---
const router = useRouter();
const onSortingChange = (key: string) => {
if (products) router.push(buildQueryUrl(products, { sort: key }));
};
const onAddFilter = (id: string, values: string[]) => {
if (products) router.push(buildQueryUrl(products, { addFilter: { [id]: values } }));
};
const onRemoveFilter = (id: string) => {
if (products) router.push(buildQueryUrl(products, { removeFilter: id }));
};
const onResetFilters = () => {
if (products) router.push(buildQueryUrl(products, { resetFilters: true }));
};
// Pagination typically uses an href builder so links render as proper <a> tags
const pageHref = (page: number) => (products ? buildQueryUrl(products, { page }) : '/');
</script>
Sort, limit, and filter changes automatically reset the page to 1; you do not need to pass page: 1 explicitly. For the full modifier reference (range filters, partial filter replacement, page-reset opt-out), see URL Query Parameters → Building URLs.
router.push. The Laioutr Pagination component takes an href-template prop for this:<Pagination
v-if="products"
:total="products.pagination.total"
:items-per-page="products.pagination.limit"
:page="products.pagination.current"
:href-template="({ page }) => (products ? buildQueryUrl(products, { page }) : '/')"
/>
Worked example
A minimal filter-and-sort block that reads its state and dispatches changes through the router:
<script lang="ts">
import { defineBlock } from '#imports';
export const definition = defineBlock({
component: 'BlockProductFiltersDemo',
schema: [
{
label: 'Content',
fields: [
{ type: 'query', entityType: 'Product', name: 'products', components: [] },
],
},
],
});
</script>
<script setup lang="ts">
import { buildQueryUrl, computed, definitionToProps, useRouter } from '#imports';
import type { QueryWireRequestFilter } from '@laioutr-core/orchestr/types';
const props = defineProps(definitionToProps(definition));
const router = useRouter();
const sortings = computed(() => props.products?.availableSortings ?? []);
const filters = computed(() => props.products?.availableFilters ?? []);
const activeFilters = computed(() => props.products?.filter ?? {});
const activeSorting = computed(() => props.products?.sorting);
const total = computed(() => props.products?.pagination.total ?? 0);
const onSortingChange = (sort: string) => {
if (props.products) router.push(buildQueryUrl(props.products, { sort }));
};
const onFiltersChange = (next: QueryWireRequestFilter) => {
if (props.products) router.push(buildQueryUrl(props.products, { resetFilters: true, addFilter: next }));
};
</script>
<template>
<div v-if="props.products">
<p>{{ total }} results</p>
<select :value="activeSorting" @change="(e) => onSortingChange((e.target as HTMLSelectElement).value)">
<option v-for="sort in sortings" :key="sort.key" :value="sort.key">{{ sort.label }}</option>
</select>
<FilterBar
:filters="filters"
:active="activeFilters"
@update:active="onFiltersChange"
/>
</div>
</template>
The Laioutr UI library ships ready-made FilterBar and SortModes components that consume these shapes directly, so most blocks do not implement the inputs themselves.
Related
Consent Adapters
How to build a Laioutr app that integrates a Consent Management Platform (CMP) by implementing the ConsentAdapter contract from frontend-core.
Implementation Overview
What a connector app needs to implement for Laioutr and Laioutr UI compatibility, and what existing connectors already provide.