Search code examples
angularprimeng

Filter nested column in PrimeNG Table (Angular)


So I have a column which is nested e.g. customer.hobbies.favourite. I want to be able to filter based on the favourite.name.

Data Sample:

{
        id: 1000,
        name: 'James Butt',
        country: {
          name: 'Algeria',
          code: 'dz',
        },
        company: 'Benton, John B Jr',
        date: '2015-09-13',
        status: 'unqualified',
        verified: true,
        activity: 17,
        representative: {
          name: 'Ioni Bowcher',
          image: 'ionibowcher.png',
        },
        balance: 70663,
        hobbies: {
          favourite: [
            {
              id: 1,
              name: 'football',
            },
            {
              id: 2,
              name: 'basketball',
            },
          ],
        },
      },

My retrieval of hobbies to have them display

  getHobbies(customer: any): string {
    return customer.hobbies.favourite.map((h: any) => h.name).join(', ');
  }

Hobbies HTML

          <div class="flex align-items-center">
            Hobbies
            <p-columnFilter
              type="text"
              field="hobbies.favourite"
              display="menu"
            />
          </div>

Row

        <td>{{ getHobbies(customer) }}</td>

For example:

  • If for the Hobbies Column, I filter so that it starts with "football" then it should show James Butt.

  • If I filter contains "c" then only all rows with cricket would show. In this case Josephine Darakjy

Stackblitz replicating my issue: https://stackblitz.com/edit/8xxfhn-hapzpy

Any ideas on how to solve this issue?


Solution

  • With FilterService, you can customize the filter (logic) for the single column.

    <div class="flex align-items-center">
      Hobbies
      <p-columnFilter
        type="text"
        field="hobbies.favourite"
        display="menu"
        [showMatchModes]="false"
        [showOperator]="false"
        [showAddButton]="false"
        [matchModeOptions]="{ label: 'Hobbies Contain', value: 'hobbies-contain' }"
        [matchMode]="'hobbies-contain'"
      />
    </div>
    

    Register the FilterService into the component and add the filter constraint: "hobbies-contain"

    import { FilterService } from 'primeng/api';
    
    @Component({
      selector: 'table-filter-advanced-demo',
      templateUrl: 'table-filter-advanced-demo.html',
      standalone: true,
      imports: [ImportsModule],
      providers: [CustomerService, FilterService],
    })
    export class TableFilterAdvancedDemo implements OnInit {
      ...
    
      constructor(
        private customerService: CustomerService,
        private filterService: FilterService
      ) {}
    
      ngOnInit() {
        ...
    
        this.filterService.register('hobbies-contain', (value, filter): boolean => {
          if (filter === undefined || filter === null || filter.trim() === '') {
            return true;
          }
    
          if (value === undefined || value === null) {
            return false;
          }
    
          return this.filterFavoriteHobbies(value, filter).length > 0;
        });
      }
    
      ...
    }
    

    However, the global filter won't work as it filters the data with "contains" match mode. The custom filter for hobbies.favourite does not apply to the global filter.

    The easiest way is to modify your table data and interface to return the hobbies.favourite as string[] instead of the nested array of objects. Apply the new field: favouriteHobbies to the interface, data and PrimeNG table.

    export type CustomerWithFavoriteHobbies = Customer & {
      favouriteHobbies: string[] | null;
    } 
    
    customers!: CustomerWithFavoriteHobbies[];
    
    ngOnInit() {
      this.customerService.getCustomersLarge().then((customers) => {
        this.customers = customers.map(x => ({
          ...x,
          favouriteHobbies: x.hobbies?.favourite?.map(y => y.name)
        }));
    
        this.loading = false;
    
        this.customers.forEach(
          (customer) => (customer.date = new Date(<Date>customer.date))
        );
      });
    }
    
    <p-table
        #dt1
        [value]="customers"
        dataKey="id"
        [rows]="10"
        [rowsPerPageOptions]="[10, 25, 50]"
        [loading]="loading"
        [paginator]="true"
        [globalFilterFields]="['name', 'country.name', 'representative.name', 'status', 'favouriteHobbies']"
      >
      <ng-template pTemplate="header">
        <tr>
          ...
    
          <th style="min-width: 10rem">
            <div class="flex align-items-center">
              Hobbies
              <p-columnFilter
                type="text"
                field="favouriteHobbies"
                display="menu"
                matchMode="contains"
              />
            </div>
          </th>
        </tr>
      </ng-template>
    
      <ng-template pTemplate="body" let-customer>
        <tr>
          ...
    
          <td>
            {{ customer.favouriteHobbies?.join(', ') }}
          </td>
        </tr>
      </ng-template>
    
      ...
    
    </p-table>
    

    Demo @ StackBlitz