Declarative Tabular Form dynamic totals
A common APEX project is to take a customer’s existing spreadsheet-based solution and convert it more-or-less as is into APEX. I’ve got one going at the moment, a budgeting solution where users need to enter their budget requests. They currently enter their requests into an XLS template file which generates subtotals and totals for them.
To do this in APEX I’m going to use a tabular form, and to do the subtotals I’ll use jQuery in a way not too dissimilar to that I described earlier.
Here is a mockup of the screen so far:
There are column totals that need to be added up and updated dynamically (indicated by the green arrows) as well as subtotals within each row (indicated by the red arrows).
I started by looking at the generated items, getting their ids (e.g. “f09_0001” etc) and writing the jQuery code to detect changes, add them up, and put the totals in the relevant items. I then started repeating this code for each column, and thought “hmmm”.
There were two problems with this approach that I could foresee:
- The generated ids in a tabular form can change if the structure of the query changes – e.g. what was f08 + f09 => f10 might change to f09 + f10 => f11
- I was aware of another form that I would need to build, with a similar structure except that there will be two sets of “Jan-Jun” + “Jul-Dec” columns, each with their own subtotal.
I wanted a more declarative solution, so that the heavy lifting will be done in one set of generic javascript functions, and I simply need to put attributes in the relevant columns to activate them. This is how I’ve approached this:
- Create the tabular form as usual (mine is based on an APEX Collection) and remove the standard DML processes, replaced with my own that calls
APEX_COLLECTION
instead. - Create a standard report that generates the total items by calling
APEX_ITEM.text
, withp_attributes=>'data-total="x"'
(with a different “x” for each column, e.g. year1). - Set the Static ID on the tabular form region (e.g.
tabularform
). - Set Element Attributes on the Jan-Jun column to
data-cell="year1" data-col="year1_jan_jun"
, similarly for the Jul_Dec column. - Set Element Attributes on all the Year columns in the tabular form to
data-col="yearx"
, where x is 1..5. - Set Element Attributes on the total for the first year to
data-subtotal="year1"
.
The following is the query for the totals report region:
select APEX_ITEM.text(1, TO_CHAR(SUM(year1_jan_jun),'FM999G999G999G999G990D00'), p_size=>10, p_maxlength=>2000, p_attributes=>'disabled=true class="edit_money" data-total="year1_jan_jun"') as year1_jan_jun ,APEX_ITEM.text(2, TO_CHAR(SUM(year1_jul_dec),'FM999G999G999G999G990D00'), p_size=>10, p_maxlength=>2000, p_attributes=>'disabled=true class="edit_money" data-total="year1_jul_dec"') as year1_jul_dec ,APEX_ITEM.text(3, TO_CHAR(SUM(year1_total),'FM999G999G999G999G990D00'), p_size=>10, p_maxlength=>2000, p_attributes=>'disabled=true class="edit_money" data-total="year1"') as year1_total ,APEX_ITEM.text(4, TO_CHAR(SUM(year2_total),'FM999G999G999G999G990D00'), p_size=>10, p_maxlength=>2000, p_attributes=>'disabled=true class="edit_money" data-total="year2"') as year2_total ,APEX_ITEM.text(5, TO_CHAR(SUM(year3_total),'FM999G999G999G999G990D00'), p_size=>10, p_maxlength=>2000, p_attributes=>'disabled=true class="edit_money" data-total="year3"') as year3_total ,APEX_ITEM.text(6, TO_CHAR(SUM(year4_total),'FM999G999G999G999G990D00'), p_size=>10, p_maxlength=>2000, p_attributes=>'disabled=true class="edit_money" data-total="year4"') as year4_total ,APEX_ITEM.text(7, TO_CHAR(SUM(year5_total),'FM999G999G999G999G990D00'), p_size=>10, p_maxlength=>2000, p_attributes=>'disabled=true class="edit_money" data-total="year5"') as year5_total from budget_collection_vw
So, to summarise: all the data-cell
items get totalled to the data-subtotal
item in the same row; and all the data-col
items get totalled to the data-total
item below the tabular form.
To do all the hard work, I’ve added the following code to my page’s Function and Global Variable Declaration:
function getSum (qry) { //get the sum over all items matching the given jQuery search criterion var t = 0; $(qry).each(function() { t += parseFloat($(this).val().replace(/,/g,''))||0; }); return t; } function updateSubTotal (item) { // update a row-level subtotal // the items to add up are identified by data-cell="x" // the item to show the total is identified by data-subtotal="x" var cell = $(item).data("cell") //get the data-cell attribute ,rn = $(item).prop("id").split("_")[1] ,t = getSum("input[data-cell='"+cell+"'][id$='_"+rn+"']"); // we need to temporarily enable then disable the subtotal // item in order for the change event to fire $("input[data-subtotal="+cell+"][id$='_"+rn+"']") .val(t.formatMoney()) .prop("disabled",false) .trigger("change") .prop("disabled",true); } function updateTotal (item) { // update a column total var col = $(item).data("col") //get the data-col attribute ,t = getSum("input[data-col='"+col+"']"); $("input[data-total="+col+"]") .val(t.formatMoney()) .trigger("change"); }
In case you’re wondering, I’m re-using the formatMoney function here.
There’s a number of things happening here. On page load, we add a listener for changes to any input item that has a data-cell
attribute; this calls updateSubTotal
, which detects the row number for the triggering item, adds up all the values for any input item that has the same data-cell
value; and puts the total in the input item with a matching data-subtotal
attribute.
We also have a listener for changes to any item with a data-col
class; when these are changed, updateTotal
adds up any item with the same attribute, and puts the total in an item with attribute data-total
.
The jQuery selector [id$='_"+rn+"']
makes sure that the row-level code only finds items ending with the given row number (i.e. '*_0001'
).
The benefit of this declarative approach is that it is much easier to re-use and adapt.
EDIT: fixed the change trigger so that I don’t need to call updateTotal from updateSubTotal.
Elke
6 April 2016 - 11:09 pm
nice solution.
but in my case, on save the cells and the totals are empty
how can I set the session state?
Thanks.
Jeffrey Kemp
6 April 2016 - 11:34 pm
Hi Elke,
When the user submits the page, you need a page process that handles it somehow. What this is depends on what your tabular form is based on. If it’s an ordinary table, you need to do the inserts/updates/deletes on that table. If it’s based on an Apex collection, you need to call the relevant procedures in the APEX_COLLECTION package. Once that’s done, if the user returns to that page, when it loads the tabular form it will read the new (saved) values.
The easiest way to do this is with a Tabular Form process (one that is linked to your tabular form) – this process will be called once for each record that needs to be inserted or updated.
Enrique
15 April 2016 - 1:10 pm
Hello Jeffrey,very nice post! is this oracle apex 5?
Jeffrey Kemp
15 April 2016 - 1:11 pm
This site is currently on Apex 4.2.4, will be upgrading later this year.