# 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

```typescript
list.catalog.fn.facets(options)
```

## Parameters

- `attribute` (`string`, required) — The attribute to get facets for
- `count` (`number`, optional) — 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
- `excludeGroup` (`string`, optional) — 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
- `excludeZeros` (`boolean`, optional) — Exclude facets with 0 count (default: true)
- `excludeUndefineds` (`boolean`, optional) — Exclude undefined values (default: true)
- `calculateResults` (`boolean`, optional) — 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)
- `excludeUnresolved` (`boolean`, optional) — Exclude reference keys that do not resolve to an item of the referenced model (default: true)

## Return type

Returns an array of DcuplFacet objects:

```typescript
// 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

- [How to Build Faceted Search](/sdk/advanced/faceted-search)
- [Functions API reference: global dcupl.fn.\* form and errors](/sdk/api/functions)

## Examples

**Building a filter sidebar**

Create a complete filter UI with facets showing available options, counts, and selected state.

```typescript
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.

```typescript
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).

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
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 + ')');
});
```

**SDK**

```typescript
// 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
});
```
