How to hack APEX Interactive Grid Part 4

In this fourth and final part of the series I’ll cover events and working with the data model. I’ll assume you have read parts 1, 2, and 3 and have at least intermediate level experience with APEX and JavaScript. A very important point is that the information in this article applies to APEX version 5.1.1. Some things may work in 5.1 but I’m not going to bother distinguishing which. Anyone programming Interactive Grid should move to 5.1.1 as soon as possible.

Events

Part 3 covered a number of things you could do to control IG using actions or methods but often you want to do something in response to what the user does and that is where events come in. In addition to the standard browser events, UI widgets generally add their own higher level events. For example any widget that has the concept of a current selection should have a selection change event.

Most of the newer APEX components are implemented as jQuery UI based widgets. Widgets based on the jQuery UI widget factory have a common pattern for event handling where the event can be handled as either a callback function or an event. You can learn about this from the jQuery UI widget factory documentation. On an APEX page it is generally better to use the event rather then the callback unless you are directly creating the widget from JavaScript, which you don’t do for IG widget. What little information there is about the APEX widget events can typically be found in the source file comments after the event callback options property. The full event name is the name of the widget plus the event. For example the pagechange event of the grid widget is gridpagechange. The full name can be used as a dynamic action Custom Event or with the jQuery on method.

The most commonly used widget events are exposed as Dynamic Action component events. For IG there are presently two such events: Selection Change and Row Initialization.

The Selection Change Dynamic Action event (full event name: interactivegridselectionchange) fires any time the selection changes. This applies to any of the views that support selection, which is currently grid view including single row view and icon view. The underlying widget that implements the view such as the grid widget has its own selection change event but using the IG selection change event is preferred because it works for any of the views that support selection.

The Sample IG app has a good example using the selection change event on the Reporting: Multiple Selection page. The selection change event provides the apex.model interface used by the view as well as an array of selected model records. The event handler uses the model to set a hidden page item to the list of selected employee numbers and refreshes the chart region. The chart region uses the hidden page item to select which employees to include in the chart.

The Row Initialization Dynamic Action event fires when a row becomes active for editing. (This event only applies to editable interactive grids.) This happens after all the column items have been initialized from the model. The event is called apexbeginrecordedit, it is not prefixed with any widget name and is not a callback. It is not implemented by the IG widget but rather the table model view base widget, which is a base class used by other widgets such as grid.

One use for the row initialization event is to provide complex default initialization for newly added rows. The event is given the model, record, and record id of the active row. You can tell if the active row is inserted by looking at the record metadata.

The following example will set the default value of the JOB column based on the current value of a page item called P1_DEFAULT_JOB. To be clear this is different from specifying Default: Type = Item and then picking an item, in which case the default comes from the session state at the time the page is rendered. The examples are using the EBA_DEMO_IG_EMP table from the Sample IG app to make it easy for you to try them out by modifying that app.

Create a dynamic action for the Row Initialization event on the IG region. Add a JavaScript action. Set Fire on Initialization to No because that is what this event is already doing. Add the following JavaScript code.

var model = this.data.model,
    rec = this.data.record,
    meta = model.getRecordMetadata(this.data.recordId);

if ( meta.inserted ) {
    model.setValue(rec,"JOB", $v("P36_DEFAULT_JOB"));
}

This technique of setting defaults can be used to work around an issue where columns that use a column type with distinct display and return values and with a static default end up showing the static return value for inserted rows rather than the display value. This is a common issue with Switch column types. For example on added rows they show N rather than No. To solve this issue add a row initialization dynamic action similar to the previous example but with this JavaScript code.

var val,
    model = this.data.model,
    rec = this.data.record,
    meta = model.getRecordMetadata(this.data.recordId);

if ( meta.inserted ) {
    val = model.getValue(rec, "JOB")
    if ( val.v === val.d ) {
        model.setValue(rec,"JOB", {d:apex.item("C_JOB").displayValueFor("CLERK"), v: "CLERK"});
    }
    val = model.getValue(rec, "ONLEAVE"); 
    if ( val.v === val.d ) {
        model.setValue(rec,"ONLEAVE", {d:apex.item("C_ONLEAVE").displayValueFor("N"), v:"N"});
    }
}

The JOB column is a select list and the ONLEAVE column is a switch. For the above to work the JOB and ONLEAVE columns need static ids C_JOB and C_ONLEAVE respectively. Also the columns must not be given any static default.

This shows that the value of a model column that has a display value is actually an object with two properties: v is the return value and d is the display value.

There is also an apexendrecordedit that is fired after the active row is done being edited. This happens after the column item values have been validated and saved back to the model.

