An an extra Add Rule button to react-querybuilder

Viewed 68

I am using react-querybuilder what I need is another Add Rule button to be add next to the original one and want to add differnt set of fields and operators when using the new button. Here is some part of my code:

import { HBButton, HBIcon } from '@hasty-bazar/core'
import { FC } from 'react'
import { useIntl } from 'react-intl'
import queryBuilderMessages from '../HBQueryBuilder.messages'

interface AddRuleActionProps {
  handleOnClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}

const AddGroupAction: FC<AddRuleActionProps> = ({ handleOnClick }) => {
  const { formatMessage } = useIntl()
  return (
    <>
    <HBButton
      onClick={handleOnClick}
      size="small"
      leftIcon={<HBIcon type="plus" />}
      sx={{ marginRight: 2, minWidth: 50 }}
    >
      {formatMessage(queryBuilderMessages.rule)}
    </HBButton>
   // >>> ANOTHER HBButton with different implementation to be added here 
    </>
  )
}

export default AddGroupAction
2 Answers

Update: see my other answer

This is a little tricky because the onAddRule callback function only accepts the rule to be added (which is always the default rule), and the parent path. If we could pass custom data into it this question would be much easier to answer.

The best way I can think to do it today is to externalize the query update methods out of the QueryBuilder component and manage them yourself (for the most part). In the example below, I've used Redux Toolkit (overkill for this use case but it's what I'm familiar with) to manage the query and replaced the Add Rule button with a custom component that renders two buttons, one to add a new rule for First Name and one to add a new rule for Last Name.

Working CodeSandbox example.

The redux store:

import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RuleGroupType } from 'react-querybuilder';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';

interface State {
  query: RuleGroupType;
}

export const getQuery = (state: State) => state.query;

const initialState: State = {
  query: {
    combinator: 'and',
    rules: [],
  },
};

const querySlice = createSlice({
  name: 'query',
  initialState,
  reducers: {
    setQuery(state: State, action: PayloadAction<RuleGroupWithAggregation>) {
      state.query = action.payload;
    },
  },
});

const { reducer } = querySlice;

export const { setQuery } = querySlice.actions;

export const store = configureStore({ reducer });

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

The App component:

import {
  ActionWithRulesProps,
  add,
  Field,
  formatQuery,
  QueryBuilder,
} from 'react-querybuilder';
import 'react-querybuilder/dist/query-builder.scss';
import { getQuery, setQuery, useAppDispatch, useAppSelector } from './store';

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

const AddRuleButtons = (props: ActionWithRulesProps) => {
  const dispatch = useAppDispatch();
  const query = useAppSelector(getQuery);

  const onClickFirst = () =>
    dispatch(
      setQuery(
        add(
          query,
          { field: 'firstName', operator: '=', value: 'First' },
          props.path
        )
      )
    );
  const onClickLast = () =>
    dispatch(
      setQuery(
        add(
          query,
          { field: 'lastName', operator: '=', value: 'Last' },
          props.path
        )
      )
    );

  return (
    <>
      <button onClick={onClickFirst}>+Rule (First Name)</button>
      <button onClick={onClickLast}>+Rule (Last Name)</button>
    </>
  );
};

export default function App() {
  const dispatch = useAppDispatch();
  const query = useAppSelector(getQuery);

  return (
    <div>
      <QueryBuilder
        fields={fields}
        query={query}
        onQueryChange={(q) => dispatch(setQuery(q))}
        controlElements={{
          addRuleAction: AddRuleButtons,
        }}
      />
      <pre>{formatQuery(query, 'json')}</pre>
    </div>
  );
}

Adding a new answer based on your feedback and because this one is very different from the other. I'm about to release v5.0 of react-querybuilder that has the feature I mentioned in the first paragraph of the other answer. This makes achieving the desired result much more straightforward and also eliminates the need for external state management (i.e. Redux).

TL;DR: working codesandbox example here (uses react-querybuilder@5.0.0-alpha.2).

React Query Builder only takes one fields prop, but you can organize the fields into an array of option groups instead of a flat array. I set the operators property on each field to the default operators, filtered appropriately for the type of field (text vs numeric).

import { Field, OptionGroup } from 'react-querybuilder';
import { nameOperators, numberOperators } from './operators';

export const fields: OptionGroup<Field>[] = [
  {
    label: 'Names',
    options: [
      { name: 'firstName', label: 'First Name', operators: nameOperators },
      { name: 'lastName', label: 'Last Name', operators: nameOperators },
    ],
  },
  {
    label: 'Numbers',
    options: [
      { name: 'height', label: 'Height', operators: numberOperators },
      { name: 'weight', label: 'Weight', operators: numberOperators },
    ],
  },
];

Next I set up a custom field selector component to only allow fields that are part of the same option group. So if a "name" field is chosen, the user can only select other "name" fields.

const FilteredFieldSelector = (props: FieldSelectorProps) => {
  const filteredFields = fields.find((optGroup) =>
    optGroup.options.map((og) => og.name).includes(props.value!)
  )!.options;

  return <ValueSelector {...{ ...props, options: filteredFields }} />;
};

This custom Add Rule button renders a separate button for each option group that calls the handleOnClick prop with the option group's label as context.

const AddRuleButtons = (props: ActionWithRulesAndAddersProps) => (
  <>
    {fields
      .map((og) => og.label)
      .map((lbl) => (
        <button onClick={(e) => props.handleOnClick(e, lbl)}>
          +Rule ({lbl})
        </button>
      ))}
  </>
);

The context is then passed to the onAddRule callback, which determines what field to assign based on the context value.

const onAddRule = (
  rule: RuleType,
  _pP: number[],
  _q: RuleGroupType,
  context: string
) => ({
  ...rule,
  context,
  field: fields.find((optGroup) => optGroup.label === context)!.options[0].name,
});

Put it all together in the QueryBuilder props, and voilĂ :

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

  return (
    <div>
      <QueryBuilder
        fields={fields}
        query={query}
        onQueryChange={(q) => setQuery(q)}
        controlElements={{
          addRuleAction: AddRuleButtons,
          fieldSelector: FilteredFieldSelector,
        }}
        onAddRule={onAddRule}
      />
      <pre>{formatQuery(query, 'json')}</pre>
    </div>
  );
}
Related