1

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 2

3

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 [email protected]).

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>
  );
}
Sign up to request clarification or add additional context in comments.

1 Comment

Hi Jake. Thank you for your answer. Would you please answer the following question too : stackoverflow.com/questions/74872787/…
0

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>
  );
}

2 Comments

@Tthank you for your answer. But what I need is to be able to add more than one different field sets (fields). Based on each button click separate sets of fields should be populated in dropdownlist.
@theGhostN - see my more recent answer (stackoverflow.com/a/73523849/765987)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.