One common point of confusion is that the IG widget never fires the Before Refresh (apexbeforerefresh) and After Refresh (apexafterrefresh) events. These events apply to components that refresh by getting completely new markup from the server. IG is more complex than that. It makes a number of different kind of requests to the server including fetching new data as JSON, saving report setting changes and saving changes. The semantics of apexbefore/afterrefresh are not rich enough to handle the different kinds of requests that IG makes.

So here are the events that IG does fire, what they do, and what you might use them for.

  • viewchange: This event fires when the view changes. The data object has property view which is the new view id such as “grid” or “chart” and property created which is true if the view has just been created. The IG is free to destroy and create views whenever it wants so if you do anything to configure or control the view it should be done in response to this event after checking the view and created properties. I showed examples of this event in part 2. You can use this event to show or hide other content on the page when the view changes.
  • viewmodelcreate: This fires when the model used by a view is created. The data object has two properties viewId and model. The IG is free to destroy and create models as needed. If you need to do anything with the model such as establishing a notification listener then you should handle this event.
  • save: This event fires after the IG has saved. The data object has one property called status which is a string. The string can be “success”, “error” for validation errors, or “fail” for some failure with the ajax call. Note this is only fired if the IG is saved via the save action. Submitting the APEX page will also submit the IG models but does not fire this event. If you use the model APIs directly to save then again this event is not fired. The model save APIs return a promise that you can use. This event is useful if the IG is in a modal dialog page that is not submitted and you want to wait until the save is complete before closing the modal dialog. It is also useful if there is any other content on the page that may need to be refreshed after the IG data is saved.
  • reportchange, reportsettingschange: These events fire when the report or report settings change. Because APIs to access report settings either don’t exist or are not stable there isn’t much you can do with these events at this time.

The view widgets may have their own events. Many of the events are handled by the IG widget and are not very useful. The grid view widget has a modechange event that fires when switching between edit and navigation mode. The grid widget and tableModelView widget (icon and detail views) have a pagechange event which fires any time a new page of data records are rendered. The pagechange event has data properties offset and count. These pagechange events are probably the closest thing to apexafterrefresh fired by the Interactive Report region.

Most of the time event handlers are established with Dynamic Actions or with JavaScript code added to page attribute Execute when Page Loads. This is fine for most events but for events that fire while a widget is being created the handler will not be called. This is true for the create event that all jQuery UI based widgets have as well as the viewchange and viewmodelcreate events. To catch the first time these events fire you must establish the event handler before the IG widget is created. An example of how to set up an event handler before regions (or other components) are initialized is shown in the next section. This technique is not specific to IG.

Model

The model is implemented by namespace apex.model. You can read background information about the model here. For full details on the API you need to read the doc comments in the source file model.js.

Models store and manage the table shaped data used by the views of Interactive Grid. The data is an ordered collection of records with each record made up of fields; also referred to as rows and columns. In addition to the record data the model may store metadata about each record and field. Metadata includes things like highlight data, and change state like deleted, inserted, and updated.

Models have an id. The apex.model namespace has functions to manage model instances (also simply called models when there is no confusion with the apex.model namespace). It can create, list, get, release, save and more. Models are reference counted so if you call apex.model.get you must call apex.model.release when you are done. When working with IG models you shouldn’t have to worry about that because you will get a reference to the model from the IG. We have seen this above where the selection change or row initialization events give you the model instance. You can also get the model instance from the IG view. For example in part 2 we saw code like this

...
var view = apex.region("emp").widget().interactiveGrid("getViews", "grid")
...
view.model.getValue(record, "ENAME")

When using the model instance from IG like this there is no need to call apex.model.get or apex.model.release. Also you should not hold on to or store the model instance for later use. Always get it from the IG otherwise you could be referencing a model that no longer exists.

You can see all the models in use on a page with this code typed into the JavaScript console.

apex.model.list();

Try the above on the Sample IG app Master Detail page. See how the list changes as you click on different master IG records.

When the data in the model changes all views using that model get notified so that they can update the data in the view. This is done using the observer pattern with subscribe and unSubscribe methods. This is why it is important to use model instance methods such as deleteRecords and setValue rather than trying to directly modify the data in the model’s internal data structures. You can find a description of all the model notifications in a comment at the top of the model.js file.

I’m not going to explain each and every model method. Instead I’ll show two examples. Both examples use the employee table from the Sample IG app and you should be able to work this code into appropriate pages in that app.

This first example will calculate the total of the Salary (SAL) column and put the total in a page item P1_TOTAL. It updates the total anytime the model changes. It knows to ignore deleted and aggregate records. It correctly registers a model notification listener anytime a model is created.

