.md

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 selection
  • catalog.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');
});
Show unavailable options

Display all filter options including those with zero matches (grayed out).

const productList = dcupl.lists.create({ modelKey: 'Product' });

// Show all categories, even those with 0 products matching current filters
const facets = productList.catalog.fn.facets({
  attribute: 'category',
  excludeZeros: false, // Include facets with size=0
});

facets.forEach((facet) => {
  const status = facet.enabled ? '✓' : '✗';
  const availability = facet.size > 0 ? 'available' : 'unavailable';

  console.log(status + ' ' + facet.value + ' (' + facet.size + ') - ' + availability);
});

// Use case: Show "Electronics (0)" grayed out when no electronics match filters
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
});