Chapter 3. Tabular Forms

by Denes Kubicek

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.

Changes in APEX 4.0

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.

Constraints

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.

Typical error message after referencing a nonexisting array

Figure 3-1. Typical error message after referencing a nonexisting array

Warning

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.

Purpose of Tabular Forms

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.

Tabular Forms Are Not Spreadsheets

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.

New Features in APEX 4

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.

New Item Types

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

Single Checkbox

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:

  1. 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.).

  2. This will create the required tables you need for this demonstration (EMP and DEPT).

  3. Change the EMP table by adding an additional column and changing one of the columns, as shown in Listing 3-1.

  4. Create a new application.

  5. Create a tabular form using a wizard based on the EMP table and include all the columns.

Note

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 Simple Checkbox item type

Figure 3-2. Using Simple Checkbox item type

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.

Simple Checkbox LOV

Figure 3-3. Simple Checkbox LOV

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.

Simple Checkbox in a Tabular Form

Figure 3-4. Simple Checkbox in a Tabular Form

EMP table

Figure 3-5. EMP table

Warning

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.

jQuery Date Picker

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.

jQuery Date Picker in a Tabular Form

Figure 3-6. jQuery Date Picker in a Tabular Form

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.

Radio Group

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
Changing Column Attributes to Radio Group

Figure 3-7. Changing Column Attributes to Radio Group

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.

Tabular Form with Radio Group column

Figure 3-8. Tabular Form with Radio Group column

Popup Key LOV

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).

Select list error

Figure 3-9. Select list error

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.

Tabular Form with a Popup Key LOV

Figure 3-10. Tabular Form with a Popup Key LOV

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.

Declarative Validations

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

  • NOT NULL validations

  • Column String Comparison validations

There are several Column String Comparison validations and you can view them in the Figure 3-11.

Tabular Form validations—Column String Comparison

Figure 3-11. Tabular Form validations—Column String Comparison

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).

Validations Created by Tabular Form Wizard

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.

Tabular Form validations—created by the wizard

Figure 3-12. Tabular Form validations—created by the wizard

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.

Highlighting Validation Errors

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.

Tabular Form validations

Figure 3-13. Tabular Form validations

Warning

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.

Tabular Form validations – error highlighting

Figure 3-14. Tabular Form validations – error highlighting

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.

Tabular Form validations—substitution string for column names

Figure 3-15. Tabular Form validations—substitution string for column names

Other Features

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.

Lost Update Protection

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.

Tabular Form— lost update protection

Figure 3-16. Tabular Form— lost update protection

Client-Side Add Row Functionality

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.

Reduced Number of Processes

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:

  • Apply MRU process for saving data on submit

  • Apply MRU process for saving data after adding a new row

  • Apply MRD process for deleting of data

  • AddRow process for adding one or multiple rows

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.

Future Features We'd Like to See

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.

Custom Coding in Tabular Forms

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.

Processing the Correct Rows

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:

  1. 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.

  2. Set the existing ApplyMRD process to Conditional Never, so it doesn't run.

  3. 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;
  4. Use the substitution string for the success message:

    &T_MESSAGE.
  5. Make the process conditional so it runs on condition type PL/SQL Expression:

    :REQUEST IN ('MULTI_ROW_DELETE')
  6. Confirm and create a process.

Warning

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.

Tabular Form—deleting records using custom process

Figure 3-17. Tabular Form—deleting records using custom process

Tabular Form— deleting records—results

Figure 3-18. Tabular Form— deleting records—results

Run the following block of code in SQL Workshop in order to restore the EMP table:

BEGIN
   tab_form_emp_pkg.restore_tables;
END;

Data Integrity

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.

Checksum

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.

Tabular Form— checksum

Figure 3-19. Tabular Form— checksum

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.

Validations

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;

Warning

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.

Tabular Form—Data Integrity Validation Error

Figure 3-20. Tabular Form—Data Integrity Validation Error

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.

Manual Tabular Forms

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.

Warning

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.

Click the Add Row button to add a new row at the top of the form.

Figure 3-21. Click the Add Row button to add a new row at the top of the form.

Submit the form and notice the success message.

Figure 3-22. Submit the form and notice the success message.

Notice that the new row is now at the bottom of the form.

Figure 3-23. Notice that the new row is now at the bottom of the form.

Try entering an invalid commission value.

Figure 3-24. Try entering an invalid commission value.

Notice the error message from entering the invalid value.

Figure 3-25. Notice the error message from entering the invalid value.

Warning

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.

Tabular Forms and 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.

A window of data for department 30

Figure 3-26. A window of data for department 30

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.

Creating a Collection

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.

Tabular Form on APEX Collection

Figure 3-27. Tabular Form on APEX Collection

The question now is, how do you update your collection? Following are three possible methods for updating the collection:

  • Instead of Trigger method

  • Writing packaged procedures for updating of collections

  • Using On-Demand Process and Ajax for collection updates

The sections to follow describe each of these methods in detail.

Using Instead of Trigger

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.

Tabular Form on APEX Collection—Instead of trigger error

Figure 3-28. Tabular Form on APEX Collection—Instead of trigger error

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.

Writing Packaged Update Procedures

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.

Making a change to a Tabular Form built atop a collection

Figure 3-29. Making a change to a Tabular Form built atop a collection

You should get a similar result to the one shown in Figure 3-30.

A success message from making the change shown in Figure 3-29

Figure 3-30. A success message from making the change shown in Figure 3-29

Updating a Collection via an On-Demand Process

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

Note

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.

Updating rows on the tabular form

Figure 3-31. Updating rows on the tabular form

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.

Watching the On-Demand Process from Firebug

Figure 3-32. Watching the On-Demand Process from Firebug

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.

Changes to be saved to the source table

Figure 3-33. Changes to be saved to the source table

The success message

Figure 3-34. The success message

Warning

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.

Interesting Techniques

This section will cover some other useful techniques for working with tabular forms. These include

  • Simple Autocomplete

  • Autocomplete returning key value

  • Clone Row functionality

Simple Autocomplete

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.

  1. Download the jquery.autocompleteApex1.2.js available in Tyler's blog.

  2. Upload it to your workspace.

  3. Reference it on your page:

    <script type="text/javascript" src="#WORKSPACE_IMAGES#jquery.autocompleteApex1.2.js"></script>

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"
Tabular Form— SQL for Simple Autocomplete
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.

Tabular Form—Autocomplete on JOB column

Figure 3-35. Tabular Form—Autocomplete on JOB column

Autocomplete Returning Key Value

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"
Tabular Form—SQL for Autocomplete Returning Key Value
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.

Selecting a value from the list

Figure 3-36. Selecting a value from the list

If you now change the value and select another manager, the MGR column value should change correspondingly, as shown in Figure 3-37.

Returning the key value

Figure 3-37. Returning the key value

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"
Returning the key value
autocomplete="off">

Then remove the column heading in the Report Attributes.

Warning

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.

Clone Rows

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.

Tabular Form— cloning rows

Figure 3-38. Tabular Form— cloning rows

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.

Summary

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.

Note

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset