AdvancedFilterBuilder

free

AdvancedFilterBuilder edits nested rule groups and emits both a filter tree and query string.

@floating-ui/reactdayjsmotionreact-number-format
Preview
Dark
Where
              Current Query
              Use Apply Filter to commit the current rules.
              import { useState } from 'react';
              import AdvancedFilterBuilder, {
                toQueryString,
                type FieldConfig,
                type FilterTree,
              } from '@/components/composites/AdvancedFilterBuilder';
              
              const orderFields: FieldConfig[] = [
                {
                  key: 'status',
                  label: 'Status',
                  type: 'select',
                  options: [
                    { value: 'processing', label: 'Processing' },
                    { value: 'completed', label: 'Completed' },
                    { value: 'pending', label: 'Pending' },
                    { value: 'cancelled', label: 'Cancelled' },
                  ],
                },
                { key: 'order_id', label: 'Order ID', type: 'text' },
                { key: 'customer', label: 'Customer', type: 'text' },
                { key: 'total', label: 'Order total', type: 'number' },
                { key: 'placed_at', label: 'Placed date', type: 'date' },
                { key: 'paid', label: 'Paid', type: 'boolean' },
              ];
              
              const initialOrderFilter: FilterTree = {
                id: 'orders-root',
                type: 'group',
                condition: 'AND',
                children: [
                  {
                    id: 'order-status',
                    type: 'rule',
                    field: 'status',
                    operator: 'is',
                    value: 'processing',
                  },
                  {
                    id: 'order-total',
                    type: 'rule',
                    field: 'total',
                    operator: 'greater_equal',
                    value: 250,
                    logicalOperator: 'AND',
                  },
                ],
              };
              
              export default function UsageDemo() {
                const [query, setQuery] = useState(() =>
                  toQueryString(initialOrderFilter),
                );
                const [applied, setApplied] = useState('');
              
                return (
                  <div className="w-full space-y-4">
                    <div className="w-full">
                      <AdvancedFilterBuilder
                        fields={orderFields}
                        value={initialOrderFilter}
                        onChange={(_, nextQuery) => setQuery(nextQuery)}
                        onApply={(_, nextQuery) => setApplied(nextQuery)}
                      />
                    </div>
              
                    <div className="rounded-card border bg-card p-4 text-sm">
                      <div className="font-medium">
                        {applied ? 'Applied query' : 'Current Query'}
                      </div>
                      <div className="mt-1 font-mono text-xs text-muted-foreground">
                        {applied || 'Use Apply Filter to commit the current rules.'}
                      </div>
                    </div>
                  </div>
                );
              }

              Installation

              Add this component with the NateUI CLI.

              npx nateui@latest add AdvancedFilterBuilder

              Examples

              Custom Fields

              Preview
              Dark
              Where
                  Current query
                  Start with the first rule.
                  import { useState } from 'react';
                  import AdvancedFilterBuilder from '@/components/composites/AdvancedFilterBuilder';
                  import Tag from '@/components/ui/Tag';
                  import type { FieldConfig } from '@/components/composites/AdvancedFilterBuilder';
                  
                  const contactFields: FieldConfig[] = [
                    { key: 'contact_email', label: 'Contact email', type: 'text' },
                    { key: 'contact_score', label: 'Contact score', type: 'number' },
                    {
                      key: 'contact_status',
                      label: 'Contact status',
                      type: 'select',
                      options: [
                        { value: 'lead', label: 'Lead' },
                        { value: 'qualified', label: 'Qualified' },
                        { value: 'customer', label: 'Customer' },
                      ],
                    },
                    { key: 'last_activity_at', label: 'Last activity', type: 'date' },
                    { key: 'subscribed', label: 'Subscribed', type: 'boolean' },
                  ];
                  
                  export default function CustomFieldsDemo() {
                    const [query, setQuery] = useState('');
                  
                    return (
                      <div className="w-full space-y-4">
                        <div className="w-full">
                          <div className="min-w-[680px]">
                            <AdvancedFilterBuilder
                              fields={contactFields}
                              onChange={(_, nextQuery) => setQuery(nextQuery)}
                            />
                          </div>
                        </div>
                        <div className="rounded-card border bg-card p-4">
                          <div className="font-medium">
                            Current query
                          </div>
                          <div className="mt-1 font-mono text-xs text-foreground">
                            {query || 'Start with the first rule.'}
                          </div>
                        </div>
                      </div>
                    );
                  }

                  Initial Value

                  Preview
                  Dark
                  Where
                                      (status=active AND rating>=4 OR category=electronics)
                                      import { useState } from 'react';
                                      import AdvancedFilterBuilder, {
                                        toQueryString,
                                        type FilterTree,
                                      } from '@/components/composites/AdvancedFilterBuilder';
                                      import Tag from '@/components/ui/Tag';
                                      
                                      const savedView: FilterTree = {
                                        id: 'saved-view',
                                        type: 'group',
                                        condition: 'AND',
                                        children: [
                                          {
                                            id: 'saved-status',
                                            type: 'rule',
                                            field: 'status',
                                            operator: 'is',
                                            value: 'active',
                                          },
                                          {
                                            id: 'saved-rating',
                                            type: 'rule',
                                            field: 'rating',
                                            operator: 'greater_equal',
                                            value: 4,
                                            logicalOperator: 'AND',
                                          },
                                          {
                                            id: 'saved-category',
                                            type: 'rule',
                                            field: 'category',
                                            operator: 'is',
                                            value: 'electronics',
                                            logicalOperator: 'OR',
                                          },
                                        ],
                                      };
                                      
                                      export default function InitialValueDemo() {
                                        const [query, setQuery] = useState(() => toQueryString(savedView));
                                      
                                        return (
                                          <div className="w-full space-y-4">
                                            <div className="w-full">
                                              <div className="min-w-[680px]">
                                                <AdvancedFilterBuilder
                                                  value={savedView}
                                                  onChange={(_, nextQuery) => setQuery(nextQuery)}
                                                />
                                              </div>
                                            </div>
                                      
                                            <div className="rounded-card border bg-card p-4 font-mono text-xs text-muted-foreground">
                                              {query}
                                            </div>
                                          </div>
                                        );
                                      }

                                      Popover

                                      Preview
                                      Dark
                                      import { useState } from 'react';
                                      import Button from '@/components/ui/Button';
                                      import Popover from '@/components/ui/Popover';
                                      import DebounceInput from '@/components/composites/DebounceInput';
                                      import AdvancedFilterBuilder, {
                                        toQueryString,
                                        type FieldConfig,
                                        type FilterTree,
                                      } from '@/components/composites/AdvancedFilterBuilder';
                                      import { PiFunnel, PiMagnifyingGlass } from 'react-icons/pi'
                                      
                                      const fields: FieldConfig[] = [
                                        {
                                          key: 'status',
                                          label: 'Status',
                                          type: 'select',
                                          options: [
                                            { value: 'processing', label: 'Processing' },
                                            { value: 'completed', label: 'Completed' },
                                            { value: 'pending', label: 'Pending' },
                                            { value: 'cancelled', label: 'Cancelled' },
                                          ],
                                        },
                                        { key: 'order_id', label: 'Order ID', type: 'text' },
                                        { key: 'customer', label: 'Customer', type: 'text' },
                                        { key: 'total', label: 'Order total', type: 'number' },
                                        { key: 'placed_at', label: 'Placed date', type: 'date' },
                                        { key: 'paid', label: 'Paid', type: 'boolean' },
                                      ];
                                      
                                      const initialFilter: FilterTree = {
                                        id: 'popover-root',
                                        type: 'group',
                                        condition: 'AND',
                                        children: [
                                          {
                                            id: 'popover-status',
                                            type: 'rule',
                                            field: 'status',
                                            operator: 'is',
                                            value: 'processing',
                                          },
                                          {
                                            id: 'popover-total',
                                            type: 'rule',
                                            field: 'total',
                                            operator: 'greater_equal',
                                            value: 250,
                                            logicalOperator: 'AND',
                                          },
                                        ],
                                      };
                                      
                                      export default function PopoverDemo() {
                                        const [open, setOpen] = useState(false);
                                        const [filter, setFilter] = useState<FilterTree>(initialFilter);
                                        const [appliedQuery, setAppliedQuery] = useState(() =>
                                          toQueryString(initialFilter),
                                        );
                                      
                                        return (
                                          <div className="w-full">
                                            <div className="flex justify-center">
                                              <Popover
                                                open={open}
                                                onOpenChange={setOpen}
                                                placement="bottom"
                                                className="p-0"
                                                renderTrigger={
                                                  <Button icon={<PiFunnel />}>
                                                    Filters ({filter.children.length})
                                                  </Button>
                                                }
                                              >
                                                <AdvancedFilterBuilder
                                                  fields={fields}
                                                  value={filter}
                                                  onChange={(nextFilter) => setFilter(nextFilter)}
                                                  onApply={(nextFilter, nextQuery) => {
                                                    setFilter(nextFilter);
                                                    setAppliedQuery(nextQuery);
                                                    setOpen(false);
                                                  }}
                                                  onReset={(nextFilter, nextQuery) => {
                                                    setFilter(nextFilter);
                                                    setAppliedQuery(nextQuery);
                                                  }}
                                                />
                                              </Popover>
                                            </div>
                                          </div>
                                        );
                                      }

                                      Nested Groups

                                      Preview
                                      Dark
                                      Where
                                              Where
                                                        Last applied
                                                        (status=processing OR (customer~=acme AND item_count>2))
                                                        import { useState } from 'react';
                                                        import AdvancedFilterBuilder, {
                                                          toQueryString,
                                                          type FieldConfig,
                                                          type FilterTree,
                                                        } from '@/components/composites/AdvancedFilterBuilder';
                                                        
                                                        const fields: FieldConfig[] = [
                                                          {
                                                            key: 'status',
                                                            label: 'Status',
                                                            type: 'select',
                                                            options: [
                                                              { value: 'processing', label: 'Processing' },
                                                              { value: 'completed', label: 'Completed' },
                                                              { value: 'pending', label: 'Pending' },
                                                            ],
                                                          },
                                                          { key: 'customer', label: 'Customer', type: 'text' },
                                                          { key: 'item_count', label: 'Item count', type: 'number' },
                                                          { key: 'created_at', label: 'Created date', type: 'date' },
                                                        ];
                                                        
                                                        const nestedFilter: FilterTree = {
                                                          id: 'nested-root',
                                                          type: 'group',
                                                          condition: 'AND',
                                                          children: [
                                                            {
                                                              id: 'nested-status',
                                                              type: 'rule',
                                                              field: 'status',
                                                              operator: 'is',
                                                              value: 'processing',
                                                            },
                                                            {
                                                              id: 'nested-group',
                                                              type: 'group',
                                                              condition: 'AND',
                                                              logicalOperator: 'OR',
                                                              children: [
                                                                {
                                                                  id: 'nested-customer',
                                                                  type: 'rule',
                                                                  field: 'customer',
                                                                  operator: 'contains',
                                                                  value: 'acme',
                                                                },
                                                                {
                                                                  id: 'nested-count',
                                                                  type: 'rule',
                                                                  field: 'item_count',
                                                                  operator: 'greater_than',
                                                                  value: 2,
                                                                  logicalOperator: 'AND',
                                                                },
                                                              ],
                                                            },
                                                          ],
                                                        };
                                                        
                                                        export default function NestedGroupsDemo() {
                                                          const [applied, setApplied] = useState(() => toQueryString(nestedFilter));
                                                        
                                                          return (
                                                            <div className="w-full space-y-4">
                                                              <div className="w-full">
                                                                <div className="min-w-[680px]">
                                                                  <AdvancedFilterBuilder
                                                                    fields={fields}
                                                                    value={nestedFilter}
                                                                    onApply={(_, nextQuery) => setApplied(nextQuery)}
                                                                  />
                                                                </div>
                                                              </div>
                                                        
                                                              <div className="rounded-card border bg-card p-4">
                                                                <div className="font-medium">
                                                                  Last applied
                                                                </div>
                                                                <div className="mt-1 font-mono text-xs text-foreground">
                                                                  {applied}
                                                                </div>
                                                              </div>
                                                            </div>
                                                          );
                                                        }

                                                        Query Helpers

                                                        parseQueryString turns a query string into a FilterTree; toQueryString serializes the edited tree back to the string form.

                                                        Preview
                                                        Dark
                                                          Where
                                                                              import { useState } from 'react';
                                                                              import AdvancedFilterBuilder, {
                                                                                parseQueryString,
                                                                                toQueryString,
                                                                                type FilterTree,
                                                                              } from '@/components/composites/AdvancedFilterBuilder';
                                                                              import Button from '@/components/ui/Button';
                                                                              import Input from '@/components/ui/Input';
                                                                              
                                                                              const defaultQuery =
                                                                                '(status=active AND price>=100) OR category=electronics';
                                                                              
                                                                              export default function QueryHelpersDemo() {
                                                                                const [draft, setDraft] = useState(defaultQuery);
                                                                                const [tree, setTree] = useState<FilterTree>(() =>
                                                                                  parseQueryString(defaultQuery),
                                                                                );
                                                                              
                                                                                const loadQuery = () => {
                                                                                  setTree(parseQueryString(draft));
                                                                                };
                                                                              
                                                                                return (
                                                                                  <div className="w-full space-y-4">
                                                                                    <div className="flex flex-col gap-2 sm:flex-row">
                                                                                      <Input
                                                                                        value={draft}
                                                                                        onChange={(event) => setDraft(event.target.value)}
                                                                                        className="font-mono"
                                                                                      />
                                                                                      <Button onClick={loadQuery}>Load query</Button>
                                                                                    </div>
                                                                              
                                                                                    <div className="w-full">
                                                                                      <div className="min-w-[680px]">
                                                                                        <AdvancedFilterBuilder
                                                                                          value={tree}
                                                                                          onChange={(nextTree, nextQuery) => {
                                                                                            setTree(nextTree);
                                                                                            setDraft(nextQuery);
                                                                                          }}
                                                                                          onApply={(nextTree) => {
                                                                                            setDraft(toQueryString(nextTree));
                                                                                          }}
                                                                                        />
                                                                                      </div>
                                                                                    </div>
                                                                                  </div>
                                                                                );
                                                                              }

                                                                              Callback Events

                                                                              Preview
                                                                              Dark
                                                                              Where
                                                                                    Event log
                                                                                    • No callback has fired yet.
                                                                                    import { useState } from 'react';
                                                                                    import AdvancedFilterBuilder from '@/components/composites/AdvancedFilterBuilder';
                                                                                    import type {
                                                                                      FieldConfig,
                                                                                      FilterTree,
                                                                                    } from '@/components/composites/AdvancedFilterBuilder';
                                                                                    
                                                                                    const fields: FieldConfig[] = [
                                                                                      {
                                                                                        key: 'status',
                                                                                        label: 'Status',
                                                                                        type: 'select',
                                                                                        options: [
                                                                                          { value: 'open', label: 'Open' },
                                                                                          { value: 'in_review', label: 'In review' },
                                                                                          { value: 'closed', label: 'Closed' },
                                                                                        ],
                                                                                      },
                                                                                      { key: 'owner', label: 'Owner', type: 'text' },
                                                                                      { key: 'amount', label: 'Amount', type: 'number' },
                                                                                    ];
                                                                                    
                                                                                    const initialFilter: FilterTree = {
                                                                                      id: 'callback-root',
                                                                                      type: 'group',
                                                                                      condition: 'AND',
                                                                                      children: [
                                                                                        {
                                                                                          id: 'callback-status',
                                                                                          type: 'rule',
                                                                                          field: 'status',
                                                                                          operator: 'is',
                                                                                          value: 'open',
                                                                                        },
                                                                                      ],
                                                                                    };
                                                                                    
                                                                                    export default function CallbackEventsDemo() {
                                                                                      const [events, setEvents] = useState<string[]>([
                                                                                        'No callback has fired yet.',
                                                                                      ]);
                                                                                    
                                                                                      const record = (name: string, query: string) => {
                                                                                        setEvents((prev) => [
                                                                                          `${name}: ${query || 'empty query'}`,
                                                                                          ...prev.filter((event) => event !== 'No callback has fired yet.'),
                                                                                        ].slice(0, 4));
                                                                                      };
                                                                                    
                                                                                      return (
                                                                                        <div className="w-full space-y-4">
                                                                                          <div className="w-full">
                                                                                            <AdvancedFilterBuilder
                                                                                              fields={fields}
                                                                                              value={initialFilter}
                                                                                              onChange={(_, query) => record('change', query)}
                                                                                              onApply={(_, query) => record('apply', query)}
                                                                                              onReset={(_, query) => record('reset', query)}
                                                                                            />
                                                                                          </div>
                                                                                    
                                                                                          <div className="rounded-card border bg-card p-4">
                                                                                            <div className="font-medium">
                                                                                              Event log
                                                                                            </div>
                                                                                            <ul className="mt-2 space-y-2">
                                                                                              {events.map((event, index) => (
                                                                                                <li key={`${event}-${index}`} className="font-mono text-xs text-foreground">
                                                                                                  {event}
                                                                                                </li>
                                                                                              ))}
                                                                                            </ul>
                                                                                          </div>
                                                                                        </div>
                                                                                      );
                                                                                    }

                                                                                    API

                                                                                    AdvancedFilterBuilder stores rule edits internally unless a new value tree is passed from the parent. All callbacks receive the current tree and the matching query string.

                                                                                    PropDescriptionTypeDefault
                                                                                    fieldsField list used to render the field dropdown, operator list, and value input for each rule.FieldConfig[]DEFAULT_FIELDS
                                                                                    valueFilter tree used to initialize or replace the builder state.FilterTree-
                                                                                    onChangeCalled after a rule or group edit.(tree: FilterTree, queryString: string) => void-
                                                                                    onApplyCalled when the Apply Filter button is pressed.(tree: FilterTree, queryString: string) => void-
                                                                                    onResetCalled after Reset restores a single empty rule.(tree: FilterTree, queryString: string) => void-

                                                                                    Utilities

                                                                                    ExportDescriptionType
                                                                                    toQueryStringSerializes a FilterTree to the query string form used by callbacks.(tree: FilterTree) => string
                                                                                    parseQueryStringParses a query string into a FilterTree. Field type coercion currently follows the built-in default fields.(query: string) => FilterTree

                                                                                    Types

                                                                                    type FieldType = 'text' | 'number' | 'select' | 'date' | 'boolean';
                                                                                     
                                                                                    type FieldConfig = {
                                                                                      key: string;
                                                                                      label: string;
                                                                                      type: FieldType;
                                                                                      options?: Array<{ value: string; label: string }>;
                                                                                    };
                                                                                     
                                                                                    type Rule = {
                                                                                      id: string;
                                                                                      type: 'rule';
                                                                                      field: string;
                                                                                      operator: string;
                                                                                      value: string | number | boolean;
                                                                                      logicalOperator?: 'AND' | 'OR' | 'NOT';
                                                                                    };
                                                                                     
                                                                                    type RuleGroup = {
                                                                                      id: string;
                                                                                      type: 'group';
                                                                                      condition: 'AND' | 'OR';
                                                                                      children: Array<Rule | RuleGroup>;
                                                                                      logicalOperator?: 'AND' | 'OR' | 'NOT';
                                                                                    };
                                                                                     
                                                                                    type FilterTree = RuleGroup;