// create a private scope where $ is set to apex.jQuery
(function($) {
    // This is the function that calculates over all the rows of the model and then
    // updates something else.
    // Call this whenever the model data changes.
    function update(model) {
        var salKey = model.getFieldKey("SAL"), 
            total = 0;

        model.forEach(function(record, index, id) {
            var sal = parseInt(record[salKey], 10),  // record[salKey] should be a little faster than using model.getValue in a loop
                meta = model.getRecordMetadata(id);

            if (!isNaN(sal) && !meta.deleted && !meta.agg) {
                total += sal;
            }
        });
        $s("P1_TOTAL", total);
    }

    //
    // This is the general pattern for subscribing to model notifications
    //
    // need to do this here rather than in Execute when Page Loads so that the handler
    // is setup BEFORE the IG is initialized otherwise miss the first model created event
    $(function() {
        // the model gets released and created at various times such as when the report changes
        // listen for model created events so that we can subscribe to model notifications
        $("#emp").on("interactivegridviewmodelcreate", function(event, ui) {
            var sid,
                model = ui.model;

            // note this is only done for the grid veiw. It could be done for
            // other views if desired. The important thing to realize is that each
            // view has its own model
            if ( ui.viewId === "grid" ) {
                sid = model.subscribe( {
                    onChange: function(type, change) {
                        if ( type === "set" ) {
                            // don't bother to recalculate if other columns change
                            if (change.field === "SAL" ) {
                                update( model );
                            }
                        } else if (type !== "move" && type !== "metaChange") {
                            // any other change except for move and metaChange affect the calculation
                            update( model );
                        }
                    },
                    progressView: $("#P1_TOTAL") // in theory this will cause a spinner on this field but I don't see it.
                } );
                // if not lazy loaded there is no notification for initial data so update
                update( model ); 
                // just in case fetch all the data. Model notifications will
                // cause calls to update so nothing to do in the callback function.
                // can remove if data will always be less than 50 records
                model.fetchAll(function() {});
            }
        });
    });
})(apex.jQuery);

Don’t make assumptions about what the index of a field in the record array is. Use getValue or getFieldKey to get the field by column name. To be really good don’t even assume the record is an array.

The second example is a function that will increase the salary by a given percent for all the currently selected records. Using what you learned in part 2 you could call this function from an action associated with an IG toolbar button or a selection action menu item.

function increaseSalary(percent) {
    var i, records, record, sal, model,
        view = apex.region("emp").widget().interactiveGrid("getCurrentView");

    if ( view.supports.edit ) { // make sure this is the editable view
        percent = percent / 100;
        model = view.model;
        records = view.getSelectedRecords();
        if ( records.length > 0 ) {
            for ( i = 0; i < records.length; i++ ) {
                record = records[i];
                sal = parseFloat(model.getValue(record, "SAL"));
                if ( !isNaN(sal) ) {
                    sal = sal + sal * percent;
                    model.setValue(record, "SAL", "" + sal);
                }
            }
        }
    }
}

You must call setValue to modify the record so that any views showing that record will be notified about the change. This example assumes that all the records can be edited. If some records are readonly then you should use the allowEdit method to check if it can be edited first. For example: if ( !isNaN(sal) && model.allowEdit(record) ) {...

Most of the options of a model are determined by declarative IG attributes and handled automatically. If you find a need to set some obscure model option you can do so with the IG config option defaultModelOptions in the JavaScript Code attribute.

Summary

This series has covered a lot of ground; configuration, menu and toolbar customization, defining actions, control using actions and methods, events, and the data model layer. But it is far from complete. There is just so much that is possible with Interactive Grids. This is not a reference but I have shown where and how to find more information. It is also not a tutorial or worked example but I expect these to pop up like this one. Remember the APEX forum is a great place to ask questions and find answers. I hope these articles help you create awesome APEX apps.

3 thoughts on “How to hack APEX Interactive Grid Part 4

  1. Thank you. You have a full-fledged demo application where dismantled all aspects of IG. In particular interested in saving to a data table from the grid, if the grid was constructed on the “view” from multiple tables?

  2. Hello,
    and how to get currently displayed value for a switch or a select list ?
    After a page is rendered every switch or select list returns ‘-‘ where there ‘-‘ is a first value in a LOV assigned to them, however on the rendered page you can see different values:

    var view = apex.region(“P01”).widget().interactiveGrid(“getViews”, “grid”);
    view.model.getRecordValue(13, “A1”);
    Object { v: “-“, d: “-” }

    or

    view.model.getFieldKey(“A1”);
    5
    var rec = view.model.getRecord(13);
    undefined
    rec[5]
    Object { v: “-“, d: “-” }

    Regards.

Leave a Reply

Your email address will not be published. Required fields are marked *