Jun 30

A while back we posted on adding grid filters to our APEX Ext Grid template by simply setting attributes under the “Column Definition” setting in the report definition.

In order to support grid filters on our APEX grid/report we need to create a number of hidden items on the page to filter the query by. e.g. here’s a query example

SELECT geoname_id
,      name
,      modification_date
,      latitude
,      longitude
,      population
FROM geoname
WHERE  (modification_date = to_date(:P9_EXT_MODIFICATION_DATE_EQ, 'DD-MON-YYYY') OR :P 9_EXT_MODIFICATION_DATE_EQ IS NULL)
AND instr(upper("NAME"),upper(nvl(:P9_EXT_NAME,"NAME"))) > 0
AND (geoname_id = :P 9_EXT_GEONAME_ID_EQ OR :P 9_EXT_GEONAME_ID_EQ IS NULL)
AND (geoname_id >= :P 9_EXT_GEONAME_ID_GT OR :P 9_EXT_GEONAME_ID_GT IS NULL)
AND (geoname_id <= :P 9_EXT_GEONAME_ID_LT OR :P 9_EXT_GEONAME_ID_LT IS NULL)
AND modification_date BETWEEN to_date(nvl(:P9_EXT_MODIFICATION_DATE_GT,'01-JAN-0001'), 'DD-MON-YYYY') AND to_date(nvl(:P9_EXT_MODIFICATION_DATE_LT,'01-JAN-2999'), 'DD-MON-YYYY')
AND (population > :P 9_EXT_POPULATION_GT OR :P 9_EXT_POPULATION_GT IS NULL)

For string filters we simply do a “like” comparison or “soundex” so we only require 1 hidden item. However for date and numeric filters we have a number of options, “Less Than / Before”, “Equal To / On”, and “Greater Than / After” so we need three hidden items for the one column filter.

We are faced with two options, we can either create these hidden items manually…. boring! or we can use the APEX data dictionary and API to dynamically create them using our own custom PLSQL. This is where APEX really gets really interesting, we can use wwv_flow_api to generate the items as well as a region to hold them. To make it that little bit sweeter, we’ll also add the PLSQL call to an application process that runs before header (this gives us the benefit of being within the APEX runtime engine), which means that for every new grid page we create, when we view the page in runtime mode (first time only) it will auto create the items for us! When it comes time for our production release we will simply set the condition of the application level process to “Never” to avoid any unnecessary processing.

When we create the hidden items, we need work out the data type for the column filter and create the necessary supporting hidden items which will appear in our query filter. We simply use the APEX data dictionary to get the report items with filtering enabled and then query the databae data dictionary to get the data type in order to work out what hidden items we need to generate.

Here’s an extract of our PLSQL…

    IF NOT v_region_exists THEN
      vId := wwv_flow_id.next_val;
      vPlugId := vId;
      wwv_flow_api.set_version(wwv_flow_api.g_compatable_from_version);
      wwv_flow_api.create_page_plug
      ( p_id                        => vId
      , p_flow_id                   => v('APP_ID')
      , p_page_id                   => v('APP_PAGE_ID')
      , p_plug_name                 => 'APExtJS Hidden Items'
      , p_region_name               => 'APExtJS Hidden Items'
      , p_plug_display_sequence     => '10000'
      , p_plug_template             => 0
      );
    END IF;
    --
    -- Lets have a look at the current page and see if there is a apex report
    -- defined and filtering enabled by setting the reference table fields
    -- if so we will create a hidden item if one does not exist
    --
    FOR c IN (
      SELECT v.column_alias
      ,      v.reference_schema
      ,      v.reference_table_name
      ,      v.reference_column_name
      FROM   apex_application_page_rpt_cols v
      WHERE  v.application_id        = v('APP_ID')
      AND    v.page_id               = v('APP_PAGE_ID')
      AND    upper(v.column_alias)   <> 'TOTALROWS'
      AND    v.reference_schema      IS NOT NULL
      AND    v.reference_table_name  IS NOT NULL
      AND    v.reference_column_name IS NOT NULL
      AND NOT EXISTS (SELECT item_name
                      FROM   apex_application_page_items
                      WHERE  application_id = v.application_id
                      AND    page_id = v.page_id
                      AND    (item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias
                              OR item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias||'_LT'
                              OR item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias||'_EQ'
                              OR item_name = 'P'||v('APP_PAGE_ID')||'_EXT_'||v.column_alias||'_GT'
                              )
                    )
      ORDER BY v.region_id, v.display_sequence
    ) LOOP
      FOR c1  IN (SELECT data_type
                  FROM   all_tab_columns
                  WHERE  owner       = c.reference_schema
                  AND    table_name  = c.reference_table_name
                  AND    column_name = c.reference_column_name)
      LOOP
        IF c1.data_type IN ('VARCHAR2','CLOB','NVARCHAR2','NCLOB','CHAR') THEN
          --
          -- The following was taken from John Scotts book Pro Application Express
          -- page 675
          --
          vId := wwv_flow_id.next_val;
          wwv_flow_api.set_version(wwv_flow_api.g_compatable_from_version);
          wwv_flow_api.create_page_item
          ( p_id                        => vId
          , p_flow_id                   => v('APP_ID')
          , p_flow_step_id              => v('APP_PAGE_ID')
          , p_display_as                => 'HIDDEN'
          , p_item_plug_id              => vPlugId
          , p_name                      => 'P' || v('APP_PAGE_ID') || '_EXT_' || c.column_alias
          );
        ELSIF c1.data_type IN ('DATE','NUMBER','FLOAT') THEN
          --
          -- The following was taken from John Scotts book Pro Application Express
          -- page 675
          --
          vId := wwv_flow_id.next_val;
          wwv_flow_api.set_version(wwv_flow_api.g_compatable_from_version);
          wwv_flow_api.create_page_item
          ( p_id                        => vId
          , p_flow_id                   => v('APP_ID')
          , p_flow_step_id              => v('APP_PAGE_ID')
          , p_display_as                => 'HIDDEN'
          , p_item_plug_id              => vPlugId
          , p_name                      => 'P' || v('APP_PAGE_ID') || '_EXT_' || c.column_alias ||'_LT'
          );

