0

I am working on a jqGrid that will be used to view users. I'm trying to make this grid as flexible and robust as possible as it may be included in several places - it will be used as a standalone page to view/search through all users, it will be included on a specific company's page filtered by that company to show all the users within that company, and it will be used as a result of a "quick search" available on the site (input a single search term, hit "quick search", and it will pull up any user with a meaningful field that matches the search term). So I need to accommodate 3 options:

  1. Nothing passed in - load an empty grid with the ability to filter on specific columns (I'm using the filterToolbar)
  2. Quick Search Term passed in - invoke the search with that term
  3. Filters passed in (in a single URL parameter in JSON format) - invoke the search with those column filters (and load those filters into the toolbar)

I have a ColdFusion cfc function that accepts the jqGrid params (page, sidx, etc), the column filter params, and the quick search param. If the quick search is passed in it invokes a quick search stored procedure; if it doesn't it invokes a separate filter stored procedure passing the column filters. In either case it returns the current page's results in the proper format for the grid.

I am having a hard time navigating the jqGrid docs to figure out the best way to accomplish this. I'm not sure how to initially load the grid up with no results, or how to pass in the filters or the quick search term from the URL before loading the grid so we can pull up a predefined search. I am able to accomplish it with the following logic:

  • Determine whether quickSearch was passed in. If so, set the URL to myFunctionURL?quickSearch=passedinSearchTerm
  • If quickSearch was not passed in, set URL to myFunctionURL?initialLoad=true (and set up the function to return no results if initialLoad is true)
  • On loadComplete, (if quickSearch was not passed in and this is the initialLoad) load all the filters into the filter toolbar, reset the URL programmatically to plain myFunctionURL, and invoke triggerToolbar

This seems really hacky. What is a cleaner way to accomplish this workflow?

EDIT: Here's some sample code. Keep in mind that some of the logic is in pseudocode so may not be syntactically correct. The grid looks something like this:

$(function(){
    var initialLoad = true;
    var quickSearch = getUrlParam("quickSearch");
    var basicGridURL = 'myCfc.cfc?method=myFunction';
    var gridURL = basicGridURL;
    if(len(quickSearch)){
        gridURL = gridURL + '&quickSearch=' + quickSearch;
    } else{
        gridURL = gridURL + '&initialLoad=' + initialLoad;
    }
    $("#users").jqGrid({
            datatype: 'json',
            url: gridURL,
            gridview: true,
            colModel: [
                {name: 'lastname', label: 'Last Name'},
                {name: 'firstname', label: 'First Name'},
                ... more column code here ...
              ],
            height:'auto',
            autowidth:true,
            caption:'Users',
            rowNum:20,
            rowList:[10,20,50],
            sortorder:'asc',
            sortname: 'lastname',
            ignoreCase: true, // case-insensitive filtering
            pager: '#pager',
            jsonReader: {
                root: "ROWS", //our data
                page: "PAGE", //current page
                total: "TOTAL", //total pages
                records:"RECORDS", //total records
                cell: "", //not used
                id: "0" //will default first column as ID
            },
            // after loading, fill the filter toolbar with any filters passed in through the URL and trigger search
            loadComplete: function(){
                // load filterParams from URL and cpnvert to object
                filterParams = $.parseJSON(getUrlParam(filterParams));
                // if this is the first load of the table and there were filterParams passed in
                if(initialLoad and filterParams.length){
                    // it seems to need a timeout to allow everything to load properly before triggering
                    setTimeout(function () {
                        // loop through filter toolbar elements and set any passed-in filters. Example for a single element:
                        $('#gs_lastname').val(filterParams.lastName);
                        // set URL to basic function without any extra params
                        $("#users").jqGrid('setGridParam',{url:basicGridURL});
                        // trigger search
                        $('#users')[0].triggerToolbar();
                    }, 100);
                    initialLoad = false;
                }
            }
        });
    $("#users").jqGrid("filterToolbar", {searchOnEnter: true});

});
</script>

<table id="users"><tr><td></td></tr></table>
<div id="pager"></div>

The cfc function looks something like this: component {

/* Arguments passed in by jqGrid:
    *  1. page - page user is on
    *  2. rows - number of rows to display per page
    *  3. sidx - sort column
    *  4. sord - sort order */
    remote String function getUsersForGrid(numeric page, numeric rows, String sidx, String sord, initialLoad="false",
                                        String firstName='', String lastName='' ... additional filter params here ...
                                        String quickSearchTerm) returnformat="JSON" {
        // if this is the first load, return no data
        if(initialLoad){
            return SerializeJSON({total=0, page=0, records=0, rows=[]});
        }

        // if quickSearchTerm is passed in, run quickSearch; otherwise run regular filtered search
        if(isDefined("arguments.quickSearchTerm")){
            users = ... invoke stored proc here ...
        } else{
            //  invoke stored proc with filters - get all users that match this search
            users = ... invoke stored proc here ...
        }

        // sanitize the ordering inputs (if it doesn't match a valid value it will throw exception)
        param name="sidx" type="regex" pattern="(?i)^(?:firstName|lastName|companyname|cabgroupid|status|email|state|username)$";
        param name="sord" type="regex" pattern="(?i)^(?:asc|desc)$";

        // qoq to order by dynamic variables
        users = new Query(
            dbType="query",
            users = users,
            sql = "SELECT * FROM users ORDER BY " & sidx & " " & sord
        ).execute().getResult();

        // array to hold all users in current page
        var arrUsers = ArrayNew(1);
        // calculate start row
        var start = ((arguments.page-1) * arguments.rows) + 1;
        // calculate end row
        var end = (start - 1) + arguments.rows;
        end = (end > users.recordCount) ? users.recordCount : end;
        var totalPages = Ceiling(users.recordcount / arguments.rows);

        for(currRow = start; currRow <= end; currRow++){
            currUser = [users["lastname"][currRow],
                        users["firstname"][currRow],
                        ... additional columns here ...;
            ArrayAppend(arrUsers, currUser);
        }

        return SerializeJSON({total=totalPages, page=arguments.page, records=users.recordcount, rows=arrUsers});
    }
}
TylerH
  • 20,799
  • 66
  • 75
  • 101
froadie
  • 79,995
  • 75
  • 166
  • 235
  • It's really difficult to understand the requirements without an example. The problem is: you have an example how you use jqGrid, but there are a lot of other cases of usage. Thus it's difficult to guess how you use it. First of all your text look like you use *server side filtering* for all the grids. Do you use *server side paging* too? In case of no so large data set (<1000 or <10000 rows) it could be more effectively to return all data at once (`loadonce: true`) and make paging and filtering locally. QuickSerching can be made like in [the answer](http://stackoverflow.com/a/19728244/315935). – Oleg Dec 05 '15 at 22:46
  • Try [the example](http://www.ok-soft-gmbh.com/jqGrid/OK/performane-13-40000-20-free-jqgrid.htm) which have 13 column and 40000 rows loaded locally. The demo uses page size 20. You can see in your main web browser how long is loading the grid, sorting and filtering. On my Google Chrome it takes about 300ms. Typical roundtrip to the server inclusive processing the request to database take longer time. The usage of `loadonce: true` in *in the most cases* can simplify the server code. Only really large datasets require server side filtering and paging. – Oleg Dec 05 '15 at 22:52
  • @Oleg - loadonce takes too long for this dataset. Also, that creates difficulty with running a "quick search" against the data as the logic for that is different than for the filters. So yes, I'm using server-side filtering, and actually doing the paging in the front-end coldfusion function. – froadie Dec 06 '15 at 09:35
  • And @Oleg I'll try to post a simplified version of my code with pseudocode chunks – froadie Dec 06 '15 at 09:48
  • I can repeat that it's difficult to me to understand your question. I can post my answer on the short part of your question: "I am having a hard time navigating the jqGrid docs to figure out the best way to accomplish this. I'm not sure how to initially load the grid up with no results, or how to pass in the filters or the quick search term from the URL before loading the grid so we can pull up a predefined search." – Oleg Dec 06 '15 at 10:41
  • @Oleg - I tried to add the basics of what the code looks like with my proposed logic. Would be nice if there was a more straightforward way to accomplish the same. – froadie Dec 06 '15 at 12:46

1 Answers1

1

First of all, you ask the following question: "how to initially load the grid up with no results". The answer is very easy. jqGrid don't makes any request to the url on creating the grid if datatype has the value "local". Thus you can just use datatype: "local" during creating the grid and to change the value of datatype to datatype: "json" only before the first reloading grid from the server.

Seconds, you don't specify and mtype parameter. Thus jqGrid use default mtype: "GET". In other words, it makes HTTP GET request to the url. In the case it's not recommended to append manually any parameter to url. If you remind the call of $.ajax you knows that one can specify data parameter as object. jQuery.ajax calls internally jQuery.param which build the parameters and which encode the value using encodeURIComponent. To be exactly you should do the same. Thus the usage of object {quickSearch: quickSearch} or beter {quickSearch: function () { return getUrlParam("quickSearch"); }} as the data value can build the required URL parameter for you. The parameter postData of the grid will be forwarded to jQuery.ajax. Thus I suggest you to use

postData: {
    quickSearch: function () {
        return getUrlParam("quickSearch");
    }
}

You don't posted and HTML fragments used on the page. It's still unclear, where quickSearch control (probably some <input> element) exist on the page. I don't see any requirement to use filterToolbar in your case. The code which you posed don't contains analysis of parameters, which sent filterToolbar. Thus the existing of the filter toolbar can bring only misunderstandings. Instead of that you can create one external input element of to place the input element in the top-toolbar of jqGrid. See the demos from the answer. The demos use toolbar: [true, "top"] parameter which creates empty div on top of jqGrid. Then one uses $('#t_' + $.jgrid.jqID($grid[0].id)).append() to append or to move custom searching element(s) inside of the toolbar.

I would recommend you additionally to read the answer which shows how to use postData defined as function. What you really need to do is defining event handler which allows the user to start searching. For example you can bind keyup/ or keydown event handler to catch what the user press Enter key in the searching control. Alternatively you can add "start Searching" button near the <input> control. In any way, you should set datatype to "json" (using setGridParam for example) and trigger reloadGrid inside of the event handler. It will force jqGrid to make new request to the server using the current value from the custom searching control.

One more remark. You use SELECT * FROM users on the server. I personally prefer to have IDENTITY (AUTOINCREMENT) id column in every table of my database (see here). It creates automatically immutable integer id for every row in the table of the database. The usage of the value instead of lastName have many advantages. It could exist multiple people with the same last name. Moreover deleting one user with the lastName and creating another one with the same lastName should be not interpreted as updating the previous item. Usage of IDENTITY (AUTOINCREMENT) id don't have the problem. I personally prefer don't use array format of data (see currUser = [users["lastname"][currRow],... in your server code). It could be more difficult to handle the input value, if you would consider of include new column at the beginning of the grid (for example the column having formatter: "actions" for simplify editing the row) or if you would allow the user to change the order of columns (by using columnChooser or sortable: true option of jqGrid which allows the user to change the order of the columns by drag & drop of column headers). The usage of named properties seems to increase the size of transferred data. It's true, but the usage of gzip compression of JSON response reduce the problem and finally the flexibility in managing of the server and client code is much more important aspect of the problem.

Community
  • 1
  • 1
Oleg
  • 220,925
  • 34
  • 403
  • 798
  • I'm not sure if I fully follow your answer, but I don't think you fully understood my question... The quick search option is one that can be passed in, but cannot be actually executed against the grid once it's loaded. Once the grid is loaded, I want a filter toolbar that will allow users to search on all the columns. – froadie Dec 06 '15 at 17:20
  • Some of the points here may be helpful to me, though. I'm not sure if setting a datatype of local is the solution, though, as there are times (such as when quicksearch or column filters are passed in) that I *do* want to load up results initially. I only want to load no results if there is no quicksearchterm passed in, and no column filters passed in. – froadie Dec 06 '15 at 17:21
  • As far as postdata: how can I set up postData params that will ONLY pass in to the proc on the first load and not after any other search filters are applied in the filter toolbar? – froadie Dec 06 '15 at 17:23
  • @froadie: It would be very helpful if you post a picture (pictures) and an examples of URLs which you use. I reread your code once more and I see that you use `filterParams` URL parameter which you don't described in the text. Moreover I see in the server method the parameters `String firstName='', String lastName='' ...` and I realize that you use old style of parameters of `filterToolbar` (without usage of `stringResult: true` or `searchOperators: true`). I hope that I understand now what you need and I will update my answer later. – Oleg Dec 06 '15 at 20:07
  • @froadie: I tried to write the answer, but it seems **it's required to have an example of `filterParams`**. The value of the parameter is JSON string which will be converted to object or array (`filterParams = $.parseJSON(getUrlParam(filterParams));`). You don't test `filterParams` to `!= null`, thus it seems to exist always if `initialLoad === true`. On the other side you use `if(initialLoad and filterParams.length){...}` which is very strange (`and` instead of `&&`). The `filterParams.length` looks like `filterParams` is an array, but you use later `filterParams.lastName` like it's object... – Oleg Dec 06 '15 at 20:22
  • sorry for the confusion, it's pseudocode. `filterParams` can be passed in the URL or not; I didn't write the full code for dealing with it. It will be a JSON string of filter options (column names and values). See my 3 options that I need to accommodate that I listed at the beginning of my post - that summarizes what I need to accomplish – froadie Dec 06 '15 at 20:27
  • @froadie: It seems to me that the existence of `filterParams` can replace `initialLoad` parameter. It's important to understand that you can parse `filterParams` and fill `postData` parameter with the corresponding properties like `postData: {lastname: filterParams.lastName, ...}` You can use `datatype: "local"` if `initialLoad` is false or if `filterParams` an empty object and `datatype: "json"` if it's not so. As the result the grid will send the parameters which you need. No login in `loadComplete` is required. – Oleg Dec 06 '15 at 20:43
  • @froadie. I would recommend you to try [the demo](http://www.ok-soft-gmbh.com/jqGrid/OK/ColumnChooserAndLocalStorage2_singleSelect.htm) created for [the answer](http://stackoverflow.com/a/31663268/315935) and some previous one started with [this one](http://stackoverflow.com/a/8436273/315935). It uses `refreshSerchingToolbar` function, which I posed in [another old answer](http://stackoverflow.com/a/6884755/315935) for filling filter toolbar based on the saved filter. It seems to me that it's in general idea very close to what you need, but I used `localStorage` instead of URL parameters. – Oleg Dec 06 '15 at 20:47
  • can you set up the postData to only pass in on the first load? – froadie Dec 07 '15 at 09:53
  • @froadie: `postData` is just an object. You can modify it at any time. Using `var p = $("#users").jqGrid("getGridParam");` you get **the reference** to all parameters of jqGrid. Thus you can add new postData by usage `p.postData.lastname = filterParams.lastName;` To remove `lastname` you can use `delete p.postData.lastname;` or to set it to `""`, `undefined` or `null`: `p.postData.lastname = undefined`. The `postData` will be filled by additional properties `page`, `rows`, `sidx`, `sord` which you know. Thus you can make *any kind of modifications* of `postData` before `.trigger("reloadGrid")` – Oleg Dec 07 '15 at 10:36
  • Thanks, I ended up using a lot of details from your answer and comments to construct my final logic, which is a lot better than my initial logic outlined in the question. So I'm going to accept this – froadie Dec 21 '15 at 19:39