React Query Builder Overview
The Ignite UI for React Query Builder provides a rich UI that allows developers to build complex data filtering queries for a specified data set. With this component, you can build an expression tree and specify AND/OR conditions between expressions, with editors and condition lists determined by each field's data type. The expression tree can then be easily transformed to a query in a format the backend supports.
Getting started with React Query Builder
To start using the QueryBuilder, first, you need to install the Ignite UI for React package by running the following command:
npm install igniteui-react igniteui-react-grids
You also need to reference the corresponding styles based on your project configuration.
import 'igniteui-webcomponents-grids/grids/themes/light/bootstrap.css';
Using the React Query Builder
If no expression tree is initially set, you start by choosing an entity and which of its fields the query should return. After that, conditions or sub-groups can be added.
In order to add a condition you select a field, an operand based on the field data type and a value if the operand is not unary. The operands In and Not In will allow you to create an inner query with conditions for a different entity instead of simply providing a value. Once the condition is committed, a chip with the condition information appears. By clicking or hovering the chip, you have the options to modify it or add another condition or group right after it.
Clicking on the (AND or OR) button placed above each group, will open a menu with options to change the group type or ungroup the conditions inside.
Since every condition is related to a specific field from a particular entity changing the entity will lead to resetting all preset conditions and groups.
You can start using the component by setting the Entities property to an array describing the entity name and an array of its fields, where each field is defined by its name and data type. Once a field is selected it will automatically assign the corresponding operands based on the data type.
The Query Builder has the IgrExpressionTree property. You could use it to set an initial state of the control and access the user-specified filtering logic.
private queryBuilderRef: React.RefObject<IgcQueryBuilderComponent>;
constructor(props: any) {
super(props);
this.queryBuilderRef = React.createRef();
this.state = {
expressionTree: null
};
}
componentDidMount() {
const tree = new IgrFilteringExpressionsTree();
tree.operator = FilteringLogic.And;
tree.entity = 'Orders';
this.setState({ expressionTree: tree });
if (this.queryBuilderRef.current && tree) {
const queryBuilder = this.queryBuilderRef.current;
queryBuilder.entities = this.entities as any;
queryBuilder.expressionTree = tree;
queryBuilder.addEventListener('expressionTreeChange', this.handleExpressionTreeChange);
}
}
componentWillUnmount() {
if (this.queryBuilderRef.current) {
this.queryBuilderRef.current.removeEventListener('expressionTreeChange', this.handleExpressionTreeChange);
}
}
private handleExpressionTreeChange = (event: CustomEvent<IgcExpressionTree>) => {
this.setState({ expressionTree: event.detail });
};
private get ordersFields(): Field[] {
return [
{ field: 'orderId', dataType: 'number' },
{ field: 'customerId', dataType: 'string' },
{ field: 'orderDate', dataType: 'date' }
];
}
private get entities(): Entity[] {
return [
{ name: 'Orders', fields: this.ordersFields }
];
}
private onExpressionTreeChange() {
// Handle expression tree changes
console.log('Expression tree changed:', this.state.expressionTree);
}
public render(): JSX.Element {
return (
<div className="container sample">
<IgrQueryBuilder ref={this.queryBuilderRef} id="queryBuilder"></IgrQueryBuilder>
</div>
);
}
The IgrExpressionTree is stored in the component state which means you can subscribe to the ExpressionTreeChange event to receive notifications when the end-user changes the UI by creating, editing or removing conditions. The event listener is attached in componentDidMount and cleaned up in componentWillUnmount.
private handleExpressionTreeChange = (event: CustomEvent<IgcExpressionTree>) => {
this.setState({ expressionTree: event.detail });
this.onExpressionTreeChange();
};
Expressions Dragging
Condition chips can be easily repositioned using mouse Drag & Drop or Keyboard reordering approaches. With those, users can adjust their query logic dynamically.
- Dragging a chip does not modify its condition/contents, only its position.
- Chip can also be dragged along groups and subgroups. For example, grouping/ungrouping expressions is achieved via the Expressions Dragging functionality. In order to group already existing conditions, first you need to add a new group through the 'add' group button. Then via dragging, the required expressions can be moved to that group. In order to ungroup, you could drag all conditions outside their current group and once the last condition is moved out, the group will be deleted.
[!NOTE] Chips from one query tree cannot be dragged in another, e.g. from parent to inner and vice versa.
Keyboard interaction
Key Combinations
- Tab / Shift + Tab - navigates to the next/previous chip, drag indicator, remove button, 'add' expression button.
- Arrow Down/Arrow Up - when chip's drag indicator is focused, the chip can be moved up/down.
- Space / Enter - focused expression enters edit mode. If chip is been moved, this confirms it's new position.
- Esc - chip's reordering is canceled and it returns to it's original position.
[!NOTE] Keyboard reordering provides the same functionality as mouse Drag & Drop. Once a chip is moved, user has to confirm the new position or cancel the reorder.
Templating
The Ignite UI for React Query Builder allows defining templates for the component's header and search value:
Header Template
By default the {ComponentName} header would not be displayed. In order to define such, the igc-query-builder-header component should be added inside igc-query-builder.
Search Value Template
The search value of a condition can be templated by setting the SearchValueTemplate property to a function that returns a lit-html template.
[!Note] When using
SearchValueTemplate, you must provide templates for all field types in your entity, or the query builder will not function correctly. It is mandatory to implement a default/fallback template that handles any fields or conditions not covered by specific custom templates. Without this, users will not be able to edit conditions for those fields.
<IgrQueryBuilder
ref={this.queryBuilderRef}
id="queryBuilder"
searchValueTemplate={this.buildSearchValueTemplate}>
<IgrQueryBuilderHeader title="Query Builder Template Sample"></IgrQueryBuilderHeader>
</IgrQueryBuilder>
componentDidMount() {
if (this.queryBuilderRef.current && tree) {
const queryBuilder = this.queryBuilderRef.current;
queryBuilder.entities = this.entities as any;
queryBuilder.expressionTree = tree;
}
}
private buildSearchValueTemplate = (ctx: QueryBuilderSearchValueContext) => {
const field = ctx.selectedField?.field;
const condition = ctx.selectedCondition;
const matchesEqualityCondition = condition === 'equals' || condition === 'doesNotEqual';
if (!ctx.implicit) {
ctx.implicit = { value: null };
}
if (field === 'Region' && matchesEqualityCondition) {
return this.buildRegionSelect(ctx);
}
if (field === 'OrderStatus' && matchesEqualityCondition) {
return this.buildStatusRadios(ctx);
}
if (ctx.selectedField?.dataType === 'date') {
return this.buildDatePicker(ctx);
}
if (ctx.selectedField?.dataType === 'time') {
return this.buildTimeInput(ctx);
}
return this.buildDefaultInput(ctx, matchesEqualityCondition);
};
Below are examples showing one template for each type of editor:
For the Region Select example:
// Field definition
{ field: 'Region', dataType: 'string' }
// Template
private buildRegionSelect = (ctx: QueryBuilderSearchValueContext) => {
const currentValue = ctx?.implicit?.value?.value ?? '';
const key = `region-select-${currentValue}`;
return (
<IgrSelect
className="qb-select"
key={key}
value={currentValue}
change={(sender: any) => {
const value = sender.value;
const currentKey = ctx?.implicit?.value?.value ?? '';
if (!value || value === currentKey) return;
setTimeout(() => {
ctx.implicit.value = this.regionOptions.find(option => option.value === value) ?? null;
});
}}>
{this.regionOptions.map(option => (
<IgrSelectItem key={option.value} value={option.value}>
<span>{option.text}</span>
</IgrSelectItem>
))}
</IgrSelect>
);
};
For the Status Radio Group example:
// Field definition
{ field: 'OrderStatus', dataType: 'number' }
// Template
private buildStatusRadios = (ctx: QueryBuilderSearchValueContext) => {
const implicitValue = ctx.implicit?.value;
const currentValue = implicitValue === null ? '' : implicitValue.toString();
const key = `status-radio-${currentValue}`;
return (
<IgrRadioGroup
key={key}
style={{ gap: '5px' }}
alignment="horizontal"
value={currentValue}
change={(sender: any) => {
const value = sender.value;
if (value === undefined) return;
const numericValue = Number(value);
if (ctx.implicit.value === numericValue) return;
setTimeout(() => {
ctx.implicit.value = numericValue;
});
}}>
{this.statusOptions.map(option => (
<IgrRadio
key={option.value}
name="status"
value={option.value.toString()}
checked={option.value.toString() === currentValue}
labelText={option.text}>
</IgrRadio>
))}
</IgrRadioGroup>
);
};
For the Date Picker example:
// Field definition
{ field: 'OrderDate', dataType: 'date' }
// Template
private buildDatePicker = (ctx: QueryBuilderSearchValueContext) => {
const implicitValue = ctx.implicit?.value;
const currentValue = implicitValue instanceof Date
? implicitValue
: implicitValue
? new Date(implicitValue)
: null;
const allowedConditions = ['equals', 'doesNotEqual', 'before', 'after'];
const isEnabled = allowedConditions.indexOf(ctx.selectedCondition ?? '') !== -1;
const key = `date-picker-${currentValue}`;
return (
<IgrDatePicker
key={key}
value={currentValue}
disabled={!isEnabled}
click={(sender: any) => sender.show()}
change={(sender: any) => {
setTimeout(() => {
ctx.implicit.value = sender.value;
});
}}>
</IgrDatePicker>
);
};
For the Time Input example:
// Field definition
{ field: 'RequiredTime', dataType: 'time' }
// Template
private buildTimeInput = (ctx: QueryBuilderSearchValueContext) => {
const currentValue = this.normalizeTimeValue(ctx.implicit?.value);
const allowedConditions = ['at', 'not_at', 'at_before', 'at_after', 'before', 'after'];
const isDisabled = ctx.selectedField == null || allowedConditions.indexOf(ctx.selectedCondition ?? '') === -1;
const key = `time-input-${currentValue}`;
return (
<IgrDateTimeInput
key={key}
inputFormat="hh:mm tt"
value={currentValue}
disabled={isDisabled}
change={(sender: any) => {
setTimeout(() => {
ctx.implicit.value = sender.value;
});
}}>
<div slot="prefix">
<IgrIcon name="clock" collection="material" />
</div>
</IgrDateTimeInput>
);
};
For the Default Input template:
// Field definitions for string, number, and boolean types
{ field: 'ShipCountry', dataType: 'string' }
{ field: 'OrderID', dataType: 'number' }
{ field: 'IsRushOrder', dataType: 'boolean' }
// Template that handles all these types
private buildDefaultInput = (ctx: QueryBuilderSearchValueContext, matchesEqualityCondition: boolean) => {
const selectedField = ctx.selectedField;
const dataType = selectedField?.dataType;
const isNumber = dataType === 'number';
const isBoolean = dataType === 'boolean';
const placeholder = ctx.selectedCondition === 'inQuery' || ctx.selectedCondition === 'notInQuery'
? 'Sub-query results'
: 'Value';
const currentValue = typeof ctx.implicit?.value === 'object' && (ctx.implicit.value && 'text' in ctx.implicit.value)
? matchesEqualityCondition ? ctx.implicit.value.text : ''
: ctx.implicit?.value;
const inputValue = currentValue == null ? '' : currentValue;
const disabledConditions = ['empty', 'notEmpty', 'null', 'notNull', 'inQuery', 'notInQuery'];
const isDisabled = isBoolean || selectedField == null || disabledConditions.indexOf(ctx.selectedCondition ?? '') !== -1;
const key = `default-input-${inputValue}`;
return (
<IgrInput
key={key}
value={inputValue?.toString() || ''}
disabled={isDisabled}
placeholder={placeholder}
type={isNumber ? 'number' : 'text'}
input={(sender: any) => {
const value = sender.value;
setTimeout(() => {
ctx.implicit.value = isNumber
? value === '' ? null : Number(value)
: value;
});
}}>
</IgrInput>
);
};
Formatter
In order to change the appearance of the search value in the chip displayed when a condition is not in edit mode, you can set a formatter function to the fields array. The search value can be accessed through the value argument as follows:
this.ordersFields = [
{ field: 'OrderID', dataType: 'number' },
{ field: 'ShipCountry', dataType: 'string' },
{
field: 'OrderDate',
dataType: 'date',
formatter: (value: any) => value.toLocaleDateString(this.queryBuilder?.locale, {
month: 'short',
day: 'numeric',
year: 'numeric'
})
},
{
field: 'Region',
dataType: 'string',
formatter: (value: any) => value?.text ?? value?.value ?? value
}
];
Demo
We’ve created this example to show you the templating and formatter functionalities for the header and the search value of the React Query Builder component.
API Reference
QueryBuilderQueryBuilderHeaderIgrExpressionTreeIgrFilteringExpressionsTreeFilteringLogicIgrStringFilteringOperandQueryBuilderSearchValueContext- Styling & Themes
Additional Resources
Our community is active and always welcoming to new ideas.