If you interested in the dynamic generation of APEX components then you should have a look at the APEXGEN package, though it is a little disappointing it hasn’t been updated since it’s initial release, but the concept is fantastic. It won’t be long before we will be creating APEX applications with an Ext interface just using an ER model generated by SQL Developer’s new modeling tool. Of course there are a lot of code generators out there but none the likes of APEX with it’s excellent customization framework, so watch this space!

Jun 11

As previously stated we are looking to integrate ExtJS with as many of the apex wizard driven components and templates as possible. This post focuses on the tabular form and how we integrated it into the Ext grid whilst retaining the APEX form processing functionality e.g. “ApplyMRU” and “ApplyMRD”.

APEX ExtJS - APEX Form Component Selection

In the previous posts we have highlighted that we have created a region and report template that takes a standard SQL report and turns it into and Ext grid. We are reusing the same templates on the tabular form. What this highlights is code reusability and centralization, we don’t need to create another set of templates for the tabular form. That said it does come with some restrictions as we can’t support a Popup LOV or APEX Date Picker (we’ll explain why shortly). We will also be reusing the button template that previously posted about to add the buttons to the grid toolbar and the delete button will reuse the Modal window popup for confirm deletion prompt and simply post the page using “doSubmit”. It’s funny how nicely all the pieces start fitting together, design is key, remember that!

Since we are simply using a template to enable the Ext grid functionality whatever is defined under the report definition will be displayed in the Ext grid. So if our columns are select lists, then select lists will be displayed, same for text fields, and textareas. This also means that they will be generated with the right name and id’s which are parameters for APEX wwv_flow.accept package call e.g. “f01″. This is how we can still use the APEX MRU,MRD processing functionality.

