How to Filter in "Add Filters"

I have developed some js to enable the filtering of connected fields in the "Add Filter" modal.

I started by creating a new object that holds all the filter "Rules". It looks like this:

![](upload://cRGWYWjdmZ5CsXv4d5yybOGGxDD.png)

The records atm look like this:

![](upload://7tjrBDpxWlPGR4FJges9bqm33gC.png)

 

In my example below, "Staff" is field_1304 and based on my rules above, I only want to show staff records where field_1308 (which is status) is Active. I get this info from scene_3118 and view_6577.

![](upload://qzWUg7bdxLqbtVrVOyB0qUuyGAK.png)

 

Here is the js:

//////////////////////////////////////////////////////////////////////////////
// This function gets all the filter rules and stores them in a global array.
// The scene and view are specified in the url.
// The fields are specified to match the fields in the created filter rule object.
// Within this function is the only place that code needs to change when moving the code to another Knack App. 
var getFilterRules = function () {
	var tryAttempts = 0;
	// load the filter records
	var recordUrl = "https://api.knack.com/v1/pages/scene_3119/views/view_6578/records/";
	var recordUrl = recordUrl + "?rows_per_page=500";
$.ajax( {
	url: recordUrl,
	type: "GET",
	dataType: 'JSON',
	headers: {
		'Authorization': Knack.getUserToken(),
		'X-Knack-Application-ID': Knack.application_id,
		'X-Knack-REST-API-Key': 'knack'
	},
	tryCount : tryAttempts,
	retryLimit : 5,

	success: function (filterData) {
		//Loop through filterData, build g_fC
		for (var i = 0; i < filterData.total_records; i++) {
			g_fC[i] = {												//filter Record
				id: filterData.records[i].id,						//Record Id
				s_field: filterData.records[i].field_3318,			//Selected Filter Field
				t_field: filterData.records[i].field_3319,			//Field To Test
				t_operator: filterData.records[i].field_3320,		//Operator to use
				t_value: filterData.records[i].field_3321,			//Value to test against
				t_scene: filterData.records[i].field_3322,			//Ajax Scene
				t_view: filterData.records[i].field_3323,			//Ajax View
				descriptor: filterData.records[i].field_3324		//Descriptor for Filter List Dropdown
			}
		}
		return g_fC;
	},
	error : function (request, status, error) {
		tryAttempts = this.tryCount++;
		if (this.tryCount <= this.retryLimit) {
			var endTime = new Date();
			var startTime = new Date();
			do {
				endTime = new Date();
			} while (endTime - startTime < 200);
			$.ajax(this);
		}
	}
});

}

// This is where the global variables are declared and the filter rules loaded into an array (once only)
var g_filtersObjectLoaded;

if (!g_filtersObjectLoaded) { // Only load this once
g_filtersObjectLoaded = true;

var g_fC = [];					// Filters Collection
var g_fL = [];					// Filters List array - used on the Filter Form

getFilterRules();

}

// To catch the filter form, need to bind on to something that is always there.
// The filter form is dynamically created so need to start at a higher level than where it appears.
var knackDistEl = document.getElementById(“knack-dist_1”);
$(knackDistEl).on(“click”, “#kn-filters-form”, function (event) {
// isTrigger indicates the “Add Filters” button has been clicked. When this happens, need to reset array
// and reload any previously selected filters
if (event.isTrigger) {
g_fL = ; // Reset Filters List array when filter form displayed
}
var filterFormEl = document.getElementById(“kn-filters-form”);
var filterFieldEls = filterFormEl.getElementsByClassName(“field select”); // This is the left hand side of the filter

// Go through each row of filter lines
for (var j = 0; j < filterFieldEls.length; j++) {
	var filterFieldEl = filterFieldEls[j];
	var filterFieldOptions = filterFieldEl.getElementsByTagName("option");
	var optCount = filterFieldOptions.length;
	// Find the field selected in each filter row
	for (var i = 0; i < optCount; i++) {
		if (filterFieldOptions[i].selected == true) {
			var selectedField = filterFieldOptions[i];
			break;
		}
	}
	
	// Compare the selected field to the value in our array.
	// If it is not the same then load it in the array and compare against the filter rules
	if (selectedField.value != g_fL[j]) {
		g_fL[j] = selectedField.value;

		var arrRules = [];
		var k = 0;
		var matchRule = [];
		
		// Go through all the rules in g_fC and see if the selectedField.value matches any rules
		// If it does (could be multiple rules) add the rules to the array called arrRules
		var filterCount = g_fC.length;
		for (var i = 0; i < filterCount; i++) {
			if (g_fC[i].s_field == selectedField.value) {
				matchRule = {
					'field': g_fC[i].t_field, 'operator': g_fC[i].t_operator, 'value': g_fC[i].t_value
				}
				arrRules[k] = matchRule;
				k++;
				var t_scene = g_fC[i].t_scene;
				var t_view = g_fC[i].t_view;
				var descriptor = g_fC[i].descriptor;
			}
		}
		// If we have any rules matching, make the Ajax call
		if (k > 0) {

			var filterValueEls = filterFormEl.getElementsByClassName("kn-filter-value");
			var filterNumber = j;
			var pFilterValueEl = filterValueEls[filterNumber];
			
			// Protect the Filter Value field until we have reloaded  
			pFilterValueEl.classList.add("protected-fields");

			getFilterList(arrRules, t_scene, t_view, descriptor, filterNumber, function (filterList, fNumber) {
				var incSelected = true;

				var filterValueEl = filterValueEls[fNumber];
				var filterListEls = filterValueEl.querySelectorAll("select");
				var filterListEl = filterListEls[0];
				
				// Delay 0.5 seconds as sometimes our build is faster than the initial load
				var delayInMilliseconds = 500; //0.5 second
				setTimeout(function () {
					loadFilterList(filterListEl, filterList, incSelected, function () {
						filterValueEl.classList.remove("protected-fields");
					});
				}, delayInMilliseconds);
			});
		}
	}
}

});

var loadFilterList = function (filterListEl, filterList, incSelected, callback) {

var selectVal = [];

// Need to start at the end and work backwards when removing options.
var L = filterListEl.options.length - 1;
for (var i = L; i > 0; i--) { 		// To keep 'Select' then leave the first option in place. ie >0, not >=0
	var selectedOption = filterListEl.options[i].getAttribute("selected");
	if (selectedOption != null) {
		selectVal[i] = filterListEl.options[i].value;
	}
	filterListEl.options.remove(i);
}
// Rebuild the options based on the retrieved data
for (var i = 0; i < filterList.length; i++) {
	// Create an Option object       
	var opt = document.createElement("option");

	// Assign text and value to Option object
	opt.value = (filterList[i][0]);
	opt.text = (filterList[i][1]);

	if (incSelected) {
		for (j = 0; j < selectVal.length; j++) {
			if (opt.value === selectVal[j]) opt.selected = "selected";
		}
	}
	// Add Option object to Drop Down List
	filterListEl.add(opt);
}
// Update the list
$("#" + filterListEl.id).trigger("liszt:updated").change();
$("#" + filterListEl.id).chosen().trigger("change");

callback();

}

var getFilterList = function (arrRules, t_scene, t_view, descriptor, filterNumber, callback) {

var tryAttempts = 0;
var filterList = [];

// Prepare filters
var filters = {
	'match': 'and',
	'rules': arrRules
}

var recordUrl = "https://api.knack.com/v1/pages/" + t_scene + "/views/" + t_view + "/records";
var filterUrl = '?rows_per_page=500&filters=' + encodeURIComponent(JSON.stringify(filters));
var totalRecordUrl = recordUrl + filterUrl;

$.ajax( {
	url: totalRecordUrl,
	type: "GET",
	dataType: 'JSON',
	headers: {
		'Authorization': Knack.getUserToken(),
		'X-Knack-Application-ID': Knack.application_id,
		'X-Knack-REST-API-Key': 'knack'
	},
	tryCount : tryAttempts,
	retryLimit : 5,
	success: function (data) {
		for (var i = 0; i < data.total_records; i++) {
			filterList[i] = [data.records[i].id, data.records[i][descriptor]];
		}
		callback(filterList, filterNumber);
	},
	error : function (request, status, error) {
		tryAttempts = this.tryCount++;
		console.log(tryAttempts);
		if (this.tryCount <= this.retryLimit) {
			var endTime = new Date();
			var startTime = new Date();
			do {
				endTime = new Date();
			} while (endTime - startTime < 200);
			$.ajax(this);
		}
	}
});

}

// Make sure all the filter selections on all views are not clickable.
$(document).on(‘knack-view-render.any’, function (event, view, data) {
var viewEl = document.getElementById(view.key);
if (viewEl != null) {
var editFilterEls = viewEl.querySelectorAll(".kn-edit-filter");
for (var i = 0; i < editFilterEls.length; i++) {
editFilterEls[i].classList.add(“protected-fields”);
}
}
});

 

and a little bit of CSS:

/* Protects Fields while loading */
.protected-fields {
    pointer-events: none;
    opacity: 0.4; /* Opacity is for test use only */
}

 

If you have any questions, suggestions or improvements, please let me know.

Tony

tony@omegaim.com.au

 

 

Hi Phil, I've set up a demo app. If you want to send me an email with your email address, I'll share my demo app with you.

tony@omegaim.com.au

Tony

That's some nice JS code 116907185011

I'm tempted to have a go. If you happen to create a demo then I'll take a look and likely implement fro a few clients.

Thanks for sharing 

I now have it successfully working in two customer sites. One customer is using multiple rules to filter on one selected field. It takes a few minutes to get your head around how it works but so far it seems pretty robust.

No live demo available. 

Nice work. Do you have a demo?

This is needed, but we should have this natively on knack.