Oracle JET - Filtering Rows in Table with Multiselect and Search Field Filters image 35

Oracle JET – Filtering Rows in Table with Multiselect and Search Field Filters

image

A common requirement in any web application: allow the user to quickly drill down to records of interest by specifying relevant filters. The figure overhead shows two way of setting filters: by selecting from the [limited number of] existing values in a certain column – here Location – and by specifying a search string whose value should occur in records to be displayed after filtering.

Oracle JET is a toolkit that supports development of rich web applications. With Oracle JET too this filtering feature is a common requirement. In this article I take a brief look at how to:

  • create the multi select element and how to populate it with data from the Location attribute of the records in the table
  • handle a (de)selection event in the multi select – leading to filtering of the records shown in the table
  • create the search field and intercept changes in the search field
  • handle resetting the search field
  • invoking the REST API when the search field has changed

I am not claiming to present the best possible way to implement this functionality. I am not fluent enough in JET to make such a claim, and I have seen too many different implementations in Oracle documentation, blog articles, tutorials etc. to be able to point out the one approach that stands out (for the current JET release). However, the implementation I demonstrate here seems good enough as a starting point.

The HRM module is a tab I have added to the Work Better demo application. It has its own ViewModel (hrm.js) and its own HTML view (hrm.html). I have implemented a very simple REST API in Node (http://host:port/departments?name=)  that provides the departments in a JSON document.

Sources are in this Gist: https://gist.github.com/lucasjellema/e133e5e769c13ba8507a3bee0ebc30d1

Starting Point

The starting point in this article is a simple JET application with a tab that contains a  table that displays Department records retrieved from a REST API. The implementation of this application is not very special and is not the topic of this article.

image

The objective of this article is to show how to add the capability to filter the records in this table – first by selecting the locations for which departments should be shown, using a multiselect widget. The filtering takes place on the client, against the set of departments retrieved from the backend service. The second step adds filtering by name using a search field. This level of filtering is performed by the server that exposes the REST API.

 

Create and Populate the Multiselect Element for Locations

The multiselect element in this case is the Oracle JET ojSelect component (see cookbook). `The element shows a dropdownlist of options that can be selected, displays the currently selected options and allows selected options to be deselected.

SNAGHTML5c0eb46

The HTML used to add the multiselect component to the page is shown here:

<label for="selectLocation">Locations</label>
<select id="selectLocation" data-bind="ojComponent: { component: 'ojSelect' , options: locationOptions, multiple: true , optionChange:optionChangedHandler, rootAttributes: {style:'max-width:20em'}}">  
</select>

The options attribute references the locationOptions property of the ViewModel that returns the select(able) option values – more on that later. The attribute multiple is set to true to allow multiple values to be selected and the optionChange attribute references the optionChangedHandler, a function in the ViewModel that handles option change events that are published whenever options are selected or deselected.

When the Departments have been fetched from the REST API, the locationOptions are populated by identifying the unique values for the Location attribute in all Department records. Subsequently, all locations are set as selected values on the select component – as we started out with an unfiltered set of departments. function handleDepartmentsFetch is called whenever fresh data has been fetched from the REST API.

// values for the locations shown in the multiselect
self.locationOptions = ko.observableArray([]);

self.handleDepartmentsFetch = function (collection) {
    var locationData = new Set();
    //collect distinct locations and add to locationData array 
    var locations = collection.pluck('Location'); // get all values for Location attribute
    // distill distinct values
    var locationData = new Set(locations.filter((elem, index, arr) => arr.indexOf(elem) === index));

    // rebuild locationOptions
    self.locationOptions.removeAll();

    var uniqueLocationsArray = [];
    for (let location of locationData) {
        uniqueLocationsArray.push({ 'value': location, 'label': location });
    }
    ko.utils.arrayPushAll(self.locationOptions(), uniqueLocationsArray);
    // tell the observers that this observable array has been updated
    // (as result, the Multiselect UI component will be refreshed)
    self.locationOptions.valueHasMutated();
    
    // set the selected locations on the select component based on all distinct locations available
    $("#selectLocation").ojSelect({ "value": Array.from(locationData) });
};

I did not succeed in setting the selected values on the select component by updating an observable array that backs the value attribute of the ojSelect component. As a workaround, I now use the direct manipulation using the programmatic manipulation via jQuery selection ($(“#selectLocation”)) of the ojSelect component.

 

Handle a (de)selection event in the multi select

When the user changes the set of selected values in the Locations multiselect, we want the set of departments shown in the table to be updated – narrowed down or expanded, depending on whether a location was removed or added to the selected items.

The ojSelect component has the optionChange attribute that in this case references the function optionChangeHandler . This function inspects the type of option change (equals “data”?) and then invokes function prepareFilteredDepartmentsCollection while passing the self.deppies collection that was initialized during the fetch from the REST API. This function clones the collection of all departments fetched from the REST API and subsequently filters it based on the selectedLocations.

// returns an array of the values of the currently selected options in select component with id selectLocation
self.getCurrentlySelectedLocations = function () {
    return $("#selectLocation").ojSelect("option", "value");
}

self.optionChangedHandler = function (event, data) {
    if (data.option == "value") {
        // REFILTER the data in self.DeptCol into the collection backing the table
        self.prepareFilteredDepartmentsCollection(self.deppies, getCurrentlySelectedLocations());
    }
}


// prepare (possibly filtered) set of departments and set data source for table
self.prepareFilteredDepartmentsCollection = function (collection, selectedLocations) {
    if (collection) {
        // prepare filteredDepartmentsCollection
        var filteredDepartmentsCollection = collection.clone();

        var selectedLocationsSet = new Set(selectedLocations);
        var toFilter = [];
        // find all models in the collection that do not comply with the selected locations
        for (var i = 0; i < filteredDepartmentsCollection.size(); i++) {
            var deptModel = filteredDepartmentsCollection.at(i);
            if (!selectedLocationsSet.has(deptModel.attributes.Location)) {
                toFilter.push(deptModel)
            }
        }
        // remove all departments that do not qualify according to the locations that are (not) selected
        filteredDepartmentsCollection.remove(toFilter);

        // update data source with fresh data and inform any observers of data source (such as the table component)
        self.dataSource(
            new oj.CollectionTableDataSource(filteredDepartmentsCollection));
        self.dataSource.valueHasMutated();
    }// if (collection)
}

When the collection of filtered departments is created, the self.dataSource is refreshed with a new CollectionTableDataSource. With the call to self.dataSource.valueHasMutated(), we explicitly trigger subscribers to the dataSource – the Table component.

 

Create the search field and Intercept Changes in the Search Field

The search field is simply an inputText element with some decoration. Associated with the search field is a button to reset (clear) the search field.

image

The HTML code for these elements is:


<div class="oj-flex-item oj-sm-8 ">
    
<div class="oj-flex-item" style="max-width: 400px; white-space: nowrap">
        <input aria-label="search box" placeholder="search" data-bind="value: nameSearch, valueUpdate: 'afterkeydown' , ojComponent: {component: 'ojInputText', rootAttributes:{'style':'max-width:100%;'} }" />
        
<div id="searchIcon" class="demo-icon-sprite demo-icon-search demo-search-position"></div>

        <button id="clearButton" data-bind="click: clearClick, ojComponent: { component: 'ojButton', label: 'Clear', display: 'icons', chroming: 'half', icons: {start:'oj-fwk-icon oj-fwk-icon-cross03'}}">
        </button> 
        </div>

</div>

The search field is bound to nameSearch, an observable in the ViewModel. When the user edits the contents of the search field, the observable is updated and any subscribers are triggered. One such subscriber is function self.search() – this is a computed KnockOut function that has a dependency on nameSearch. When the function is triggered – by a change in the value of nameSearch – it checks if the search string consists of three or more characters and if so, it triggers a new fetch of departments from the REST API by calling function fetchDepartments().

// bound to search field
self.nameSearch = ko.observable('');

// this computed function is implicitly subscribed to self.nameSearch; any changes in the search field will trigger this function
self.search = ko.computed(function () {
    var searchString = self.nameSearch();
    if (searchString.length > 2) {
        self.fetchDepartments();
    }
})

function getDepartmentsURL(operation, collection, options) {
    var url = dataAPI_URL + "?name=" + self.nameSearch();
    return url;
};

Function getDepartmentsURL() is invoked just prior to fetching the Departments. It returns the URL to use for fetching from the REST API. This function will add a query parameter to the URL with the value of the nameSearch observable.

 

Handle Resetting the Search Field

The Clear button – shown in the previous HTML snipptet – is associated with a click event handler: function clearClick. This function resets the nameSearch observable and explicitly declares its value updated – in order to trigger subscribers to the nameSearch observable. One such subscriber is function self.search() which will be triggered by this, and will go ahead with refetching the departments from the REST API.

// event handler for reset button (for search field)
self.clearClick = function (data, event) {
    self.nameSearch('');
    self.searchDepartments();
    return true;
}

The REST API

The REST API is implemented with Node and Express. It is extremely simple; initially it just returns the contents of a static file (departments.json) with department records. It is slightly extended to handle the name query parameter, to only return selected departments. Note that this implementation is not the most efficient. For the purpose of this article, it will do the job.

image

var app = express();
var locations = ['AMSTERDAM','ZOETERMEER','NIEUWEGEIN','MAASTRICHT']
var departments  = JSON.parse(require('fs').readFileSync('./departments.json', 'utf8'));
  // add a location to each record
  for (i = 0; i < departments.length; i++) { departments[i].location = locations[Math.floor(Math.random() * locations.length)] ; } app.get('/departments', function (req, res) { //process var nameFilter = req.query.name; // read query parameter name (/departments?name=VALUE) // filter departments by the name filter res.send( departments.filter(function (department, index, departments) { return !nameFilter ||department.DEPARTMENT_NAME.toLowerCase().indexOf(nameFilter)>-1; 
                                 })
            ); //using send to stringify and set content-type
	   });

Complete Source Code GIST

Putting all source code together:

 

 

 

Resources

Sources for this article in GitHub Gist: https://gist.github.com/lucasjellema/e133e5e769c13ba8507a3bee0ebc30d1

JET Cookbook on Multiselect http://www.oracle.com/webfolder/technetwork/jet/jetCookbook.html?component=select&demo=multi

JET Cookbook on Table and Filtering – http://www.oracle.com/webfolder/technetwork/jet/jetCookbook.html?component=table&demo=filteringTable

Blog post by Andrejus Baranovskis – Oracle JET Executing Dynamic ADF BC REST URL

JET Documentation on Collection  and its API Documentation.

Knock Documentation on Computed [Obserable] and Observable

JavaScript Gist on removing duplicates from an array – https://gist.github.com/telekosmos/3b62a31a5c43f40849bb

JavaScript Filter, Map and Reduce on Arrays: https://danmartensen.svbtle.com/javascripts-map-reduce-and-filter

Oracle JET Cookbook – Recipe on Filtering

One Response

  1. Kenny July 26, 2018