Adding new custom operator without recreating old operators

Hello!

I'm attempting to add a new operator to a date field (within, which will allow users to add a number and select a time length like day, month, or year) but from what I can tell from the docs, using a template will require that I handle the other operator cases and I really don't want to do that!

Am I misunderstanding what template does or is there a way to add a new operator without needing to recreate the already existing operators.


5 Replies

NH Nicholas Hagans March 31, 2025 08:27 PM UTC

Somewhat related question:
The `actionBegin` event is not firing for some reason. I'll post what I have so far, hopefully it'll help. I guess I should note that this is in Svelte but I can't imagine that being the reason this isn't working since I'm just using the JS library.

I feel I'm almost there when it comes to getting the new operator to work alongside the existing ones, but I have not been able to get the values to combine into an array or object and I haven't been able to get a rule to prepopulate the query builder.


onMount(async () => {
let { QueryBuilder } = await import('@syncfusion/ej2-querybuilder');
let { NumericTextBox } = await import('@syncfusion/ej2-inputs');
let { DropDownList } = await import('@syncfusion/ej2-dropdowns');
qryBldrObj = new QueryBuilder({
width: width,
columns: qbSchema,
enableNotCondition: true,
rule: rules,
separator: '.',
actionBegin: (args) => {
console.log(args);
},
});
qryBldrObj.appendTo(`#${title}`);

qryBldrObj.addEventListener('actionBegin', (args) => {
console.log(args);
});

qryBldrObj.addEventListener('change', (args) => {
if (args.type === 'operator' && (args.value === 'within' || args.value === 'notwithin')) {
console.log(args);
const ruleID = `${title}_${args.ruleID}`;
const newElem = `<input type="text" class= "e-within-template" id='${ruleID}_within'>
<input type="text" class="e-period-template" id='${ruleID}_period'>`;
if (document.getElementById(ruleID)) {
(
(document.getElementById(ruleID) as HTMLElement).querySelector(
'.e-rule-value',
) as Element
).innerHTML = newElem;
}

let numberInput = new NumericTextBox({
min: 0,
max: Number.MAX_VALUE,
format: 'n',
value: 0,
change: (e: any) => {
let elem: HTMLElement = document
.getElementById(ruleID)
?.querySelector('.e-rule-value .e-within-template');
qryBldrObj.notifyChange(e.value, elem, 'value');
},
});
numberInput.appendTo(`#${ruleID}_within`);
let ds = [
{ value: 'days', key: 'Days' },
{ value: 'weeks', key: 'Weeks' },
{ value: 'months', key: 'Months' },
{ value: 'years', key: 'Years' },
];
let dropdownInput = new DropDownList({
dataSource: ds,
fields: { text: 'key', value: 'value' },
value: ds[0].value,
change: (e: any) => {
let elem: HTMLElement = document
.getElementById(ruleID)
?.querySelector('.e-rule-value .e-period-template');
qryBldrObj.notifyChange(e.value as string, elem, 'value[1]');
},
});
dropdownInput.appendTo(`#${ruleID}_period`);
}
});
qryBldrObj.addEventListener('ruleChange', (args: RuleChangeEventArgs) => {
console.log(args);
console.log('schema', qbSchema);
rules = args.rule;
});
});


NH Nicholas Hagans April 2, 2025 02:57 PM UTC

I've got a custom template working the way I wanted to (getting a "Within" and "Not Within" operator with two inputs) but I ended up just recreating the other operators. One of the issues I ran into with getting `actionBegin` to work is that it only fires for fields that has the `template` field set. Also, when setting a template, it'll need to have an accompanying script element in the html that defines the template (see below).

I'd still REALLY like to know if there is a way to do this without having to recreate the behavior of the other operators.

For completeness and for those coming from Google, here is what I came up with for Sveltekit (some things omitted):


