catalog.fn.facets()
Get unique values with counts for building filter UIs. Returns the distinct values of an attribute with how many items match each value under the list's current query.
Syntax
list.catalog.fn.facets(options)Parameters
attributestringRequired- The attribute to get facets for
countnumberOptional- Limit the number of facet values returned (applied after sort, if set)
sort'size-desc' | 'size-asc' | 'value-asc' | 'value-desc'Optional- Sort the values; without it they come back in index order
excludeGroupstringOptional- Key of the query group left out when counting (default: the group keyed by attribute), found at any nesting depth. Use it when the selection for this attribute lives in a group with a custom key
excludeZerosbooleanOptional- Exclude facets with 0 count (default: true)
excludeUndefinedsbooleanOptional- Exclude undefined values (default: true)
calculateResultsbooleanOptional- Count every matching item and return all their keys (default: true). With false each value stops at its first match: size is 0 or 1 and resultKeys has at most one key. Ignored when sort is set (sorting needs the real sizes)
excludeUnresolvedbooleanOptional- Exclude reference keys that do not resolve to an item of the referenced model (default: true)
Return type
Returns an array of DcuplFacet objects:
// Can be imported from @dcupl/common
type DcuplFacet = {
key: string; // The value as a string
value: boolean | string | null | undefined; // The raw value (a number for numeric attributes)
size: number; // Count of items with this value
enabled: boolean; // size > 0
selected: boolean; // The excluded group has a condition on this value
entry: ListItem; // The referenced item for a direct reference attribute; { key } for
// properties and dotted paths; undefined for an unresolved or null reference key
// (listed only with excludeUnresolved: false) or a reference's no-value entry
// (only with excludeUndefineds: false)
resultKeys: string[]; // Matching item keys
children?: DcuplFacet[]; // Not set by fn.facets()
}Usage notes
- Counts are disjunctive: each value is counted under every other group of the current query. The group keyed by the faceted attribute (or the one named by excludeGroup) is left out, so selecting a value does not hide its siblings, while conditions on other attributes reduce the counts
- Ideal for building filter sidebars, search refinements, and data exploration
- Performance: Set calculateResults: false when you only need to know which values are available (enabled), not their counts or item keys
- Performance: Use count parameter to limit results for attributes with many unique values
- Values with 0 matches are left out by default (excludeZeros: true); pass excludeZeros: false to show them disabled
- Troubleshooting: If a facet shows only the selected value and selected is false, the selection lives in a group with a custom key. Pass that key as excludeGroup.
Related methods
catalog.query.addCondition()— Apply filters based on facet selectioncatalog.query.many()— Get the actual json values behind the resultKeys
Related guides/docs
Examples
Building a filter sidebar
Create a complete filter UI with facets showing available options, counts, and selected state.
const productList = dcupl.lists.create({ modelKey: 'Product' });
function renderFilterSidebar() {
const categoryFacets = productList.catalog.fn.facets({
attribute: 'category',
excludeZeros: false, // keep values that would return 0 items (rendered disabled)
});
const filterHTML = categoryFacets.map((facet) => {
const checkedAttr = facet.selected ? 'checked' : '';
const disabledAttr = !facet.enabled ? 'disabled' : '';
const countClass = facet.enabled ? '' : 'text-text-muted';
return `
<label class="flex items-center gap-2 ${disabledAttr ? 'opacity-50' : ''}">
<input type="checkbox" value="${facet.key}" ${checkedAttr} ${disabledAttr}>
<span>${facet.value}</span>
<span class="${countClass}">(${facet.size})</span>
</label>
`;
}).join('');
document.getElementById('category-filters')!.innerHTML = filterHTML;
}Deep property facets
Get facets from referenced models using dot notation for related data.
const orderList = dcupl.lists.create({ modelKey: 'Order' });
// Facets on a property two references away (Order -> Product -> Brand).
// A path that ends at a reference ('product.brand') returns the brand keys.
const brandFacets = orderList.catalog.fn.facets({
attribute: 'product.brand.name',
});
console.log('Brands in orders:', brandFacets.map(f => f.value));
// Facets from a multi-level reference
const countryFacets = orderList.catalog.fn.facets({
attribute: 'customer.address.country',
});
console.log('Customer countries:');
countryFacets.forEach((f) => {
console.log(' ' + f.value + ': ' + f.size + ' orders');
});Performance optimization
Skip full counting when you only need to know which values are available, not how many items each has.
const productList = dcupl.lists.create({ modelKey: 'Product' });
// Fast: stop at the first matching item per value
const fastFacets = productList.catalog.fn.facets({
attribute: 'category',
calculateResults: false,
});
// size is only 0 or 1 here and resultKeys holds at most one key,
// so use enabled (available or not), not size
fastFacets.forEach((f) => {
console.log(f.value + ': ' + (f.enabled ? 'available' : 'unavailable'));
});
// Full (default): real counts and every matching item key
const fullFacets = productList.catalog.fn.facets({
attribute: 'category',
calculateResults: true,
});
fullFacets.forEach((f) => {
console.log(f.value + ': ' + f.size + ' items, e.g. ' + f.resultKeys.slice(0, 3).join(', '));
});Limiting facet results
Limit the number of facet values returned for "Top N" style filters.
const productList = dcupl.lists.create({ modelKey: 'Product' });
// Get top 5 brands by count
const topBrands = productList.catalog.fn.facets({
attribute: 'brand',
sort: 'size-desc', // without sort, count keeps the first 5 values in index order
count: 5,
});
console.log('Top 5 Brands:');
topBrands.forEach((f, i) => {
console.log((i + 1) + '. ' + f.value + ' (' + f.size + ' products)');
});
// Combine with excludeZeros for populated results only
const popularCategories = productList.catalog.fn.facets({
attribute: 'category',
count: 10,
excludeZeros: true,
excludeUndefineds: true,
});Facets with active filters
Conditions on other attributes reduce the counts; values that would return 0 items stay listed with excludeZeros: false.
const productList = dcupl.lists.create({ modelKey: 'Product' });
// Initial facets (no filters)
console.log('All Categories:');
let categoryFacets = productList.catalog.fn.facets({ attribute: 'category' });
categoryFacets.forEach(f => console.log(' ' + f.value + ': ' + f.size));
// Apply a filter
productList.catalog.query.addCondition({
operator: 'eq',
attribute: 'brand',
value: 'Nike',
});
// Counts now reflect the brand filter
console.log('\nCategories for Nike products:');
categoryFacets = productList.catalog.fn.facets({ attribute: 'category', excludeZeros: false });
categoryFacets.forEach(f => console.log(' ' + f.value + ': ' + f.size));
// enabled=false means selecting would result in 0 items
categoryFacets.forEach(f => {
if (!f.enabled) {
console.log(' ⚠️ ' + f.value + ' would return 0 results');
}
});Multi-select with a custom group (excludeGroup)
Counts are disjunctive: the group keyed by the faceted attribute is left out. If the selection lives in a group with another key, name it with excludeGroup.
const productList = dcupl.lists.create({ modelKey: 'Product' });
// Selection stored in a group with a custom key
productList.catalog.query.addGroup({
groupKey: 'category-filter',
groupType: 'or',
queries: [{ operator: 'eq', attribute: 'category', value: 'Electronics' }],
});
// Without excludeGroup the selection counts against itself:
// only Electronics is listed, and selected is false
const collapsed = productList.catalog.fn.facets({ attribute: 'category' });
// Leave the selection group out: every category keeps its count,
// Electronics is marked selected
const facets = productList.catalog.fn.facets({
attribute: 'category',
excludeGroup: 'category-filter',
});
facets.forEach((f) => {
console.log((f.selected ? '[x] ' : '[ ] ') + f.value + ' (' + f.size + ')');
});// Get facets for the 'brand' attribute
const facets = list.catalog.fn.facets({
attribute: 'brand'
});
// With options
const topCategories = list.catalog.fn.facets({
attribute: 'category',
sort: 'size-desc', // without sort, count takes the first values in index order
count: 5,
excludeZeros: false
});[]