TheProblems we Encountered:

  1. First Problem: Popup LOV’s and APEX Date Picker cannot be supported (it broke our report template as we generate a JSON object in javascript) as previously stated, this is because the APEX engine generates some additional javascript into the report row entries which is outside of our control so columns cannot be defined with these two types. We will however provide an Ext equivalent (which is better) simply by defining the item as a text field but using the CSS class to determine what Ext widget to override it with, i.e. ext-date, ext-popuplov.
  2. Next Problem: our region template broke, this was because the APEX engine automatically appends some javascript code to the #BODY# subsititution tag for tabular forms to control row highlighting on selection. e.g.

            <script type="text/javascript">
            <!--
                var rowStyle      = new Array(5);
                var rowActive     = new Array(5);
                var rowStyleHover = new Array(5);
    
                rowStyle[1]='';
                rowStyleHover[1]='';
                rowActive[1]='N';
                rowStyle[2]='';
                rowStyleHover[2]='';
                rowActive[2]='N';
                rowStyle[3]='';
                rowStyleHover[3]='';
                rowActive[3]='N';
                rowStyle[4]='';
                rowStyleHover[4]='';
                rowActive[4]='N';
                rowStyle[5]='';
                rowStyleHover[5]='';
                rowActive[5]='N';
    
                function checkAll(masterCheckbox) {
                    if (masterCheckbox.checked) {
                        for (var i = 0; i<document.wwv_flow.f01.length; i++) {
                            if (document.wwv_flow.f01[i].checked==false) {
                              document.wwv_flow.f01[i].checked=true;
                              highlight_row(document.wwv_flow.f01[i],i);
                            }
                        }
                    } else {
                        rowsNotChecked=0;
                        for (var i = 0; i<document.wwv_flow.f01.length; i++) {
                           if (document.wwv_flow.f01[i].checked!=true) {
                               rowsNotChecked=rowsNotChecked+1;
                           }
                        }
                        if (rowsNotChecked==0) {
                            for (var i = 0; i<document.wwv_flow.f01.length; i++) {
                                if (document.wwv_flow.f01[i].checked==true) {
                                  document.wwv_flow.f01[i].checked=false;
                                  highlight_row(document.wwv_flow.f01[i],i);
                                }
                            }
                        }
                    }
                }
    
                function highlight_row(checkBoxElemement,currentRowNum) {
                    if(checkBoxElemement.checked==true) {
                        for( var i = 0; i < checkBoxElemement.parentNode.parentNode.childNodes.length; i++ ) {
                            if (checkBoxElemement.parentNode.parentNode.childNodes[i].tagName=='TD') {
                                if(rowActive=='Y') {
                                    rowStyle[currentRowNum] = rowStyleHover[currentRowNum];
                                } else {
                                    rowStyle[currentRowNum] = checkBoxElemement.parentNode.parentNode.childNodes[i].style.backgroundColor;
                                }
                                checkBoxElemement.parentNode.parentNode.childNodes[i].style.backgroundColor = '';
                            }
                        }
                        rowStyleHover[currentRowNum] =  '';
                    } else {
                          for( var i = 0; i < checkBoxElemement.parentNode.parentNode.childNodes.length; i++ ) {
                              if (checkBoxElemement.parentNode.parentNode.childNodes[i].tagName=='TD') {
                                  checkBoxElemement.parentNode.parentNode.childNodes[i].style.backgroundColor =  rowStyle[currentRowNum];
                                  rowStyleHover[currentRowNum] =  rowStyle[currentRowNum];
                                  document.wwv_flow.x02.checked=false;
                              }
                          }
                    }
                }
            // -->
            </script>
    

    The solution was to put in some extra start and end script tags in the report template and region template (remember we need two templates to work together to achieve the Ext grid). Originally our region template had an open script tag at the top and a close script tag after the #BODY# substitution string.We simply put a close script tag at the end of the report template and a new script start tag immediately after the #BODY# tag in the region template.

  3. Next Problem: Add Rows via page submission and the application process “Add Rows” would not work as it was not output by the report template. The apex engine auto generated this code so this was a big problem. Our solution (trust me there’s always one with anything, it can just take a little time to figure out) was to create a javascript function to add the new row to the grid. Its achieved by copying the last row in the Ext store and modifying the row HTML data before adding the extra row back to the store (note the actual row HTML has been generated by APEX not Ext). The HTML modification required incrementing input id’s and nulling values. I’d recommend that you familiarize yourself with regular expressions as it makes this task achievable in just a few lines of code.
    lRowArray[i] = lRowArray[i].replace(/(id=\"f\d+_).*?\"/g, '$1' + PadDigits(lastIndex,0,4) + '"');
    

    As for working out the right ID to increment the HTML input elements to i.e. “f1_0011″ we simply counted the records in the Ext store.

    That’s the great thing about Ext is that your report data is held in an accessible JSON object, and in this example our report data is not just data it also happens to be HTML, i.e. input fields and select lists so this means we don’t need to query the server for adding another row, which is more “end user” friendly.

    This gave us the benefit of being able to add more than one row at a time before committing the changes, as multiple “Add Row” clicks on a normal tabular form saves the new record each time.

  4. Next Problem: When copying the Ext store row we needed to make sure that the “fcs” hidden item, which is used for a row checksum to compare if the data had changed, contained a dummy value, as when we initially erased the value via a regex global replace the rows weren’t saved e.g.
    <input type="hidden" value="" name="fcs"/>

    was a problem. Solution: we simply changed it to

    <input type="hidden" value="12345" name="fcs"/>

    and the rows were then saved.

  5. Next Problem: we had to disable Ext grid filtering for dates and numbers. This was due to us defining the data type of the column in the grid column model and the Ext renderer replacing our HTML with “NaN” which is basically a return error identifier.

As we were missing the date picker field type we simply added an onload event to the Store which converted text items into Ext date pickers based on the items having a class setting of “ext-form-date-picker”. The class setting was set in the “Element Attributes” in the “Tabular Form Element” section. We also allowed all textareas to be manually resized e.g.

pStore.on({load: function() { extDateFields('grid'+pRegionID);extResizableByID('grid'+pRegionID); } });

// convert inputs with class "ext-form-date-picker" to date fields
function extDateFields(pID){
  var els=Ext.get(pID).select("[class=ext-form-date-picker]");
  els.each(function(el){
    if (el.dom.className.indexOf("aext-form-date") == -1) {
       el.dom.className += " "+"aext-form-date";
       var df = new Ext.form.DateField({"applyTo": el.dom.id, "format":'d-M-Y',"altFormats":'j|j/n|j/n/y|j/n/Y|j-M|j-M-y|j-M-Y'});
       df.render();
    }
  })
}
function extResizableByID(pID){
  var els=Ext.get(pID).select('textarea',true);
  els.each(function(e){
    if (e.dom.className.indexOf("aext-resizeable") == -1) {
       e.dom.className += " "+"aext-resizeable";
       var resizeMe = new Ext.Resizable(e, {
          wrap:true,
          pinned:true,
          width:e.getWidth(),
          height:e.getHeight(),
          minWidth:e.getWidth(),
          minHeight: e.getHeight()
       });
    }
  })
}

The end result is an APEX tabular form displayed in an Ext grid which supports inserts/updates/deletes, here’s an example from our development environment….

APEX ExtJS - APEX Tabular Form in an Ext Grid

preload preload preload