Skip to main content
Version: Next

Customization showcase

The following examples demonstrate the extensive customization capability of React Query Builder. If you have an interesting or unusual implementation, feel free to submit a pull request to add it here!

Justified layout

These CSS rules will push any "clone", "lock", or "remove" buttons to the right edge, giving the query builder a more "justified" appearance. The demo has an option to enable this technique.

SCSS
.queryBuilder {
.ruleGroup-addGroup {
& + button.ruleGroup-cloneGroup,
& + button.ruleGroup-lock,
& + button.ruleGroup-remove {
margin-left: auto !important;
}
}
.rule-operators,
.rule-value {
& + button.rule-cloneRule,
& + button.rule-lock,
& + button.rule-remove {
margin-left: auto !important;
}
}
}
CSS (compiled from SCSS)
.queryBuilder .ruleGroup-addGroup + button.ruleGroup-cloneGroup,
.queryBuilder .ruleGroup-addGroup + button.ruleGroup-lock,
.queryBuilder .ruleGroup-addGroup + button.ruleGroup-remove,
.queryBuilder .rule-operators + button.rule-cloneRule,
.queryBuilder .rule-operators + button.rule-lock,
.queryBuilder .rule-operators + button.rule-remove,
.queryBuilder .rule-value + button.rule-cloneRule,
.queryBuilder .rule-value + button.rule-lock,
.queryBuilder .rule-value + button.rule-remove {
margin-left: auto !important;
}
https://example.com

Inline combinator selectors

Position each combinator selector to the right of the preceding rule or group.

note

The examples in this section implement independent combinators, but the same styles are applicable when using showCombinatorsBetweenRules.

CSS
.ruleGroup-body {
/* Override the default flex layout */
display: grid !important;
/* Allow the left-hand column (the rule/subgroup) to expand as needed */
/* Collapse the right-hand column (the combinator) to the width of the content */
grid-template-columns: auto min-content;
/* Keep the combinator aligned with the bottom of the rule/subgroup */
align-items: end;
}
https://example.com

Alternatively, position each combinator to the left of the following rule or group:

CSS
.ruleGroup-body {
/* Override the default flex layout */
display: grid !important;
/* Allow the right-hand column (the rule/subgroup) to expand as needed */
/* Collapse the left-hand column (the combinator) to the width of the content */
grid-template-columns: min-content auto;
/* Keep the combinator aligned with the top of the rule/subgroup */
align-items: start;
}

/* Indent the first rule/subgroup since it has no preceding combinator */
.ruleGroup-body > .rule:first-child:not(:only-child),
.ruleGroup-body > .ruleGroup:first-child:not(:only-child) {
grid-column-start: 2;
}
https://example.com

Disjunctive normal form

This example implements disjunctive normal form (DNF) by restricting the root group combinator to "or", the subgroup combinators to "and", and only allowing subgroups one level deep. Other customizations include:

  • CSS rules:
    • Swap the order of the header and body within each group.
    • Hide the top-level "add rule" button to prevent the addition of rules to the root group.
    • Hide the "add group" button in subgroups to prevent the addition of second-level nested groups.
    • Display the subgroups horizontally.
    • Stack the field/operator/value rule elements vertically (to account for the horizontal group layout).
    • Hide all "remove group" buttons, which would otherwise be rendered—somewhat oddly—at the bottom of the group.
  • Custom components/props:
    • Display combinators as static text instead of a selection control, since DNF is always an "OR of ANDs."
    • Groups are removed when the last rule in the group is removed.
    • New groups get a default rule automatically (otherwise a new group could not be removed until a rule was added).
tip

This example also uses some techniques which are described in more detail in the arbitrary updates tips and hooks documentation.

You may want to hide the left-hand sidebar (click << at the bottom) to have a wider view of this example.

import { useState } from 'react';
import type {
  Field,
  RuleGroupType,
  ActionWithRulesAndAddersProps,
  CombinatorSelectorProps,
} from 'react-querybuilder';
import {
  ActionElement,
  getOption,
  QueryBuilder,
  findPath,
  getParentPath,
  remove,
  useQueryBuilderQuery,
} from 'react-querybuilder';

const fields: Field[] = [
  { name: 'firstName', label: 'First Name' },
  { name: 'lastName', label: 'Last Name' },
];

const initialQuery: RuleGroupType = {
  combinator: 'or',
  rules: [
    {
      combinator: 'and',
      rules: [
        { field: 'firstName', operator: 'beginsWith', value: 'Stev' },
        { field: 'lastName', operator: 'in', value: 'Vai,Vaughan' },
      ],
    },
    {
      combinator: 'and',
      rules: [
        {
          field: 'firstName',
          operator: 'beginsWith',
          value: 'To remove group, remove last rule',
        },
      ],
    },
  ],
};

const RemoveRule = (props: ActionWithRulesAndAddersProps) => {
  const query = useQueryBuilderQuery();
  if (!query) return null;
  const parentPath = getParentPath(props.path);
  /** Count of rules in the same group */
  const siblingCount = (findPath(parentPath, query) as RuleGroupType).rules.length - 1;
  const onClick = (e: React.MouseEvent) => {
    if (siblingCount > 0) {
      // If sibling rules exist, remove the current rule
      // using the default handler
      props.handleOnClick(e, props.context);
    } else {
      // Otherwise, remove the entire group
      props.schema.dispatchQuery(remove(query, getParentPath(props.path)));
    }
  };
  // Render with default props except for our custom click handler
  if (siblingCount === 0 && query.rules.length === 1) {
    // Don't render the "remove rule" action if this is
    // the only rule in the only group
    return null;
    // (You could also go ahead and render the button but
    // not remove its group in the click handler)
  }
  return <ActionElement {...props} handleOnClick={onClick} />;
};

const CombinatorSelector = (props: CombinatorSelectorProps) => (
  // Render static value to prevent combinator updates
  <div className={props.className} title={props.title}>
    {getOption(props.options, props.value!)?.label}
  </div>
);

export default () => {
  const [query, setQuery] = useState(initialQuery);

  return (
    <QueryBuilder
      fields={fields}
      query={query}
      onQueryChange={setQuery}
      // Move combinator control from group
      // header to group body:
      showCombinatorsBetweenRules
      // Ensure new groups are not empty:
      addRuleToNewGroups
      controlElements={{
        // Use our custom components
        removeRuleAction: RemoveRule,
        combinatorSelector: CombinatorSelector,
      }}
      translations={{
        addGroup: { label: '+' },
        addRule: { label: '+' },
      }}
    />
  );
};