Skip to main content

Interactive Grid - Bulk Operation on Selected Rows

What's the Problem?

Let's say you have an IG on employee table and you want to update selected employees commission based on user's input. Ideally, it should be very simple, where you can write UPDATE statement in page process and select IG region as "Editable Region" under "Execution Options" of the page process. But, when you select rows and submit page, you can see that, this process won't get executed! The reason is 
Selection of 'Row Selector' check-boxes is not considered as row-change. Thus selected rows are not submitted to server.


So, is there any work around? 

Yes! Luckily there are lot of JavaScript (JS) APIs available to work with IG. If you are not already aware, you can refer "APEX IG Cookbook" or JavaScript API Reference documentation.

If we continue with above Employee IG example, when user selects IG rows, enters "Commission %" and clicks on "Update Commission" button, then we can write Dynamic Action (D.A.) as below.
  • Create D.A. and give it a proper name e.g. "Bulk Update Commission"
  • In "When" Section, select
    • Event: Click
    • Selection Type: Button
    • Button: UPDATE_COMISSION (In this demo, button name is UPDATE_COMISSION, you may need to change it depending on your button name)
  • True Action
    • Action: Execute JavaScript Code
    • Code: Refer below code
JS Code:
// Clear previous selections, if any
$s("P5_SELECTED_ROWS", "");
// IG static id is defined as emp
var gridView = apex.region("emp").widget().interactiveGrid("getViews").grid;
var records = gridView.getSelectedRecords();
var empNo;

// define empty JSON object to hold selected rows
var selRecords = {
	"rows": []
};

var selRecordsJSON;

if (records.length > 0) {
	if (confirm("Do you want to update commission for selected employee(s)? Please confirm.")) {
		$.each(records, function(i, r) {
			// employee number defined as PK for IG
			empNo = gridView.model.getRecordId(r);
			// you can also get other columns values using below code
			// ENAME is column name in IG query
			//empName = gridView.model.getValue(r, "ENAME");
			// Push selected rows into an array
			selRecords.rows.push({
				"empNo": empNo
			});
		});
		// convert JSON into string
		selRecordsJSON = JSON.stringify(selRecords);
		console.log(selRecordsJSON);
apex.page.submit({ request: "UPDATE_COMMISSION", set: { "P5_SELECTED_ROWS": selRecordsJSON }, showWait: true }); } } else { alert("Please select atleast one employee."); }

Note: When creating IG region, ensure correct column is selected as "Primary Key" column. If you have selected "ROWID" as Primary Key column, then above JavaScript code getRecordId will return ROWID value, not the primary key column value. In such cases, you need to modify server side PL/SQL (discussed below) accordingly, i.e. update variable to use ROWID type, change UPDATE statement to use ROWID column etc. If you use ROWID as Primary Key and use below PL/SQL code as is, then you will get "ORA-06502: PL/SQL: numeric or value error: character to number conversion error "

This code loops through selected rows and extracts Primary Key (PK) value for each row and pushes this value into an array. At the end, all these PK values are converted into JSON and it's assigned to hidden page item. Then, the page is submitted.

As we want to execute above D.A. when "Update Commission" button is clicked, we should set button's action (Button > Properties > Behavior > Action) to "Defined by Dynamic Action".


And finally, we can create regular PL/SQL page process at "Page Processing > Processing" section which execute below code. Make this process conditional based on request value used in above JavaScript code.


PL/SQL Code:  
DECLARE
    l_empno     emp.empno%type;
    l_values    apex_json.t_values;
    l_row_count PLS_INTEGER;
BEGIN
    apex_debug.message('inside process Bulk Update Commission');
    -- parse text to JSON
    apex_json.parse(p_values => l_values, p_source => :P5_SELECTED_ROWS);
    -- get number of rows selected
    l_row_count := apex_json.get_count(p_path => 'rows', p_values => l_values);
    -- loop through the selected rows
    FOR i IN 1..l_row_count LOOP
        -- get Primary Key value from JSON data
        l_empno := null;
        -- if primary key column type is not a number type, then remove TO_NUMBER part
        l_empno := TO_NUMBER(apex_json.get_varchar2(p_path => 'rows[%d].empNo', p0 => i, p_values => l_values));
        apex_debug.message('l_empno' || l_empno);
        -- update employee commission
        UPDATE emp
        SET comm = sal*TO_NUMBER(:P5_COMMISSION)/100
        WHERE empno = l_empno;
    END LOOP;
END;

In this example, we have updated the salary for selected employees. Once we get list of records selected in the IG, then we can do what ever we want based on our requirement. Other typical use case would be to add selected records into APEX Collection, so that we can process these records later. In such cases, we can use APEX_COLLECTION APIs to add selected records into a collection.

Here is the link to working example.

Please note, this works well with pagination type Scroll. But, if you are using pagination type as Page, then getSelectedRecords gives rows selected from current page alone. In these cases, we need to set persistSelection to true. To do this, go to "IG > JavaScript Initialization Code" and put following code.

function(config) {
    config.defaultGridViewOptions = {
        persistSelection: true
    };
    return config;
}

Thank you!

Comments

