Skip to Content
Technical Articles
Author's profile photo Bilen Cekic

UI5 : Dynamic Facet Filter with Lodash

Facet filter is a very cool filter control which can be used in crowded tables. Unlike column filter in grid table, it allows multi selection.

You can check explored for more detail.

During the usage of facet filter, you might notice that after selecting values, the content is not dynamically populated based on the filtered rows. By default, there is no direct connection between facet filter content and the filtered table content.

Lets say user want to filter products and later on suppliers based on filtered products row. By default, user will select products from “Products” filter but when they come to “Suppliers” filter  still they will see all values which doesn’t look proper.

There is a way of doing it before list open (haven’t tried myself) but it is not capturing if you have both column filter(with sap.ui.table) and facet filter. Lodash method always populates based on the active table contents and i find this method much more easier.

What we are going to do is, we will implement Lodash library and pre-populate facet filter content dynamically based on table content. Lodash is javascript library which contains functions that makes it easier to work with arrays and I really cannot imagine my UI5 programming without it. Whenever you feel terrified, petrified, mortified, stupified or stressed and depressed, just open a code playground and fix some complex array scenarios with Lodash :). 

I am going to demonstrate it by using codepen which my another code playground tool. It allows you create folder structure and include third party libraries without any issue.

Content of facet filter data looks something like this;

Below is a the detail of attributes;

JSON content of the facet filter should be as below;

  • FACET_ID: Column name for the data coming from JSON and to be used during filtering.
  • FACET_TITLE : What users see before click.
  • VALUES : List to be populated after click.
    • FILTER_ID : Value to be used for filtering with FACET_ID
    • FILTER_DESC : Value shown to user

We are going to populate our facet filter after our rows are populated. For this purpose, we are going to use  attachChange event of the table binding after rendering.

   onAfterRendering: function() {
        var bind = this.getView().byId("idProductsTable").getBinding("items");
        bind.attachChange(this.RowBindingChange);
      },

For filtering, sorting or after any row binding change, method “RowBindingChange” will be triggered. onAfterRendering will be triggered only once after page is rendered so please don’t confuse. What makes our function always to be triggered is “attachChange” event.

After each RowBindingChange event triggered, our algorithm will follow below steps;

  • Get existing filtered rows from table (not only visible! it is binded rows! ).
  • Decide what columns will be added to facet filter
  • Get unique records for each column to be filtered.
  • Prepare content and update model

 