onMount
(async () => {
let { QueryBuilder } = await import('@syncfusion/ej2-querybuilder');
let { NumericTextBox } = await import('@syncfusion/ej2-inputs');
let { DropDownList } = await import('@syncfusion/ej2-dropdowns');
let { DatePicker } = await import('@syncfusion/ej2-calendars');
let qryBldrObj: QueryBuilder = new QueryBuilder({
width: width,
columns: qbSchema,
enableNotCondition: true,
rule: rules,
separator: '.',
actionBegin: (args) => {
let ruleID = args.ruleID;
if (args.requestType === 'value-template-create') {
switch (args.operator) {
case 'within':
case 'notwithin':
let numberInput = new NumericTextBox({
min: 0,
max: Number.MAX_VALUE,
format: 'n',
value: args.rule.value[0] || 0,
change: (e: any) => {
let elem: Element | undefined | null = document
.getElementById(ruleID)
?.querySelector(`#${ruleID}_timeframe`);
qryBldrObj.notifyChange([e.value, args.rule.value[1]], elem as Element, 'value');
},
});
numberInput.appendTo(`#${ruleID}_timeframe`);

let ds = [
{ value: 'days', key: 'Days' },
{ value: 'weeks', key: 'Weeks' },
{ value: 'months', key: 'Months' },
{ value: 'years', key: 'Years' },
];
let dropdownInput = new DropDownList({
dataSource: ds,
fields: { text: 'key', value: 'value' },
value: args.rule.value[1] || ds[0].value,
change: (e: any) => {
let elem: Element | undefined | null = document
.getElementById(ruleID)
?.querySelector(`#${ruleID}_unit`);
qryBldrObj.notifyChange(
[args.rule.value[0], e.value as string],
elem as Element,
'value',
);
},
});
dropdownInput.appendTo(`#${ruleID}_unit`);
break;
case 'between':
case 'notbetween':
let valueElem = `<input type="text" class="e-start-template" id='${ruleID}_start' />
<input type="text" class="e-end-template" id='${ruleID}_end' />`;
if (document.getElementById(ruleID)) {
(
(document.getElementById(ruleID) as HTMLElement).querySelector(
'.e-rule-value',
) as Element
).innerHTML = valueElem;
}
let dateInputOne = new DatePicker({
value: args.rule.value[0] || getDateNow('DATE_SHORT'),
format: 'MM/dd/yyyy',
change: (e: any) => {
console.log('here now');
let elem: HTMLElement | undefined | null = document
.getElementById(ruleID)
?.querySelector(`#${ruleID}_start`);
qryBldrObj.notifyChange([e.value, args.rule.value[1]], elem, 'value');
},
});
dateInputOne.appendTo(`#${ruleID}_start`);
let dateInputTwo = new DatePicker({
value: args.rule.value[1] || getDateNow('DATE_SHORT'),
format: 'MM/dd/yyyy',
change: (e: any) => {
let elem: HTMLElement | undefined | null = document
.getElementById(ruleID)
?.querySelector(`#${ruleID}_end`);
qryBldrObj.notifyChange([args.rule.value[0], e.value], elem, 'value');
},
});
dateInputTwo.appendTo(`#${ruleID}_end`);
break;
default:
let singleValueElem = `<input type="text" class="e-rule-value" id='${ruleID}_value' />`;
if (document.getElementById(ruleID)) {
(
(document.getElementById(ruleID) as HTMLElement).querySelector(
'.e-rule-value',
) as Element
).innerHTML = singleValueElem;
}
let singleDateInput = new DatePicker({
value: args.rule.value || null,
format: 'MM/dd/yyyy',
change: (e: any) => {
let elem: HTMLElement | undefined | null = document
.getElementById(ruleID)
?.querySelector(`#${ruleID}_value`);
qryBldrObj.notifyChange(e.value, elem, 'value');
},
});
singleDateInput.appendTo(`#${ruleID}_value`);
break;
}
}
},
});
qryBldrObj.appendTo(`#${title}`);

<svelte:head>
<script id="dateTemplate" type="text/x-template">
<div class="e-rule-value">
<input type="number" class="e-timeframe-template" id="${ruleID}_timeframe" />
<input type="text" class="e-unit-template" id="${ruleID}_unit" />
</div>;
</script>
</svelte:head>
<div class="querybuilder" id={title}></div>


YS Yohapuja Selvakumaran Syncfusion Team April 4, 2025 12:54 PM UTC

Hi Nicholas,

We have reviewed your query and prepared a sample that aligns with your requirements. To achieve this functionality, you can make use of the operators property available in the QueryBuilder’s column configuration. This allows you to define and add custom operators to meet specific conditions.


In addition, by using the valueTemplate feature of the QueryBuilder, you can further customize how values are rendered and handled for each rule.


For your convenience, we’ve listed some helpful resources below that demonstrate how to add and configure custom operators in the QueryBuilder component:


How to Add Custom Operator for Query Builder in React?


https://www.syncfusion.com/forums/183939/query-builder-custom-operators-and-values-for-the-same-field


https://www.syncfusion.com/forums/170297/how-can-i-customize-the-operator-field-in-the-query-builder-rule-template


https://www.syncfusion.com/forums/193650/how-can-i-add-an-custom-operator-in-query-builder


Please feel free to review the above examples, and let us know if you need further assistance or have any specific scenarios you’d like help implementing. We’ll be glad to assist you further.



Regards,

Yohapuja S



NH Nicholas Hagans replied to Yohapuja Selvakumaran April 4, 2025 01:10 PM UTC

Thank you for the reply!


In coming to my solution I did indeed reference those other questions. My question, however, was if there is a way to add a custom operator without needing to recreate the behavior of the other operators. There are at least 3 different operator types for Dates (4 after including my new, custom one) and I would rather not have to maintain those operator types if I can avoid it.

Regards,
Nick H.



YS Yohapuja Selvakumaran Syncfusion Team April 11, 2025 12:59 PM UTC

Hi Nick,

Thank you for your follow-up and for sharing the context of your implementation.


To clarify, if you are looking to add a custom operator without needing to reimplement the logic for the existing built-in operators, the recommended approach is to extend the operators property for the specific column, as you've correctly identified.


In the example below, we demonstrate how to include a custom operator along with the default ones for a Date column:


 

 

         <e-column

            field="HireDate"

            label="Hire Date"

            type="date"

            format="dd/MM/yyyy"

            :operators="employeeOperators"

          />

 

 

 

 employeeOperators: [

        { value: 'equal', key: 'Equal' },

        { value: 'notequal', key: 'Not Equal' },

        { value: 'notin', key: 'Not In' },

        { value: 'greaterthan', key: 'Greater Than' },

        { value: 'lessthan', key: 'Lesser Than' },

      ],

 

 


Here is a sample for your reference:


Sample:
https://stackblitz.com/edit/yfdb9og2-t6fhbivx?file=src%2FApp.vue


You may also find the following documentation helpful:


Documentation:  https://ej2.syncfusion.com/vue/documentation/query-builder/columns#operators




Regards,

Yohapuja S


Loader.
Up arrow icon