adelinnep said…
Hi Hari,

I'm trying to reproduce your example, but i'm having: ORA-06502: PL/SQL: numeric or value error. I supposed that's from the conversion:
l_empno := TO_NUMBER(apex_json.get_varchar2(p_path => 'rows[%d].empNo', p0 => i, p_values => l_values));
The rows that i’m getting from javascript console log are something like this:
{"rows":[{"empNo":"[\"10\",\"7839\"]"},{"empNo":"[\"30\",\"7698\"]"}]}
If you can guide me..i’m not sure how to debug this..
Thanks,
Adelina.
Unknown said…
I'm getting the same error. I don't get the value of P5_SELECTED_ROWS
Olivier said…
An other option would be the "Save Interactive Grid Data / Interactive Grid Automatic Row Processing (DML)" process, which gets automatically created when you make an Interactive Grid editable.
hari said…
Hi Olivier,

If you just select rows and submit pages, then those rows won't be visible in "Save Interactive Grid Data / Interactive Grid Automatic Row Processing (DML)" process. Because, row selection is not considered as data change. So selected rows are not submitted to server.

You may try getting this (bulk operation on selected rows) done using automatic DML process created for IG and let me know if it works. Thank you.
hari said…
Please set-up an example at apex.oracle.com and let me know the details. I can have a look. As you can see in the demo link above, this code is working fine there.
Robin said…
Hi. This is EXACTLY what I am looking for. Can you tell me how to get behind the scenes on this app so I can examine it in detail?
Robin said…
Your workspace on apex.oracle.com for app 103639
Robin said…
I won't need your app after all. I figured it out using your tips. Again, thank you.
hari said…
I am glad that it helped you.
123 said…
It's works fine :)
YuvaRenuka said…
Exception in "ajax_execute_plsql_code":
Error Stack: ORA-06502: PL/SQL: numeric or value error: character to number conversion error
Backtrace: ORA-06512: at line 15

I am getting this error message. plz advise.
Hari said…
Hi, What is the data type of primary key column of IG table? If it's is VARCHAR2 column, then you need to remove TO_NUMBER conversion part in your PL/SQL code. If it's a NUMBER column and still you are getting an error, then please set-up an example at apex.oracle.com and provide me developer account credentials (temporarily). I can take a look.
YuvaRenuka said…
I fixed the issue. it works fine now:-). Thanks for your reply.

I am facing another issue. Data gets updated only second time when I click the submit button.
Hari said…
Hi, Please share developer credentials so I can review your application in APEX Application Builder.

Alternatively, you can use APEX_DEBUG package to log few entries from your PL/SQL code, enable debug and submit the page. Then review the debug output. This can help you to figure what is going on.
Hari said…
Hi, You have created PL/SQL process in D.A. This is wrong. You need to create PL/SQL Page process in "Page Processing > Processing" section. Please check Page 2 in Application 63562 in your workspace.
YuvaRenuka said…
Thanks for your help. it works fine. I owe you a big Thanks :-).
jorge.criv said…
It works but only for the current page,
That is, if I have several pages, it processes the selected ones on the page where I am located.
Is there a way to process all selected rows from all pages?
Veroniquemn said…
Hi Hari, I have been saved the selected rows to the hidden field. However , the PL/SQL in the page process does not get executed at all. I have reviewed the debug output , the debug messages are not there / Pls advise
Hari said…
Hi, Page process should be a normal page process defined at "Processing" section of page. Just for testing, remove server side condition for the page process and see the debug output. And, before clicking on button, ensure you have enabled the debug (default level info) and then check debug log after page submit.
Veroniquemn said…
Thanks a lot Hari ! it works after we removed all the branching..Many thanks.
Hari said…
Hi Jorge,

You are correct. It works well with pagination type "scroll". But, if you are using pagination type as "Page", then getSelectedRecords gives rows selected from current page alone. In these cases, we need to set persistSelection to true. I have updated blogpost with these details.

Thanks you.

Raj said…
Hi Hari,
I have added the PL/SQL Page process in "Page Processing > Processing" but for some reason I can't see the update happening. Hence, I added the pl/sql code to execute from DA . But I have noticed that we have to click the update button twice to get all the records updated . And the same I noticed because the p5_selected_rows item doesn't capture all the rows in first click but captures all the records from the IG in second click .
Any idea what I could be missing in page -> process or why the clicking of button twice works.

Thanks
Hari said…
Hi Raj,

Please ensure Button's "Action" is selected as "Defined by Dynamic Action". I have updated the blog with more details. Please check and ensure everything is done as explained in this blog. If you are still facing issues, please set-up an example at apex.oracle.com and share me your workspace details along with developer username/password. I can check it.

Raj said…
Thank You Hari.
It works now fine.
aura said…
Hi, I have a question, how many records can be added into the JSON Object. I tested and I only get 100 records, is there a way to increase this? Thanks!
Hari said…
Hi Aura,

There is no limit for JSON object size. However, we are converting JSON object to string and passing it to server via page item P5_SELECTED_ROWS. Here, AFAIK, we have limit of 32k for page items.