Here is the flow of RowBindingChange

 

 RowBindingChange: function(oEvent) {

First we get the existing rendered rows.

       var that = this;
        var data = [];
        // get binded rows into data!
        var oList = oEvent.getSource().oList;
        $.each(oEvent.getSource().aIndices, function(a, b) {
          data.push(oList[b]);
        });

Now we need to configure which columns needs to appear in facet filter;

       // facet configuration
        var filter_header = [];

       //filter products. column name is Name in our JSON content for that array
       //we will use FILTER_ID during filtering. Generally it is better to use ID here rather than description
        var filter_product = {
          COL: "Name",
          FILTER_ID: "Name",
          FILTER_NAME: "Name"
        };
// add supplier as well
        var filter_supplier = {
          COL: "SupplierName",
          FILTER_ID: "SupplierName",
          FILTER_NAME: "SupplierName"
        };
        // facet header configuration. TITLE is what users will see on top. FACET_ID will be used during filtering and values will be shown to user.
        var filter_product_header = {
          FACET_TITLE: "Products",
          FACET_ID: "Name",
          VALUES: filter_product
        };

        var filter_supp_header = {
          FACET_TITLE: "Suppliers",
          FACET_ID: "SupplierName",
          VALUES: filter_supplier
        };
       //populate config!
        filter_header.push(filter_product_header);
        filter_header.push(filter_supp_header);

Configuration for facet filter is done! Now we need to populate the content for facet filter. Here we are using map and uniqBy method.

Map function will trigger function for each item in the array and that array will be populated by uniqBy function based on the column and array we are passing.

      // FACET_BUILD for title and values
        function FACET_BUILD(IV_FACET_TITLE, IV_FACET_ID, IT_VALUES) {
          var result = {
            FACET_TITLE: IV_FACET_TITLE,
            FACET_ID: IV_FACET_ID,
            VALUES: IT_VALUES
          };
          return result;
        }
      //We need to get unique rows with Lodash from the table rows [obj parameter].
      //and return in proper structure.
        function FACET_GET_VALUES(IV_COL, IV_FILTER_ID, IV_FILTER_NAME, obj) {
          var result = _.map(_.uniqBy(obj, IV_COL), function(item) {
            return {
              FILTER_ID: item[IV_FILTER_ID],
              FILTER_DESC: item[IV_FILTER_NAME]
            };
          });
          return result;
        }

Now we can use our config data and populate facet filter.

        var FACET_FILTER = [];
        // prepare values for filter 
        _.each(filter_header, function(obj) {
          var FACET_VAL = FACET_GET_VAL(
            obj.VALUES.COL,
            obj.VALUES.FILTER_ID,
            obj.VALUES.FILTER_NAME,
            data
          );
        //populate final data!
          var FACET_HEADER = FACET_GET_HEADER(
            obj.FACET_TITLE,
            obj.FACET_ID,
            FACET_VAL
          );
       //collect
          FACET_FILTER.push(FACET_HEADER);
        });
// finally update model!. It is better not to use global models but this is for test purpose.
        sap.ui.getCore().getModel().setProperty("/FACET_FILTER", FACET_FILTER);
        
        console.log(FACET_FILTER);

After binding filter events of facet filter, application should be ready. I am not going to explain filtering events, you can find it at the bottom of the page. I included reset button as well to roll back filtering and all.

 

 

Here you can find the full application.

https://codepen.io/bilencekic/project/editor/ZMWNmJ

Have a nice day!

Keep your enemies close and javascript libraries closer.

 

Assigned Tags

      2 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Wolfgang Röckelein
      Wolfgang Röckelein

      Hi Bilen,

       

      thanks for the helpful blog, nice idea and implementation. Two questions:

      • Lists in SAPUI5 normally load only the first 100 Elements (esp. from an OData Binding). Thus the seleciton values would also only come from these elements and not all elements, or?
      • Does you onAfterRendering implementation not lead to multiple RowBindingChange calls if the list page is rendered multiple times (eg user navgates away from list page and later returns to the list page)?

      Regards,

      Wolfgang

      Author's profile photo Bilen Cekic
      Bilen Cekic
      Blog Post Author

      Hi Wolfgang!

      Thank you for your reply and here is the answers,

      • Lists in SAPUI5 normally load only the first 100 Elements (esp. from an OData Binding). Thus the seleciton values would also only come from these elements and not all elements, or?

      By default yes! I changed example to have more than 100 members you can have a look at it. Even JSON model binding also will show 100 members initially, i just set growing=true and growingThreshold = 110. Actually my sample should be sap.ui.table because we could observe grid column filtering and dynamic changing of facet filter content.
      For the list more than 100 members won't be a problem for our filtering but on OData binding especially partial data loading yes it will be a problem because here i am doing a client side array operations. Personally i don't use OData binding directly to tables and retrieve it via lazy loading. (first of all it irritates me second thing is client doesn't like a loading state in the middle of application either) . What i am doing is, i am converting my internal table in ABAP to JSON format and sending to UI5. Normally my tables have around 20-30K members (and of course i am using sap.ui.table ), it is working without problem. But you are right, during partial loading it will show whatever is binded to table.

      • Does you onAfterRendering implementation not lead to multiple RowBindingChange calls if the list page is rendered multiple times (eg user navgates away from list page and later returns to the list page)

      yes correct actually i am binding my attach event after service call is success on my existing application. That sample application on codepen is just for demo purpose it should not be in onAfterRendering (i just use this event to make sure data binding is complete). I checked now and it is triggered 2 times initially.

      But on performance side for this facet filter generation, even for 30K+ rows Lodash process very fast and you don't notice any issues.