Skip to main content
Version: Next

Custom components with fallbacks

You may run into a situation where one of the default components almost meets your requirements, but you don't want to recreate the entire component just to slightly modify the behavior. Falling back to the default component after implementing your custom behavior is a good way to keep your implementation up to date with the current version's standard features while retaining the flexibility of a fully custom solution.

For example, let's say you need a value editor that presents the user with a date picker (not the browser's default date picker), but only for certain fields. The default ValueEditor does not implement a date picker, so you'll need to use a custom component.

However, you don't need to copy/paste the default ValueEditor code to take advantage of its functionality. Simply spread the same props that were passed in to your custom component (<ValueEditor {...props} />) and return that if your custom behavior is not applicable.

Let's create a custom value editor that uses the react-datepicker library. First we'll set up the fields array. Each element is a standard Field object, but the two date-type fields have a special attribute called datatype that will let our custom value editor know when and how to display the date picker.

// fields.ts
import { Field } from 'react-querybuilder';

export const fields: Field[] = [
{
name: 'name',
label: 'Name',
operators: [
{ name: '=', label: 'is' },
{ name: 'beginsWith', label: 'begins with' },
],
},
{
name: 'dateOfBirth',
label: 'Date of Birth',
operators: [{ name: '=', label: 'is' }],
datatype: 'date',
},
{
name: 'dateRange',
label: 'Date Range',
operators: [{ name: 'between', label: 'is between' }],
datatype: 'dateRange',
},
];

Next, we'll define the custom value editor. The component will display a standard date picker if the datatype for the field is "date", and a date range picker if the datatype is "dateRange". If the datatype is something else or undefined, then the component will simply forward its props to the default ValueEditor.

We're also using the date-fns library to help parse and format dates. Storing the date values as strings instead of Date objects helps ensure that the query object remains serializable in case we want to safely use JSON.stringify. (Date ranges are stored as a comma-separated pair of strings.)

// CustomValueEditor.tsx
import { format, parse } from 'date-fns';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { ValueEditor, ValueEditorProps } from 'react-querybuilder';

const dateFormat = 'yyyy-MM-dd';

export const CustomValueEditor = (props: ValueEditorProps) => {
if (props.fieldData.datatype === 'date') {
return (
<div>
<DatePicker
dateFormat={dateFormat}
selected={!props.value ? null : parse(props.value, dateFormat, new Date())}
onChange={(d: Date) => props.handleOnChange(d ? format(d, dateFormat) : null)}
/>
</div>
);
} else if (props.fieldData.datatype === 'dateRange') {
const [startDate, endDate] = props.value.split(',');
return (
<div>
<DatePicker
selectsRange
dateFormat={dateFormat}
startDate={!startDate ? null : parse(startDate, dateFormat, new Date())}
endDate={!endDate ? null : parse(endDate, dateFormat, new Date())}
onChange={(update: [Date, Date]) => {
const [s, e] = update;
props.handleOnChange(
[!s ? '' : format(s, dateFormat), !e ? '' : format(e, dateFormat)].join(',')
);
}}
/>
</div>
);
}
return <ValueEditor {...props} />;
};
tip

If you're using one of the compatibility packages, you probably want to fall back to the value editor from that package instead of ValueEditor from the main package. For example, when using @react-querybuilder/antd, fall back to AntDValueEditor:

-import { ValueEditor, ValueEditorProps } from 'react-querybuilder';
+import { AntDValueEditor } from '@react-querybuilder/antd';
+import { ValueEditorProps } from 'react-querybuilder';
-  return <ValueEditor {...props} />;
+ return <AntDValueEditor {...props} />;

Finally, we can configure the main QueryBuilder component to use our custom value editor with the controlElements prop.

// App.tsx
import { useState } from 'react';
import { CustomValueEditor } from './CustomValueEditor';
import { fields } from './fields';

export default function App() {
const [query, setQuery] = useState({ combinator: 'and', rules: [] });
return (
<QueryBuilder
fields={fields}
query={query}
onQueryChange={setQuery}
controlElements={{ valueEditor: CustomValueEditor }}
/>
);
}

An interactive demo is below. Note how the "Name" field uses a text input, the "Date of Birth" field uses a standard date picker, and the "Date Range" field uses the date range picker.

import { format, parse } from 'date-fns';
import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
import { ValueEditor, ValueEditorProps } from 'react-querybuilder';

const dateFormat = 'yyyy-MM-dd';

export const CustomValueEditor = (props: ValueEditorProps) => {
  if (props.fieldData.datatype === 'date') {
    return (
      <div>
        <DatePicker
          dateFormat={dateFormat}
          selected={!props.value ? null : parse(props.value, dateFormat, new Date())}
          onChange={(d: Date) => props.handleOnChange(d ? format(d, dateFormat) : null)}
        />
      </div>
    );
  } else if (props.fieldData.datatype === 'dateRange') {
    const [startDate, endDate] = props.value.split(',');
    return (
      <div>
        <DatePicker
          selectsRange
          dateFormat={dateFormat}
          startDate={!startDate ? null : parse(startDate, dateFormat, new Date())}
          endDate={!endDate ? null : parse(endDate, dateFormat, new Date())}
          onChange={(range: [Date, Date]) => {
            const [s, e] = range;
            props.handleOnChange(
              [!s ? '' : format(s, dateFormat), !e ? '' : format(e, dateFormat)].join(',')
            );
          }}
        />
      </div>
    );
  }
  return <ValueEditor {...props} />;
};

note

Other examples of the "fallback" technique can be seen in the Limit rule groups page and these two StackOverflow answers.