If you are using scroll pagination, by default, fetches 50 rows at a time. Looks like you have used "Select All" option (header checkbox) when rows fetched so far is 100. In JS code, comment apex.page.submit call and do

console.log(selRecords.rows.length);

With this you can see selected record count in the console.
geis815 said…
Hi Hari! Thank you for posting an awesome example. I am very much wanting to implement this, but I am confused with where the Ref Cursor "l_empno emp.empno%type;" is defined/coming from. Any help would be greatly appreciated!
Hari said…
Hi geis815,

l_empno emp.empno%type;

This is not ref cursor. Here I am declaring variable l_empno and specifying that it's data type will be same as that of empno column in emp table. Search in google for oracle pl/sql %type to get more details.

Regards,
Hari
geis815 said…
Ahhh! I see. Thank you very much Hari!
Reena Joshi said…
hi Hari,
Thank you for your post. I followed it and it worked as expected.
I have only 1 query, in addition to selected rows, i also need the user to select value of 1 column in each selected row, which will specify the type of relation it is creating for selected row. How can i pass that info .Having trouble capturing the info of user selected column.
Appreciate your help.
Thanks.
Hari said…
Hi Reena,

Example is already given in the JavaScript code, to read additional columns data. Read inline comments.


// you can also get other columns values using below code
// ENAME is column name in IG query
//empName = gridView.model.getValue(r, "ENAME");


Next, you can pass empName as-well to server just like empNo

selRecords.rows.push({"empNo": empNo, "empName": empName});

Next, you can access this column value on server side as below
l_empname := apex_json.get_varchar2(p_path => 'rows[%d].empName', p0 => i, p_values => l_values);
Skip S said…
Hari,
Thanks so much for an excellent tutorial. It helped me out a lot.

Skip
vishal said…
Hi Hari,
I am facing an error : Error: ORA-06502: PL/SQL: numeric or value error
I am trying to add the selected row to another table that I created, and delete the selected row from the current table. However the values are being printed on the page element P2_Store_DATA, so I guess JS code is working fine.
Also if you can please let me know how can I track the changes in the pl/sql code in oracle apex. I mean where are the printing statements like: DBMS_OUTPUT.PUT_LINE(row_cnt) is printed??


Here is pl/sql code.

BEGIN
apex_json.parse(p_values => l_values, p_source => :P2_STORE_DATA);
row_cnt := apex_json.get_count(p_path => 'rows', p_values => l_values);
DBMS_OUTPUT.PUT_LINE(row_cnt);

FOR i IN 1..row_cnt LOOP
nums := null;
nums := TO_NUMBER(apex_json.get_varchar2(p_path => 'rows[%d].num_id', p0 => i, p_values => l_values));
apex_debug.message('nums' || nums);

INSERT INTO EXCLUSION_SHEET(NUM,EMAIL,GID,STATUS)
SELECT NUM,EMAIL,GID,STATUS from DAT where Id = nums;

DELETE from dat where Id = nums;
END;
Hari said…
Hi Vishal,

As discussed on the blog post, ensure primary key column is a number. If it's not a number (or ROWID selected as primary key), then you will get this error.

You should use apex_debug package for debugging in APEX. You can watch this video to know more about debugging.

Regards,
Hari
Luz M. said…
Thank you so much!
Shraddha O. said…
Hi!
Really amazing content so thanks so much!
I just want to know how can we implement this solution with a custom button defined in the toolbar (either via JS or as a plugin)?
How can we address that button's name in the DA?
Thanks again!

Popular posts from this blog

Interactive Grid - Conditional Enable/Disable

In this blogpost, I am going to discuss few approaches using which we can conditionally enable/disable Interactive Grid (IG) column(s) based on other column(s) values. Note:    There is a bug  30801170  in APEX 19.2/20.1 with respect to "enable/disable" dynamic actions for IG columns. Workaround for this bug is provided at the end of this blogpost . This bug has been fixed in APEX version 20.2. Client Side Only Conditions If conditions to enable/disable are simple, then we can check those conditions easily on the client side. For e.g. let's consider IG on EMP table with following SQL Query. SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, SAL, COMM, DEPTNO FROM EMP Let's consider the requirement as - Enable "Commission" column only when JOB is equals to 'SALESMAN' and disable "Commission" column in all other cases. This can be done declaratively using dynamic actions (DA) DA "Enable/Disable Commission - 1" Create DA and give it a prope

Interactive Grid - Process Filtered Data on Server Side

Recently one of the APEX developers has reached out to me and asked if it's possible to capture filtered rows data of the Interactive Grid on the server-side and do some processing. In APEX 20.1, there is a new API APEX_IG , using which we can achieve this. Photo by Jakub Kapusnak on Unsplash The approach is very simple and straightforward. Get the internal region id based on the Static ID given for the IG region Get the last viewed report id based on region id Open query context for the region and report using region id and report id Fetch and loop through the rows using the query context Do something with fetched rows And finally, close the query context If you have already done this for interactive reports, then you should be already aware of these steps. The only difference here is, we use APEX_IG APIs instead of APEX_IR APIs. For the demo purpose, let's Build an Interactive Grid on EMP table and let's give it a Static ID as emp Create a Textarea page item with