A Tabular Form provides a way to display, create, edit and delete multiple records using a grid. With Tabular Forms you can edit and change multiple records at once, without having to go back and forth as in a normal single record form.
How did Tabular Forms evolve? The feature has been part of APEX from the very beginning. Until version 4.0, however, there were no major changes in the way Tabular Forms operate. A wizard was available to lead users through the creation of a Tabular Form. This wizard would create a simple editable report. In addition to the report, it would create some buttons required for saving or discarding changes and four processes for creating, updating, and deleting data. That was basically it. If you needed more, you had to create your own code.
With APEX 4.0 came the first major changes to Tabular Forms since APEX was developed. These include
New item types (Single Checkbox, jQuery Date Picker, Radio Group, Popup Key LOV).
Client-side Add Row capability.
Validations for Tabular Form columns.
Lost update protection. Finally it is possible to validate a Tabular Form and show the error message on the same screen without losing the updates.
Reduced number of processes required for a Tabular Form (two instead of four).
Some other features, which we will mention later in this chapter.
APEX and its wwv_flows package provides 50 predefined PL/SQL arrays for Tabular Form operations. You can reference these arrays using the following syntax:
wwv_flow.g_f01 ... wwv_flow.g_f50
or
apex_application.g_f01 ... apex_application.g_f50
Every updatable column in a Tabular Form will have a unique ID in a sequential order (the SQL statement) mapped to one of these arrays:
f01_0001 ... f01_n
The limit of 50 arrays is a major constraint with Tabular Forms. Currently, a Tabular Form will allow users to update or create a maximum of 50 columns per page. The maximum number of columns an Oracle table can have is 1024. Thus, you can find yourself in a position where a table has more columns than you can display on a page.
You will receive the same error if you try building manual Tabular Forms and index a column outside of the specified range. The error is similar to the error you get if you try to create a simple form with more than 100 items per page, as shown in Figure 3-1.
Keep in mind that new item types available in APEX 4 (Simple Checkbox and Popup Key LOV) will require two of these IDs. Therefore, the limitation of 50 updatable columns per page may vary depending on how many of those elements you have.
The main purpose of a Tabular Form can be described in two cases:
To maintain smaller sets of data
To maintain parent/child relations
A typical example of the first case would be a page in your application where you maintain lists of values. Normally a list of values will contain a couple of records (options), and you could display this set of data on one page. The reason for choosing this method is so that you can quickly edit and save records without having to drill down, paginate, or switch between the pages.
An example of the second case would be an application for order management. An order would be a parent record (master) and ordered items would be child records (detail). In most cases, there is a limited (small) amount of details for one master record. Normally you would want to maintain that relation between the master and the detail on one page. In that case, Tabular Forms are the way to go.
You can even use Tabular Forms to update thousands of records with up to 50 columns each. In that case, however, you will face several issues:
Application performance will go down. APEX will call the apex_item
package for each row and each column displayed on your page. (It will be even worse if you build Tabular Forms manually since this will happen not only for displayed rows but for the whole result set.) The time required to render a page after an update process will increase dramatically.
The quality of data will suffer. Users will lose the "big picture" of their data and will eventually need to remember their changes rather than see them, since they need to scroll the screen in all directions.
The safety of data will be poor. It is quite easy to accidentally delete records without even noticing it.
One of the most frequent misunderstandings about Tabular Forms is that you can use them as a replacement for Excel spreadsheets. At least one-third of the questions posted in the Oracle APEX forum related to Tabular Forms concern this issue. The fact is, to a certain extent you can modify Tabular Forms in order to simulate the behaviour of an Excel spreadsheet. You can add some JavaScript and AJAX to it and make it behave like an Excel grid. However, very soon you will realize that there are many limitations to this solution and that your code is exponentialy growing for every functionality you add. What you definitely do not want is to have to support and debug that code later. Tabular Forms are not meant to replace Excel—they have a completely different purpose.
As mentioned earlier, the first set of major changes to Tabular Forms happened in release 4 of APEX. Some very important features were added and enhanced. We will try to cover them all.
In prior releases of APEX, Tabular Forms were somewhat limited compared to the single row Forms. A couple of important items were missing. Release 4 corrected that issue by introducing the following item types:
Single Checkbox
jQuery Date Picker
Radio Group
Popup Key LOV
In some cases your Tabular Form will need to provide a column of type Checkbox to give your users a possibility to "flag" a record. Usually it will be "Yes" or "Y" for an activated checkbox and "No" or "N" for the not checked (empty) state. In earlier versions of APEX such a requirement would cause a lot of coding just to create a workaround for a simple problem. The Checkbox item is different than the other items in APEX—its value will not be submitted to the server for the empty state. This means that the array (g_f01
) would contain the values for the checked items only; there was no really good way to get around this problem.
APEX 4 changed this for the better by introducing the Single Checkbox item. This item can be used like any other item and is capable of storing either a single value for checked (activated) or NULL for the empty state.
In order to demonstrate how this feature works, we will need to do a couple of preparation steps first:
Create a new workspace. (You need to make sure that your instance settings allow creation of the demo application, Workspace: INTERNAL Home Manage Instance Feature Configuration."Create demonstration objects in new workspaces" needs to be set to Yes.).
This will create the required tables you need for this demonstration (EMP and DEPT).
Change the EMP table by adding an additional column and changing one of the columns, as shown in Listing 3-1.
Create a new application.
Create a tabular form using a wizard based on the EMP table and include all the columns.
Together, these steps provide a framework from which to experiment with the Simple Checkbox feature. Listing 3-1 shows the code to modify the EMP and DEPT tables. The valid
column added to table EMP is the checkbox column.
Example 3-1. Extending EMP and DEPT Tables
ALTER TABLE emp MODIFY (ename VARCHAR2(40) NOT NULL); ALTER TABLE emp ADD (valid VARCHAR2(1)); CREATE TABLE emp_bkp AS SELECT * FROM emp; CREATE TABLE dept_bkp AS SELECT * FROM dept; CREATE SEQUENCE dept_seq START WITH 50 INCREMENT BY 10 NOCACHE; CREATE SEQUENCE emp_seq START WITH 7950 INCREMENT BY 1 NOCACHE; CREATE OR REPLACE TRIGGER dept_tr BEFORE INSERT ON dept FOR EACH ROW BEGIN IF :NEW.deptno IS NULL
THEN SELECT dept_seq.NEXTVAL INTO :NEW.deptno FROM DUAL; END IF; END; / CREATE OR REPLACE TRIGGER emp_tr BEFORE INSERT ON emp FOR EACH ROW BEGIN IF :NEW.empno IS NULL THEN SELECT emp_seq.NEXTVAL INTO :NEW.empno FROM DUAL; END IF; END; /
Now, editing the Report Attributes and the newly created column properties for the column VALID you can change Display As to Simple Checkbox as shown in Figure 3-2.
Using the List of Values tab you will need to enter the required static LOV for a simple checkbox (see Figure 3-3). If you require only one value, you will need to change the list of values definition to Y instead of Y,N.
After applying the changes, running the application and opening the page with the Tabular Form, you should see a result similar to Figure 3-4. Figure 3-5 shows the raw data from the EMP table that underlies the form.
All checkboxes in Figure 3-5 are empty since you initially created a new empty column. The first update on the table will correct that, setting null values to N for unchecked boxes.
If you now activate a couple of checkboxes and submit the changes, the Tabular Form will update the column to the expected values for the displayed set of rows only.
The APEX 4 wizard for creating Tabular Forms will do some additional work for you and save you some time. If you have columns of type DATE or TIMESTAMP in your table and include those in a Tabular Form, APEX will automatically set it up as a Date Picker column using the new jQuery calendar, as shown in Figure 3-6.
Unfortunately, you cannot extend that calendar feature the way you can in simple forms and specify number of months or add a year range to it. Hopefully that functionality will be included in one of the next releases.
The Radio Group is the next Tabular Form item type which came with release 4 of APEX. In earlier versions you were able to manually create that item type by using the apex_item.radiogroup
packaged function. The disadvantage of doing so was that APEX would create one array per entry in the Radio Group and you needed to write your own code in order to handle that problem while inserting or updating the record.
To show how this feature works, you will now change the item type for the column VALID to Radio Group (Static LOV), as shown in Figure 3-7. Change the "List of values definition" to
STATIC:Yes;Y,No;N
After applying the changes, running the application, and opening the page with the Tabular Form, you should see a result similar to Figure 3-8.
The limitation for the select list item type regarding the number of entries you could use was one of the biggest problems in earlier releases of APEX. As soon as the list grew over a certain size (a combination of the number and size of the available options) you would receive an error saying the character string buffer is too small (see Figure 3-9).
For the simple forms, the workaround was to use a popup LOV returning the key value, but for Tabular Forms this simply wasn't possible. APEX 4 changed this by including this item type for Tabular Forms.
To test this feature you will need to modify your Tabular Form by editing the column DEPTNO and changing it to a "Popup Key LOV (query based LOV)" and adding the following query to the "List of values definition":
SELECT dname d, deptno r FROM dept
If you now apply the changes, run the application, and sort the Tabular Form using the DEPTNO column, it should show the popup key LOV for that column as in Figure 3-10.
One additional change was added to the select list item type in general. In earlier versions of APEX the sorting on a column containing a select list would sort the values on the return value. Often this was not desirable. Since APEX 4, this behavior has changed and the sorting is done on the displayed value, as you can see in Figure 3-10.
APEX 4 introduced declarative validations for Tabular Forms. This feature is quite important since it saves time while building your applications. Declarative validations can be created on a single column and are grouped in
There are several Column String Comparison validations and you can view them in the Figure 3-11.
The current release of APEX does not provide declarative validations outside the scope of a single column; that type of validation is planned for a future release. If you need such a validation, you will have to write your own code. We will show how to do that later in the chapter (Custom Coding in Tabular Forms).
After creating a Tabular Form, you will find a couple of validations created by the wizard. An example of these validations is shown in Figure 3-12.
The APEX wizard will use the table definition and sort out the constraints you defined for your table. It will create a validation for each single column containing a NOT NULL definition (unless the column is a primary key used for the DML processes), or for columns of type numeric and columns of type date.
As you can see in Figure 3-12, the APEX Tabular Form wizard created several validations automatically. If you remember the changes made in Listing 3-1, you will notice that this change resulted in a validation checking the ENAME column (Validation with sequence 30 – ENAME IS NOT NULL). The other validations are related to the data type of all existing numeric and date columns included in the Tabular Form.
If you want to test the validations created by the wizard, you can make a couple of changes in your table to see how they work, as shown in Figure 3-13.
Prior to making these changes and submitting them, you should change the sorting of your Tabular Form from DEPTNO to EMPNO. In version 4.0.2 of APEX there is a bug described here: http://forums.oracle.com/forums/thread.jspa?threadID=2139640
.
Change the entry for the column ENAME to NULL and modify the entry for the column SALARY to an alphanumeric character. If you try to save that, you should receive an error like the one shown in Figure 3-14.
If you remember how such validations worked before version 4, you will notice a few changes:
APEX is now highlighting the cells in which there are validation errors found.
APEX will tell you the name of the column affected by the error message.
APEX will provide you with a direct link for setting focus to the affected cell as a part of the error message.
Most important, you didn't lose the changes you made.
In earlier versions of APEX a lot of custom coding was required to get a similar functionality. To avoid losing the changes you made after an unsuccessful validation, it was required to display the validation errors on an error page and press the back button.
One more important thing needs to be mentioned. If you edit one of the validations created by the wizard, you will notice that there is a new substitution string in the Error Message. You can use #COLUMN_HEADER# for the heading of the associated Tabular Form column in order to display the header name as a part of the error message, as shown in Figure 3-15.
APEX 4 also introduced a couple of other neat features. One is protection against lost updates when you resort or paginate through your data. Another is the ability to add new rows to a form without making a round trip to the server for each row added. Finally, the overall number of processes has been reduced.
APEX 4 will inform you about the changes you made and about the risk of losing your changes if you try to change the sorting of the Tabular Form or try to paginate through the Tabular Form. Figure 3-16 shows this warning message.
In earlier versions, the process for adding new rows to a Tabular Form would require you to submit and load your page once per new row. This process had to validate and save changes each time you added a new row. The Add Row functionality is done by using new JavaScript function:
javascript:addRow();
You can find the function's invocation in the ADD button, as the URL Target.
After the initial rendering of a Tabular Form, APEX will remove that new row from the DOM and put it in a JavaScript variable. After pressing the ADD button, JavaScript will replace some of the substitution strings in that variable (index, names, etc.) and add that row again to the end of the table. If you want to add multiple rows to a tabular form, the only thing you need to do is to press the button again.
The addition of functionality to add new rows using client-side code brings the additional benefit of reducing the number of processes required for DML. Earlier versions of APEX required four submit processes for DML:
APEX 4 requires only two of these processes. Only one MRU process is now needed, as new rows are no longer added using server-side code. For the same reason, the AddRow process is redundant, and has been removed.
The new Tabular Form feature of APEX 4 still has a couple of limitations. Hopefully most of them will be solved in a future release of APEX.
Changes we would like to see include
Declarative validations outside the single column scope
Multiple tabular forms per page
Item type settings equal to those for page items
Autocomplete and Autocomplete Returning Key Value for Tabular Forms
Dynamic Actions for Tabular Forms
Some of what you will read in the rest of this chapter describes workarounds that you can put into place to get some of the functionality in this list today.
As already indicated, you will need to know how to write our own code in case you have some kind of a validation you couldn't cover using column string comparison or you need to do some DML. The following sections help you write custom code to cover some common use cases that you will encounter while doing APEX development.
Sometimes you have to write custom code for deleting rows from a Tabular Form. If you are faced with a situation in which you need to do that, it's very important to be sure that your custom code actually does delete the correct rows. You can put the Checkbox item to good use in identifying those rows to delete, or to otherwise process in some way. You must be careful, though, to write your code correctly.
Listing 3-2 shows a common mistake made in deleting checked rows. The code will delete the right number of rows, but not those you checked. The code will actually delete the rows starting from the first row in the table and ending with n (representing the number of rows checked).
Example 3-2. Deleting Checked Rows—Common Mistake
BEGIN FOR i IN 1 .. apex_application.g_f01.COUNT LOOP
DELETE FROM emp WHERE empno = apex_application.g_f02 (i); -- g_f02 is the hidden column containing -- the primary key of the EMP table (empno) END LOOP; END;
Listing 3-3 shows how the correct code should look. Because several procedures are now used, they have been bundled together into a package. You can then simply call this package in your processes or validations.
The package in Listing 3-3 implements a couple of small procedures in order to make the overall processing easier. The goal is to avoid overloading the page and application with loose PL/SQL blocks.
Example 3-3. Deleting Checked Rows—Procedure
CREATE OR REPLACE PACKAGE tab_form_emp_pkg AS PROCEDURE disable_foreign_constraints; PROCEDURE enable_foreign_constraints; PROCEDURE restore_tables; PROCEDURE delete_emp_row (p_message OUT VARCHAR2); END tab_form_emp_pkg; / CREATE OR REPLACE PACKAGE BODY tab_form_emp_pkg AS PROCEDURE disable_foreign_constraints IS BEGIN FOR c IN (SELECT constraint_name, table_name FROM user_constraints WHERE table_name IN ('EMP', 'DEPT') AND constraint_type = 'R' ORDER BY table_name) LOOP EXECUTE IMMEDIATE 'ALTER TABLE ' || c.table_name || ' DISABLE CONSTRAINT ' || c.constraint_name; END LOOP; END disable_foreign_constraints; PROCEDURE enable_foreign_constraints IS BEGIN FOR c IN (SELECT constraint_name, table_name FROM user_constraints WHERE table_name IN ('EMP', 'DEPT') AND constraint_type = 'R' ORDER BY table_name)
LOOP EXECUTE IMMEDIATE 'ALTER TABLE ' || c.table_name || ' ENABLE CONSTRAINT ' || c.constraint_name; END LOOP; END enable_foreign_constraints; PROCEDURE restore_tables IS -- We will use this process to restore our date after testing. BEGIN FOR c IN (SELECT constraint_name, table_name FROM user_constraints WHERE table_name IN ('EMP', 'DEPT') AND constraint_type = 'R' ORDER BY table_name) LOOP EXECUTE IMMEDIATE 'ALTER TABLE ' || c.table_name || ' DISABLE CONSTRAINT ' || c.constraint_name; END LOOP; EXECUTE IMMEDIATE 'TRUNCATE TABLE dept DROP STORAGE'; EXECUTE IMMEDIATE 'TRUNCATE TABLE emp DROP STORAGE'; INSERT INTO dept SELECT * FROM dept_bkp; INSERT INTO emp SELECT * FROM emp_bkp; COMMIT; FOR c IN (SELECT constraint_name, table_name FROM user_constraints WHERE table_name IN ('EMP', 'DEPT') AND constraint_type = 'R' ORDER BY table_name) LOOP EXECUTE IMMEDIATE 'ALTER TABLE ' || c.table_name || ' ENABLE CONSTRAINT ' || c.constraint_name; END LOOP; END restore_tables;
PROCEDURE delete_emp_row (p_message OUT VARCHAR2) IS v_row INTEGER; v_count INTEGER := 0; BEGIN FOR i IN 1 .. apex_application.g_f01.COUNT LOOP v_row := apex_application.g_f01 (i); DELETE FROM emp WHERE empno = apex_application.g_f02 (v_row); -- g_f02 is the hidden column containing -- the primary key of the EMP table (empno) v_count := v_count + 1; END LOOP; p_message := v_count || ' row(s) deleted.'; END delete_emp_row; END tab_form_emp_pkg; /
Let's work through an explanation of Listing 3-3. You need to do the following preparations for a test:
Create an application item T_MESSAGE
and set the session state protection to Restricted – may not be set from browser. This application item will be used to display messages. In a case of an ApplyMRD process, you have the substitution strings like #MRD_COUNT#
or #MRI_COUNT#
or #MRU_COUNT#
. For a custom process, you need a new variable.
Set the existing ApplyMRD process to Conditional Never, so it doesn't run.
Create a new On Submit Process PL/SQL anonymous block ApplyMRD Manual and use the following code:
BEGIN tab_form_emp_pkg.disable_foreign_constraints; tab_form_emp_pkg.delete_emp_row (:t_message); END;
Use the substitution string for the success message:
&T_MESSAGE.
Make the process conditional so it runs on condition type PL/SQL Expression:
:REQUEST IN ('MULTI_ROW_DELETE')
Confirm and create a process.
As you can see, some additional procedures to disable constraints for referential integrity were used. This is for testing purposes only, because we are talking here about an isolated case. In the real world, you would create a validation checking whether such constraints exist.
You can now test this process and try to delete a couple of rows to see what you get. Figure 3-17 shows the selected rows to delete, and Figure 3-18 shows the result.
Run the following block of code in SQL Workshop in order to restore the EMP table:
BEGIN tab_form_emp_pkg.restore_tables; END;
As already mentioned, there is much more work to do if you write your own custom processes. Automatic processes created using the Tabular Form wizard ensure data integrity, and you would need to do the same kind of thing manually in your own code. Automatic processes may display confusing errors, but they are secure. It is not easy to write all of that code yourself since there are many details you need to think of. My goal now is to show you how to do that and make you aware of the most important things you should keep in mind.
APEX 4 Tabular Form wizard creates a hidden checksum item for every Tabular Form. This checksum will be used for later automatic MRU and MRD processes. Using the Firebug extension of Firefox, you can see that hidden item if you investigate the generated HTML, as shown in Figure 3-19.
When talking about checksum and data integrity, you need to keep several issues in mind:
If you want to run your custom code, you will need to do a check and find out if the data in a row has changed. It doesn't make sense to do an update on a row that hasn't changed.
If the data has changed, you will need to compare the checksum of the original data you loaded while rendering the page and the checksum of the current data in the table for each row.
If a row has changed (if the old checksum in the g_fcs array is not the same as the new checksum of the tabular form rows you calculate), you will need to do an update.
However, you should be able to update only then if the original data in the table hasn't been changed since the last fetch (old checksum in the g_fcs array is not the same as the new checksum of the row in the table you need to calculate). Otherwise, there should be an error displayed and you should stop the processing.
Following the rules outlined in the previous section, you can start extending your package by adding some new functions:
The first function will compare the original checksum with the new generated checksum for each row in the Tabular Form. It will return a BOOLEAN.
The second function will compare the original checksum with the checksum of the data in the table for each row where the first function returns FALSE. It will return a BOOLEAN as well.
Finally, the third function will be a validation function returning an error message in case some of the rows you are trying to update have been changed by other users. The second function returns FALSE.
Let's extend the package tab_form_emp_pkg
by adding the first function described, shown in Listing 3-4.
Example 3-4. Checksum—Function 1
FUNCTION compare_checksum_change (p_array IN NUMBER) RETURN BOOLEAN; FUNCTION compare_checksum_change (p_array IN NUMBER) RETURN BOOLEAN IS BEGIN IF apex_application.g_f02 (p_array) IS NOT NULL THEN IF apex_application.g_fcs (p_array) <> wwv_flow_item.md5 (apex_application.g_f02 (p_array), apex_application.g_f03 (p_array), apex_application.g_f04 (p_array), apex_application.g_f05 (p_array), apex_application.g_f06 (p_array), apex_application.g_f07 (p_array), apex_application.g_f08 (p_array), apex_application.g_f09 (p_array), apex_application.g_f11 (p_array) ) THEN RETURN FALSE; ELSE RETURN TRUE; END IF; ELSE RETURN TRUE; END IF; END compare_checksum_change;
As already mentioned, the items of type Simple Checkbox or Popup Key LOV will reserve two arrays for one item. This is the reason for the gap between the g_f09
and g_f11
arrays.
After that, add the second function, shown in Listing 3-5.
Example 3-5. Checksum—Function 2
FUNCTION compare_checksum_table (p_array IN NUMBER) RETURN BOOLEAN; FUNCTION compare_checksum_table (p_array IN NUMBER) RETURN BOOLEAN
IS v_empno NUMBER; v_emp_checksum VARCHAR2 (40); BEGIN IF apex_application.g_f02 (p_array) IS NOT NULL THEN v_empno := apex_application.g_f02 (p_array); SELECT wwv_flow_item.md5 (empno, ename, job, mgr, hiredate, sal, comm, deptno, valid ) INTO v_emp_checksum FROM emp WHERE empno = v_empno; IF apex_application.g_fcs (p_array) <> v_emp_checksum THEN RETURN FALSE; ELSE RETURN TRUE; END IF; ELSE RETURN TRUE; END IF; END compare_checksum_table;
As the last function, you can now create a validation function returning VARCHAR2
which you will then call on the page, as shown in Listing 3-6.
Example 3-6. Checksum—Function 3
FUNCTION validate_data_integrity RETURN VARCHAR2; FUNCTION validate_data_integrity RETURN VARCHAR2 IS v_error VARCHAR2 (4000); BEGIN FOR i IN 1 .. apex_application.g_f02.COUNT LOOP IF NOT compare_checksum_change (i) –- we changed the row AND NOT compare_checksum_table (i) -– however the table data has changed
THEN v_error := v_error || '<br/>' || 'Row ' || i || ': The version of the data in the ' || 'table has been change since the last page ' || 'rendering. Click <a href="f?p=' || v ('APP_ID') || ':' || v ('APP_PAGE_ID') || ':' || v ('APP_SESSION') || '">here</a> to reload the page.'; END IF; END LOOP; v_error := LTRIM(v_error, '<br/>'), RETURN v_error; END validate_data_integrity;
You can test this code by creating a page validation of type PL/SQL Function Returning Error Message, using sequence number 1 for the process, and naming the process Check Data Integrity. Set the process to unconditional and enter the required call to the package procedure. Here is the PL/SQL code for the page validation:
BEGIN RETURN tab_form_emp_pkg.validate_data_integrity; END;
Next, change one of the records using some other tool such as SQL Workshop, and commit your changes. Go to the Tabular Form, change the same record and submit the change. You should see an error message similar to that in Figure 3-20.
You don't need to use the technique described in this section in a standard Tabular Form. A similar message will be generated automatically in a standard form. You need the technique and code in this section only in a manually generated Tabular Form.
As long as APEX doesn't allow creation of multiple standard Tabular Forms per page, you will be faced with a requirement to create a workaround for those cases in which you need to manage one master table with multiple detail tables.
Let's look at how to write the code for a manual Tabular Form. You will use only the following item types:
apex_item.hidden
apex_item.checkbox
apex_item.text
You will also use the apex_item package and parse parameters for
Column array
Column value
Column size
Column max length
Create a second page (page 2) and call it Tabular Form 2. After that, create a report region for your manual Tabular Form. You will need to use a subquery. In the subquery you will create one empty row first, to be able to enter new rows upon request. You will also need to generate a checksum for the rows which you will use later on to check which rows have changed and run an update for those. Listing 3-7 shows our SELECT statement along with its subquery.
Example 3-7. Manual Tabular Form—SQL
SELECT apex_item.checkbox (1, '#ROWNUM#') empno, apex_item.hidden (2, empno) || apex_item.text (3, ename, 20, 20) ename, apex_item.text (4, job, 10, 10) job, apex_item.text (5, mgr, 5, 5) mgr, apex_item.text (6, hiredate, 12, 12) hiredate, apex_item.text (7, sal, 6, 6) sal, apex_item.text (8, comm, 6, 6) comm, apex_item.text (9, deptno, 4, 4) || apex_item.hidden (10, checksum) deptno FROM (SELECT NULL empno, NULL ename, NULL job, NULL mgr, NULL hiredate, NULL sal, NULL comm, NULL deptno, NULL checksum FROM DUAL WHERE :request IN ('ADD') UNION ALL SELECT empno, ename, job, mgr, hiredate, sal, comm, deptno, wwv_flow_item.md5 (empno, ename, job, mgr, hiredate, sal, comm,
deptno ) checksum FROM emp)
The select statement for the empty column will run only if the REQUEST
is set to the specified value.
Change the column type to "standard report column", otherwise you will see HTML code in your report. The need to make this change is new in APEX 4, and has to do with security and cross-site scripting.
The next step is to create four buttons as follows:
A SUBMIT button to submit the page.
A MULTI_ROW_DELET button to delete rows. The target of this button will be this URL:
javascript:apex.confirm(htmldb_delete_message,'MULTI_ROW_DELETE'),
An ADD button to add new rows. This button will also submit the page.
A CANCEL button. This button will redirect to the same page. Each of these buttons will be positioned in the report region. You will also need to create at least two branching processes:
On Submit – After Processing When Button Pressed "ADD" Page 2 include process success message Sequence 5 Conditional
On Submit – After Processing Unconditional Page 2 include process success message Sequence 10 The first branching will submit the page and redirect to the same page. You are now ready to write the code you will need for processing. You will create two processes:
ApplyMRU: This process updates existing new rows. This process will be conditional using the PL/SQL Expression: :REQUEST IN ('ADD', 'SUBMIT')
delete_emp_row
, from Listing 3-3. This process and its associated package enable you to delete rows. Since you already know how to use checksum to ensure data integrity, you will not repeat that part in this example.
Finally, you need to add a validation:
Validate Commission: This is a page-level validation ensuring that you can enter a commission value only for the department SALES, which is department 30. This validation will be unconditional.
Listing 3-8 shows the procedure for updating and the validation in your package.
Example 3-8. Manual Tabular Form—Update and Validation Processes
PROCEDURE save_emp_custom (p_message OUT VARCHAR2); FUNCTION validate_emp_comm RETURN VARCHAR2; PROCEDURE save_emp_custom (p_message OUT VARCHAR2) IS v_ins_count INTEGER := 0; v_upd_count INTEGER := 0; BEGIN FOR i IN 1 .. apex_application.g_f02.COUNT LOOP BEGIN IF apex_application.g_f02 (i) IS NOT NULL THEN IF apex_application.g_f10 (i) <> wwv_flow_item.md5 (apex_application.g_f02 (i), apex_application.g_f03 (i), apex_application.g_f04 (i), apex_application.g_f05 (i), apex_application.g_f06 (i), apex_application.g_f07 (i), apex_application.g_f08 (i), apex_application.g_f09 (i) ) THEN UPDATE emp SET ename = apex_application.g_f03 (i), job = apex_application.g_f04 (i), mgr = apex_application.g_f05 (i), hiredate = apex_application.g_f06 (i), sal = apex_application.g_f07 (i), comm = apex_application.g_f08 (i), deptno = apex_application.g_f09 (i) WHERE empno = apex_application.g_f02 (i); v_upd_count := v_upd_count + 1; END IF; ELSE INSERT INTO emp (ename, job, mgr, hiredate, sal, comm, deptno ) VALUES (apex_application.g_f03 (i),
apex_application.g_f04 (i), apex_application.g_f05 (i), apex_application.g_f06 (i), apex_application.g_f07 (i), apex_application.g_f08 (i), apex_application.g_f09 (i) ); v_ins_count := v_ins_count + 1; END IF; EXCEPTION WHEN OTHERS THEN p_message := p_message || SQLERRM; END; END LOOP; IF v_ins_count > 0 OR v_upd_count > 0 THEN p_message := p_message || v_ins_count || ' row(s) inserted. ' || v_upd_count || ' row(s) updated.'; END IF; EXCEPTION WHEN OTHERS THEN p_message := SQLERRM; END save_emp_custom; FUNCTION validate_emp_comm RETURN VARCHAR2 IS v_message VARCHAR2 (4000); BEGIN FOR i IN 1 .. apex_application.g_f02.COUNT LOOP IF apex_application.g_f09 (i) <> 30 AND apex_application.g_f08 (i) IS NOT NULL THEN v_message := v_message || '<br/>' || 'Commission is allowed for the sales department only.' || ' (Row ' || i || ')'; END IF; END LOOP;
v_message := LTRIM (v_message, '<br/>'), RETURN v_message; END validate_emp_comm;
The update/insert process will loop through the array of EMPNO (g_f02)
and, for all rows containing the primary key, compare the original checksum with the new calculated checksum. If there is a difference, it will update the corresponding rows and update the counter. For the added rows where the array is NULL, it will insert a new row. You will use this PL/SQL block to start that process:
BEGIN tab_form_emp_pkg.save_emp_custom (:t_message); END;
and put the &T_MESSAGE
. in the Process Success Message section.
The validation process is fairly simple. It will also loop through the array of EMPNO (g_f02)
and check if there is an entry for the COMM column (g_f08)
where DEPTNO (g_f09)
is different than SALES (value 30)
. You will use this PL/SQL block for the validation:
BEGIN RETURN tab_form_emp_pkg.validate_emp_comm; END;
You can now test your form to confirm that it works as expected. Figures 3-21 through 3-25 walk you through the steps in the test.
In a manual Tabular Form, changes are lost after a validation error. If you want to keep the changes, you will need to display the validation error on an error page, or create a workaround using collections.
Collections are one of the greatest features of APEX. An APEX Collection is a set of tables, packaged procedures, and functions for maintaining session-related data. You can use a collection to create, modify and delete your own datasets without having to touch the original source. Once the processing is done, you can decide either to save your changes back to the source (insert, update, or delete) or discard your changes. If you log off or somehow lose your session, you will not be able to retrieve that data again.
The need to use a collection often involves a small portion of data such as a snapshot or a window. For example, you might want to create a Tabular Form for modifying sales department records for only department 30. Figure 3-26 shows such a window.
Although a collection can accept almost an unlimited amount of data, it wouldn't make sense to use one for loading hundreds or even thousands of records. Collections are designed to serve smaller datasets.
The goal here is to show how to work with collections using Tabular Forms. This section will demonstrate a couple of possible ways and techniques to put collections to use. Specifically, you will:
Create a collection containing all employees of one department
Create a couple of alternate processes you could use for updating collections from a Tabular Form
Create a process for updating the original source using collection data
You will continue with the methods used earlier in this chapter, and keep all of your code in a package. You will use APEX regions and processes only to make calls into your package code.
First, create a procedure and a view. Listing 3-9 shows a procedure for creating a collection containing all employees of one department. Also in the listing is a view created upon that collection.
Example 3-9. Procedure and View for a Tabular Form's Collection
PROCEDURE create_emp_collection (p_deptno IN NUMBER, p_message OUT VARCHAR2); PROCEDURE create_emp_collection (p_deptno IN NUMBER, p_message OUT VARCHAR2) IS v_collection VARCHAR2 (40) := 'EMP_DEPT'; BEGIN IF apex_collection.collection_exists (v_collection) THEN apex_collection.delete_collection (v_collection); p_message := 'Collection deleted.'; END IF; apex_collection.create_collection_from_query (v_collection, 'SELECT a.*, wwv_flow_item.md5(empno, ename, job, ' || 'mgr, hiredate, sal, comm, deptno, valid) ' || 'FROM EMP a WHERE deptno = ' || p_deptno ); p_message := p_message || '<br/>' || 'Collection created.'; p_message := LTRIM (p_message, '<br/>'), END create_emp_collection; CREATE OR REPLACE VIEW emp_coll_v AS SELECT seq_id, c001 empno, c002 ename, c003 job, c004 mgr, c005 hiredate, c006 sal, c007 comm, c008 deptno, c009 valid, c010 checksum, c011 delete_flag FROM apex_collections WHERE collection_name = 'EMP_DEPT';
The procedure will check whether the collection exists. If the collection does exist, the procedure will delete the existing collection and create a new one based on the input. Otherwise, the procedure creates a new collection.
The view will make it easier to deal with the collection. You will not need to remember the member number in order to insert, update, or delete a row. The view takes care of the member number for you.
You can now start creating a new page, which will be page 3. Use a standard Tabular Form based on the view emp_coll_v
. Include all the columns. The primary key will be the combination of the SEQ_ID and EMPNO column. Make all columns editable. Make the region title Tabular Form Collection. After creating the page, change the item type for the columns CHECKSUM
and DELETE_FLAG
to Hidden.
You will also need to edit the generated SQL for the Tabular Form and add a condition as follows:
WHERE delete_flag IS NULL
This condition will exclude those records from the collection that are marked as deleted.
You will create a select list within the Tabular Form region: P3_DEPTNO
. This select list will show a list of available departments based on the SQL query:
SELECT dname, deptno FROM dept
You will also need a button displayed after the select list (create a button displayed among this region's items) which you will use to trigger the process. Name this button P3_GO
and assign it a request GO
.
Now you can start creating a page process (Create Collection) on submit for creating a collection. You will use the following PL/SQL block for this:
BEGIN tab_form_emp_pkg.create_emp_collection (:p3_deptno, :t_message); END;
Make the block conditional to run based on the following PL/SQL Expression:
:REQUEST IN ('GO')
Do not forget to code the following as the success message of the process:
&T_MESSAGE.
Finally, everything is set for testing what you have done. If you select the sales department from the list and press the GO button, you should get a result similar to the one shown in Figure 3-27.
The question now is, how do you update your collection? Following are three possible methods for updating the collection:
The sections to follow describe each of these methods in detail.
The ideal way to update the collection would be to create three instead of triggers on the view for updating, deleting, and inserting of rows. Thinking further, imagine writing a package that would automatically create those triggers for you. You would just need to provide your collection query and the package would generate all of the code for use. You would just place the resulting code in your application and it would run with the automatic DML process of Tabular Form. Such an approach is, sadly, too good to be true. Currently, you can't get the instead of triggers to work with collections. If you try using instead of triggers, you will receive error messages such as those shown in Figure 3-28.
You'll immediately notice that the error in Figure 3-28 has to do with privileges. An APEX Collection is accessible only from the session context, and the trigger is not running in that session context. You would need to grant access privileges on WWV_FLOW_COLLECTION
to your schema, and this is definitely not the way to go since you would need to change the APEX source code as provided by Oracle Corporation. Such a change would also pose a security issue. For these reasons, the instead of trigger approach, while nice to contemplate, is simply not feasible.
Before you start writing packaged procedures, you should delete the automatic DML processes that the wizard created for you on page 3 of your application. You will need to create two procedures in your package. These procedures are similar to those you wrote for the manual Tabular Forms. Add the code shown in Listing 3-10 to the package.
Example 3-10. Update and Delete Procedures for Tabular Form Collections
PROCEDURE save_emp_coll_custom (p_message OUT VARCHAR2); PROCEDURE delete_emp_coll_custom (p_message OUT VARCHAR2); PROCEDURE save_emp_coll_custom (p_message OUT VARCHAR2) IS v_ins_count INTEGER := 0;
v_upd_count INTEGER := 0; v_collection VARCHAR2 (40) := 'EMP_DEPT'; BEGIN FOR i IN 1 .. apex_application.g_f02.COUNT LOOP BEGIN IF apex_application.g_f02 (i) IS NOT NULL THEN IF apex_application.g_f12 (i) <> wwv_flow_item.md5 (apex_application.g_f03 (i), apex_application.g_f04 (i), apex_application.g_f05 (i), apex_application.g_f06 (i), apex_application.g_f07 (i), apex_application.g_f08 (i), apex_application.g_f09 (i), apex_application.g_f10 (i), apex_application.g_f11 (i) ) THEN apex_collection.update_member (p_collection_name => v_collection, p_seq => apex_application.g_f02(i), p_c001 => apex_application.g_f03(i), p_c002 => apex_application.g_f04(i), p_c003 => apex_application.g_f05(i), p_c004 => TO_NUMBER(apex_application.g_f06(i)), p_c005 => TO_DATE(apex_application.g_f07(i)), p_c006 => TO_NUMBER(apex_application.g_f08(i)), p_c007 => TO_NUMBER(apex_application.g_f09(i)), p_c008 => TO_NUMBER(apex_application.g_f10(i)), p_c009 => apex_application.g_f11(i), p_c010 => apex_application.g_f12(i), p_c011 => apex_application.g_f13(i)); v_upd_count := v_upd_count + 1; END IF; ELSE apex_collection.add_member (p_collection_name => v_collection, p_c001 => emp_seq.NEXTVAL, p_c002 => apex_application.g_f04(i), p_c003 => apex_application.g_f05(i), p_c004 => TO_NUMBER(apex_application.g_f06(i)), p_c005 => TO_DATE(apex_application.g_f07(i)), p_c006 => TO_NUMBER(apex_application.g_f08(i)), p_c007 => TO_NUMBER(apex_application.g_f09(i)), p_c008 => TO_NUMBER(apex_application.g_f10(i)), p_c009 => apex_application.g_f11(i) ); v_ins_count := v_ins_count + 1; END IF; EXCEPTION
WHEN OTHERS THEN p_message := p_message || '<br/>' || 'Row: ' || i || ' > ' || SQLERRM; p_message := LTRIM (p_message, '<br/>'), END; END LOOP; IF v_ins_count > 0 OR v_upd_count > 0 THEN p_message := p_message || '<br/>' || v_ins_count || ' row(s) inserted. ' || v_upd_count || ' row(s) updated.'; END IF; p_message := LTRIM (p_message, '<br/>'), EXCEPTION WHEN OTHERS THEN p_message := SQLERRM; END save_emp_coll_custom; PROCEDURE delete_emp_coll_custom (p_message OUT VARCHAR2) IS v_row INTEGER; v_count INTEGER := 0; v_collection VARCHAR2 (40) := 'EMP_DEPT'; BEGIN FOR i IN 1 .. apex_application.g_f01.COUNT LOOP v_row := apex_application.g_f01 (i); apex_collection.update_member (p_collection_name => v_collection, p_seq => apex_application.g_f02(v_row), p_c011 => 'Y'), v_count := v_count + 1; END LOOP; p_message := v_count || ' row(s) deleted.'; END delete_emp_coll_custom;
Before you can start testing this code, you will create two on submit processes on application page 3:
Update Collection: The process for updating existing and adding new rows. This process will be conditional using the following PL/SQL Expression:
:REQUEST IN ('ADD', 'SUBMIT')
Delete Collection Member: The process for flagging deleted records to Y. This process will be conditional using this PL/SQL Expression;
:REQUEST IN ('MULTI_ROW_DELETE')
You are going to use the following PL/SQL blocks to run these processes. The first PL/SQL block is for the update process and the second one is for the delete member process:
BEGIN tab_form_emp_pkg.save_emp_coll_custom (:t_message); END; BEGIN tab_form_emp_pkg.delete_emp_coll_custom (:t_message); END;
And, of course, do not forget to put the following into the Process Success Message section:
&T_MESSAGE.
Now, you will test your new version of page 3 and change one of the records in your Tabular Form. Figure 3-29 shows a change being made.
You should get a similar result to the one shown in Figure 3-30.
Since a collection is only a snapshot of the original data, you can use a different approach for an update. You can create some JavaScript code and combine that with an On-Demand Process to update the collection when a single Tabular Form item changes. The validation of the collection values will be done only if you decide to save the collection back to the source. You will include only some basic validations for numeric and date columns in this code.
The code you need to write for this demonstration consists of
One procedure for updating a collection member
Two small JavaScript functions
Three application items you will use for parsing parameters: T_COL_VAL_ITEM, T_COL_SEQ_ITEM, T_COL_ATTR_ITEM
One On-Demand Process to call your procedure
Always use the same prefix for application items. In this case you are using T_, but you could use any other letter or any combination of letters in the front.
The procedure code you will add to your package is shown in Listing 3-11.
Example 3-11. Tabular Form—Collection— Update on Demand
PROCEDURE update_emp_coll_member ( p_seq_id IN NUMBER, p_attribute_number IN NUMBER, p_attribute_value IN VARCHAR2 ); PROCEDURE update_emp_coll_member (
p_seq_id IN NUMBER, p_attribute_number IN NUMBER, p_attribute_value IN VARCHAR2 ) IS v_collection VARCHAR2 (40) := 'EMP_DEPT'; v_number NUMBER; v_date DATE; v_message VARCHAR2 (4000); BEGIN IF p_seq_id IS NOT NULL THEN IF p_attribute_number IN (4, 6, 7, 8) THEN v_number := TO_NUMBER (p_attribute_value); ELSIF p_attribute_number IN (5) THEN v_date := TO_DATE (p_attribute_value); END IF; apex_collection.update_member_attribute (p_collection_name => v_collection, p_seq => p_seq_id, p_attr_number => p_attribute_number, p_attr_value => p_attribute_value ); END IF; EXCEPTION WHEN OTHERS THEN v_message := SQLERRM; HTP.p (v_message); END update_emp_coll_member;
The JavaScript code shown in Listing 3-12, added to the page HTML header of application page 3, will call the On-Demand Process and check the return value of that process for errors.
Example 3-12. Tabular Form—Collection—Ajax
<script language="JavaScript" type="text/javascript"> function LPad(ContentToSize,PadLength,PadChar) { var PaddedString=ContentToSize.toString(); for(i=ContentToSize.length+1;i<=PadLength;i++) {PaddedString=PadChar+PaddedString;} return PaddedString; } function f_update_emp_coll_member(pThis,pRownum,pAttribNo){ var v_seq_array = 'f02_' + LPad(pRownum, 4, "0"); var SeqID = $x(v_seq_array).value; var get = new htmldb_Get(null,$x('pFlowId').value,
'APPLICATION_PROCESS=update_emp_coll_member',0); get.add('T_COL_VAL_ITEM',pThis); get.add('T_COL_SEQ_ITEM',SeqID); get.add('T_COL_ATTR_ITEM',pAttribNo); gReturn = get.get(); if(gReturn) {alert(gReturn)} get = null; } </script>
The code to implement the On-Demand Process update_emp_coll_member
will be a simple PL/SQL block:
BEGIN tab_form_emp_pkg.update_emp_coll_member (:t_col_seq_item, :t_col_attr_item, :t_col_val_item); END;
The last thing you need to do is to call this code from the Tabular Form. You do that by entering code similar to the following for each column on the form. Place the code in the Element Attributes field under Column Properties. Following is the code for the ENAME column:
onchange="f_update_emp_coll_member(this.value,'#ROWNUM#',2);"
The third parameter of the JavaScript function is the attribute number of the column in question. Take care to adjust that value to the right one for each column on the form. The mapping in the view code (Listing 3-9) can help you do that.
Now run page 3 and try to update some of the rows in the Tabular Form, as shown in Figure 3-31.
You can also activate Firebug and watch what happens at the browser level, as shown in Figure 3-32. Activate Firebug and switch to the console tab. After updating a column, you should see the process running in Firebug. Opening that process will display more detail.
The final step in this exercise is to save the collection data back to the table. For that you will create
One validation function that will take care of the data integrity
One procedure that will save the data back to the table
Listing 3-13 shows the code for these procedures.
Example 3-13. Tabular Form—Saving Collection Data Back to the Source
FUNCTION validate_collection_data RETURN VARCHAR2; PROCEDURE update_table_from_collection ( p_deptno IN NUMBER, p_message OUT VARCHAR2 ); FUNCTION validate_collection_data RETURN VARCHAR2 IS v_message VARCHAR2 (4000); v_checksum VARCHAR2 (400); BEGIN FOR c IN (SELECT empno, ename, job, mgr, hiredate, sal, comm, deptno, valid, checksum, delete_flag FROM emp_coll_v WHERE checksum IS NOT NULL) LOOP SELECT wwv_flow_item.md5 (empno, ename, job, mgr, hiredate, sal, comm, deptno,
valid ) INTO v_checksum FROM emp WHERE empno = c.empno; IF c.checksum <> v_checksum THEN v_message := v_message || '<br/>' || 'Empno: ' || c.empno || ': Snapshot too old.'; END IF; END LOOP; v_message := LTRIM (v_message, '<br/>'), RETURN v_message; END validate_collection_data; PROCEDURE update_table_from_collection ( p_deptno IN NUMBER, p_message OUT VARCHAR2 ) IS v_ins_count INTEGER := 0; v_upd_count INTEGER := 0; v_del_count INTEGER := 0; v_message VARCHAR2 (4000); BEGIN FOR c IN (SELECT empno, ename, job, mgr, hiredate, sal, comm, deptno, valid, checksum, delete_flag FROM emp_coll_v) LOOP IF c.delete_flag IS NULL AND c.checksum IS NOT NULL THEN IF c.checksum <> wwv_flow_item.md5 (c.empno, c.ename, c.job, c.mgr, c.hiredate, c.sal, c.comm, c.deptno, c.valid ) THEN UPDATE emp SET ename = c.ename,
job = c.job, mgr = c.mgr, hiredate = c.hiredate, sal = c.sal, comm = c.comm, deptno = c.deptno WHERE empno = c.empno; v_upd_count := v_upd_count + 1; END IF; ELSIF c.delete_flag IS NULL AND c.checksum IS NULL THEN INSERT INTO emp (empno, ename, job, mgr, hiredate, sal, comm, deptno, valid ) VALUES (c.empno, c.ename, c.job, c.mgr, c.hiredate, c.sal, c.comm, c.deptno, c.valid ); v_ins_count := v_ins_count + 1; ELSIF c.delete_flag IS NOT NULL AND c.checksum IS NOT NULL THEN DELETE FROM emp WHERE empno = c.empno; v_ins_count := v_ins_count + 1; END IF; END LOOP; p_message := p_message || '<br/>' || v_ins_count || ' row(s) inserted. ' || v_upd_count || ' row(s) updated. ' || v_del_count || ' row(s) deleted.'; create_emp_collection (p_deptno, v_message); p_message := p_message || '<br/>' || v_message; p_message := LTRIM (p_message, '<br/>'), EXCEPTION WHEN OTHERS THEN p_message := SQLERRM; END update_table_from_collection;
To be able to do an update of the table, you will need to create a button on your page 3 and name it
SAVE_DATA
You can now call the validation function and the update process on application page 3. You will create a page level validation of type PL/SQL Function Returning Error Message – Validate Collection Data and make it conditional upon the new button you created (SAVE_DATA
):
BEGIN RETURN tab_form_emp_pkg.validate_collection_data; END;
The process Save Collection to Table will be on submit, and it will be conditional when the button is pressed (SAVE_DATA
). You will also enter
&T_MESSAGE.
in the Success Message of the process
BEGIN tab_form_emp_pkg.update_table_from_collection (:p3_deptno,:t_message); END;
Now make a couple of changes in your collection and confirm that the code works as expected. Figure 3-33 shows some changes to be saved to the source table. Figure 3-34 shows the success message from saving those changes.
The examples shown here are not complete, and they focus on single functionalities. If you want to use the example code in your applications, you will need to complete functionality that I've omitted in order to keep the examples simple. For example, you will need to take care of newly-added rows when updating a collection using an On-Demand Process. The code shown here doesn't do that. However, that functionality should be fairly easy to add.
This section will cover some other useful techniques for working with tabular forms. These include
APEX 4 introduced a new item type called "Text Field with autocomplete". Such an item is a simple select list generated using jQuery libraries. This type of select list allows editing and shows the results as you type. The only thing you need to do is to define a SQL query like this:
SELECT ename FROM emp
This item type is not available for Tabular Forms. However, you can implement the same functionality using a technique written about by Tyler Muth in his blog at
http://tylermuth.wordpress.com/2010/03/16/jquery-autocomplete-for-apex/
The example to follow uses some of Tyler's code to create an autocomplete select list for the column JOB
in our example of wizard-generated Tabular Form. Before you do that, you will need to do some preparation work.
Now add the following to the tab_form_emp_pkg
package:
Procedure for creating a list of jobs based on the EMP table used for the On-Demand Process
Function for generating JavaScript
These functions and procedures are shown in Listing 3-14.
Example 3-14. Tabular Form—Autocomplete Functions and Procedures
PROCEDURE get_job; FUNCTION get_autocomplete ( p_item IN VARCHAR2, p_rownum IN VARCHAR2, p_width IN NUMBER, p_process IN VARCHAR2
) RETURN VARCHAR2; PROCEDURE get_job IS v_search emp.job%TYPE; BEGIN EXECUTE IMMEDIATE 'alter session set NLS_SORT=BINARY_CI'; EXECUTE IMMEDIATE 'alter session set NLS_COMP=LINGUISTIC'; v_search := REPLACE (wwv_flow.g_x01, '*', '%'), FOR i IN (SELECT DISTINCT job FROM emp WHERE job LIKE '%' || v_search || '%') LOOP HTP.p (i.job); END LOOP; END get_job; FUNCTION get_autocomplete ( p_item IN VARCHAR2, p_rownum IN VARCHAR2, p_width IN NUMBER, p_process IN VARCHAR2 ) RETURN VARCHAR2 IS v_rownum VARCHAR2 (20); v_item VARCHAR2 (20); v_script VARCHAR2 (1000); BEGIN v_rownum := p_rownum; v_item := p_item || '_' || v_rownum; v_script := '<script type="text/javascript">' || '$(document).ready( function() {' || '$("#' || v_item || '").autocomplete(''APEX'', {' || 'apexProcess: ''' || p_process || ''',' || 'width: ' || p_width || ',' || 'multiple: false,' || 'matchContains: true,' || 'cacheLength: 1,' || 'max: 100,' || 'delay: 150,' || 'minChars: 1,' || 'matchSubset: false' || '});' || '});' || '</script>'; RETURN v_script; END get_autocomplete;
There are only three more things you need to add or change to get your autocomplete list working:
Modify the SQL created for the Tabular Form and include a call to the function
get_autocomplete
Create an On-Demand Process in the Application Processes:
get_job
Modify the HTML Expression for the affected column JOB
The modified SQL is shown in Listing 3-15.
Example 3-15. Tabular Form— SQL for Simple Autocomplete
SELECT empno, empno || tab_form_emp_pkg.get_autocomplete ('f04', '#ROWNUM#', 85, 'get_job') empno_display,
ename, job, mgr, hiredate, sal, comm, deptno, valid FROM emp
In this SQL you concatenate the return value of the get_autocomplete
function with an existing display only column, EMPNO_DISPLAY
. This function will have four input parameters for
The referenced item for which you create this autocomplete list:
f04
Row number for the current row
The width in pixels for your select list
The name of the corresponding On-Demand Process
For the On-Demand Process get_job
, you will use a simple PL/SQL block:
BEGIN tab_form_emp_pkg.get_job; END;
Finally, modify the HTML Expression of the Tabular Form column JOB
to
<input type="text" id="f04_#ROWNUM#" value="#JOB#" maxlength="2000" size="12" name="f04" autocomplete="off" class="ac_input">
You will also need to change the Column Properties for the column EMPNO_DISPLAY
from Display as Text... to Standard Report Column.
If you now run the page, edit the JOB
column, and start typing, you should get a similar result to that shown in Figure 3-35.
The example that follows is based on the page you created in the previous section, with one addition: it will show an autocomplete select list and upon selection return a key value into another column. The EMP
table has a column MGR
showing the manager's ID. You will create an additional column showing manager names (MGR SEARCH
) and parse the selected manager's ID into the MGR
column.
For this, you will need an additional procedure and two additional functions in your package:
A procedure for creating a list of managers based on the EMP table and the selected manager used for the On-Demand Process.
A function for generating JavaScript. This function will also create an additional text item used for the autocomplete list. You will assign the array 50 to it.
An additional function for returning ENAME from the EMP table for the existing MGR entries in the EMP table.
These additional procedures are shown in Listing 3-16.
Example 3-16. Tabular Form—SQL for Autocomplete Returning Key Value
FUNCTION get_manager_for_id (p_mgr IN NUMBER) RETURN VARCHAR2; PROCEDURE get_manager; FUNCTION get_autocomplete_key ( p_value IN VARCHAR2, p_key_item IN VARCHAR2, p_rownum IN VARCHAR2, p_width IN NUMBER, p_process IN VARCHAR2, p_max_length IN NUMBER DEFAULT 80, p_size IN NUMBER DEFAULT 80 ) RETURN VARCHAR2; FUNCTION get_manager_for_id (p_mgr IN NUMBER) RETURN VARCHAR2 IS v_ename emp.ename%TYPE; BEGIN FOR c IN (SELECT ename FROM emp WHERE empno = p_mgr) LOOP v_ename := c.ename; END LOOP; RETURN v_ename; END get_manager_for_id; PROCEDURE get_manager IS v_search VARCHAR2 (255); v_output VARCHAR2 (400);
BEGIN EXECUTE IMMEDIATE 'alter session set NLS_SORT=BINARY_CI'; EXECUTE IMMEDIATE 'alter session set NLS_COMP=LINGUISTIC'; v_search := REPLACE (wwv_flow.g_x01, '*', '%'), FOR i IN (SELECT ename, empno FROM emp WHERE ename LIKE '%' || v_search || '%' AND empno IN (SELECT mgr FROM emp)) LOOP v_output := i.ename || '|' || i.empno; HTP.p (v_output); END LOOP; END get_manager; FUNCTION get_autocomplete_key ( p_value IN VARCHAR2, p_key_item IN VARCHAR2, p_rownum IN VARCHAR2, p_width IN NUMBER, p_process IN VARCHAR2, p_max_length IN NUMBER DEFAULT 80, p_size IN NUMBER DEFAULT 80 ) RETURN VARCHAR2 IS v_rownum VARCHAR2 (20); v_name VARCHAR2 (5) := 'f50'; v_item VARCHAR2 (20); v_item_html VARCHAR2 (3000); v_key_item VARCHAR2 (20); v_script VARCHAR2 (1000); v_output_item VARCHAR2 (4000); a_rownum VARCHAR2 (400); BEGIN v_rownum := p_rownum; v_item := v_name || '_' || v_rownum; v_item_html := '<input type="text" id="' || v_item || '" value="' || p_value || '" maxlength="' || p_max_length || '" size="' || p_size || '" name="' || v_name || '" ' || 'autocomplete="off" class="ac_input"'
|| 'style="width:' || p_width || 'px">'; v_key_item := p_key_item || '_' || v_rownum; v_script := '<script type="text/javascript">' || '$(document).ready( function() {' || '$("#' || v_item || '").autocomplete(''APEX'', {' || 'apexProcess: ''' || p_process || ''',' || 'width: ' || p_width || ',' || 'multiple: false,' || 'matchContains: true,' || 'cacheLength: 1,' || 'max: 100,' || 'delay: 150,' || 'minChars: 1,' || 'matchSubset: false,' || 'x02: ''foo'',' || 'x03: $(''#' || v_key_item || ''').val()' || '});' || '$("#' || v_item || '").result(function(event, data, formatted) {' || 'if (data){' || '$("#' || v_key_item || '").val(data[1]);' || '}' || '});' || '});' || '</script>'; v_output_item := v_item_html || v_script; RETURN v_output_item; END get_autocomplete_key;
After adding this code to your package you can start modifying your Tabular Form SQL. The modified SQL is shown in Listing 3-17.
Example 3-17. Tabular Form—SQL for Autocomplete Returning Key Value
SELECT empno, empno || tab_form_emp_pkg.get_autocomplete ('f04', '#ROWNUM#', 85, 'get_job') empno_display, ename, job,
tab_form_emp_pkg.get_autocomplete_key (tab_form_emp_pkg.get_manager_for_id (mgr), 'f05', '#ROWNUM#', 85, 'get_manager' ) mgr_search, mgr, hiredate, sal, comm, deptno, valid FROM emp
Listing 3-17 shows the get_autocomplete_key function between columns JOB
and MGR
. This function will have input parameters for
ENAME
of the existing manager returned by the function get_manager_for_id
The referenced item in which you will parse the key value f05
Row number for the current row
The width in pixels for our select list
The name of the corresponding On-Demand Process
To get the newly created column MGR SEARCH
into the right order, you will edit the Tabular Form properties and place the column MGR SEARCH
between columns JOB
and MGR
.
For the On-Demand Process get_manager
, use a simple PL/SQL block:
BEGIN tab_form_emp_pkg.get_manager; END;
Finally, modify the HTML Expression of the Tabular Form column MGR to
<input type="text" id="f05_#ROWNUM#" value="#MGR#" maxlength="2000" size="16" name="f05" autocomplete="off">
You will also need to change the Column Properties of the MGR_SEARCH column from Display as Text... to Standard Report Column.
If you now run the page, edit the MGR SEARCH
column, and start typing, you should get the list of managers. If you select one of them, the entry in the column MGR
should change to the corresponding EMPNO
of the selected ENAME
. The result should look similar to Figure 3-36.
If you now change the value and select another manager, the MGR column value should change correspondingly, as shown in Figure 3-37.
You can now hide the MGR
column by changing the HTML Expression from type text
to type hidden
:
<input type="hidden" id="f05_#ROWNUM#" value="#MGR#" maxlength="2000" size="16" name="f05" autocomplete="off">
Then remove the column heading in the Report Attributes.
Moving the column to the end of the report or simply changing its position will cause an error while trying to submit the page. If you must move the column, then undo the changes in the HTML Expression; change the parameters in the function used in the SQL statement; check your processes, procedures, and functions in order to make sure you are referencing the right array; and then move the column. This example will not work with the new way of adding rows in Tabular Forms. You will need to code it by adding an UNION ALL statement to your SQL which will add a row upon a REQUEST on page load.
In one of my recent projects the customer had a requirement to easily clone rows in a tabular form. My colleague and friend Linh Dinh helped me devise a solution, which you can read about in his blog at
http://www.dinh.de/wordpress/
We developed a small piece of code to clone rows. The goal was to clone one or more rows, change them, and save them. Our solution allows for new rows to be added at any position in the table, not just at the end .
You'll need to do the following to prepare a new page for a demonstration of this approach:
Copy page 1 to page 4 and remove all of the custom coding created earlier.
Change the SQL of the Tabular Form report.
Add a small bit of JavaScript code to the page header.
Modify the column holding the image/link for this process.
Following is the new SQL for the Tabular Form report:
SELECT empno, NULL clone, empno empno_display, ename, job, mgr, hiredate, sal, comm, deptno, valid FROM emp
This SQL will create a new column CLONE
, to hold the image for cloning the selected row. You will need to move that new column right after the checkbox. You will edit the column properties and put a link in the HTML Expression
<a href="#" onclick="javascript:fn_CloneRow(this);"> <img src="#IMAGE_PREFIX#copy.gif" alt=""></a>
You will also need to change the column properties from Display as Text... to Standard Report Column.
Finally, you will add a small bit of JavaScript to the page header, as shown in Listing 3-18.
Example 3-18. Tabular Form—Javascript for Cloning Rows
<script type="text/javascript"> function fn_delete(pThis) { var l_tr=$x_UpTill(pThis,'TR'), l_tr.parentNode.removeChild(l_tr); } function fn_CloneRow(pThis) { $(pThis).parent().parent().clone(true,false).insertAfter($(pThis).parent().parent()); newRow = $(pThis).parent().parent().next(); newRow.find('[type=hidden]').val(''), newRow.find('[name=fcs]').val('Z'),
newRow.find('[type=checkbox]').remove(); html_RowHighlight($(newRow).get(0),"#9E0200"); // Delete Cloned Row newLink = $(newRow).find('img')[0]; newLink.src = '/i/del.gif'; $(newLink).parent().removeAttr('onclick'), $(newLink).parent().get(0).onclick = function() { fn_delete(this)}; } </script>
Figure 3-38 shows the results after clicking a few clone icons.
This script can easily be extended to create empty rows as well. You could also change the IDs of the new items (currently those are copied). Please, use this with caution. It is suitable for Excel-like data management only, where you have a plain grid of text-only items and would like to easily multiply the rows without taking much care of the data integrity. I could imagine using it in combination with collections.
Tabular Forms are one of the greatest features in APEX. Without this functionality, APEX 4 simply wouldn't be as successful as it is. However, there is still potential to improve on this feature and make it even better. Desirable improvements include
Multiple Tabular Forms per page
Dynamic Actions for Tabular Form elements
Increasing the number of arrays
More options for adding/copying rows
Item options equal to those of page items
In this chapter, we've looked at many techniques for using Tabular Forms, and for implementing some of the functionality in the preceding list.
For even more information about Tabular Forms, you can visit a demo application that I maintain at the following URL: http://apex.oracle.com/pls/otn/f?p=31517:1
. Look in Section VI of the demo for examples of Tabular Form usage.