6 Replies Latest reply on Jan 28, 2010 2:36 PM by jbalunas

    RichFaces 4.0 Core: AJAX - JavaScript components

    nbelaevski

      We have a lot of JavaScript code in RichFaces components, because of their rich client-side functionality. Also we're providing public JavaScript API to let developers control components on the client side; there are #{rich:component()} function and rich:componentControl component that both utilize this feature. So, let's start with the information on how RichFaces JS API call is being done using #{rich:component()} function as an example.

      When JS component instance is first created, it is attached to the main DOM element of this component, using "component" property. Imagine

      <rich:calendar id="calendar" />
      . This is rendered as (code adopted for ease of understanding)
      <span id="form:calendar">
       <script>new Calendar('form:calendar')</script>
      </span>

      JS object Calendar has the following code in constructor:
      Calendar = function(mainElementId) {
       this.calendarElement = document.getElementById(mainElementId);
       this.calendarElement.component = this;
       ...
       Event.observe('mousedown', this.handler.bind(this));
      }

      On this step we already have constructed instance of calendar initialized and attached to DOM elements. Now we can call some function using JS API:
      #{rich:component('calendar')}.today()

      and this is rendered as the following JS:
      document.getElementById('form:calendar').component.today()
      Let's call this approach "current" or Curr.

      Alternative approach to this one is recreation of server-side components tree on the client using some special API. Let's call this approach "alternative" or Alt.

      Pros and cons of both approaches:
      Alt:
      - Pros:
      -- Reflects components hierarchy
      -- Doesn't pollute DOM nodes
      -- Doesn't create circular references (this MSDN article describes what's wrong with it)

      - Cons:
      -- Very hard to maintain - children components are created before parent

      Cur:
      - Pros:
      -- Very leightweight and simple to support

      - Cons:
      -- Have no pros of Alt

      In order to get rid of memory leaks, Cur includes a special code that traverses DOM tree for DOM elements being replaced and calls special "destroy" method on components. The same traversal is done for page "unload" event. Another important thing that this method does is clearing JS libraries data store for updated elements.

      Data store was developed in order to break circular references and keep JS data separated from DOM objects. Concept behind data store is very simple: it's just a global array or hash where data for DOM objects is stored using API provided by the JS library. When data for some DOM object is stored, it gets unique identifier - key of the cell in data store. Both Prototype.js and jQuery use this:
      //jQuery
       var id = elem[ expando ];
       // Compute a unique ID for the element
       if ( !id )
       id = elem[ expando ] = ++uuid;
       // Only generate the data cache if we're
       // trying to access or manipulate it
       if ( name && !jQuery.cache[ id ] )
       jQuery.cache[ id ] = {};
       // Prevent overriding the named cache with undefined values
       if ( data !== undefined )
       jQuery.cache[ id ][ name ] = data;

      //Prototype.js
       var cache = Event.cache;
      
       function getEventID(element) {
       if (element._prototypeEventID) return element._prototypeEventID[0];
       arguments.callee.id = arguments.callee.id || 1;
       return element._prototypeEventID = [++arguments.callee.id];
       }
       function getCacheForID(id) {
       return cache[id] = cache[id] || { };
       }
       function getWrappersForEventName(id, eventName) {
       var c = getCacheForID(id);
       return c[eventName] = c[eventName] || [];
       }


      What's happening when we update DOM elements that use data store? That's right, data for removed elements is waiting for page reload, when global JS objects are recreated. For one-page RIA applications this means "never". Another facet of the problem is removal of elements that attached event handlers to global objects by AJAX (e.g. D'n'D updated by poll component). So, in RichFaces we've implemented special "memory.js" scripts, that are attaching as AJAX events and window "unload" listener and do the actual clean (Fisheye, lines 65-89) for bundled JS libraries (BTW, that's one of the reasons to use bundled JS libraries instead of external).

      Is there something in Mojarra AJAX? Out of the box, no:
      var doDelete = function doDelete(element) {
       var id = element.getAttribute('id');
       var target = $(id);
       var parent = target.parentNode;
       parent.removeChild(target);
       };
      (here "element" method parameter is response XML node).

      Possible implementation variants:
      1. Use existing in Mojarra "complete" event and clear elements exploiting knowledge about JSF AJAX response structure. Disadvantages - low performance; not compatible with the different kind of extensions provided by 3rd party libraries; risk of response structure modification in the future JSF versions.
      2. Track all elements in the global JS array, find the ones that were removed from DOM and do the cleanup after AJAX update completion. Disadvantages - lot of.
      3. Request addition of such event to the specification/implementation and utilize it.

        • 1. Re: RichFaces 4.0 Core: AJAX - JavaScript components
          jbalunas

          I'm trying to understand and breakdown the event issue. Nick let me know if I'm mistaken. I'm going to leave out the current and alternate implementation discussion for now and focus on the JSF/JS event that we discussed.

          The problem is that for certain components generated in JavaScript we need to get an event when the current page unloads or closes. RichFaces currently does this via our JS libraries.

          We need this notification to perform cleanup and management of the JS created components.

          The options we have to support this as you outlined them:


          Possible implementation variants:
          1. Use existing in Mojarra "complete" event and clear elements exploiting knowledge about JSF AJAX response structure. Disadvantages - low performance; not compatible with the different kind of extensions provided by 3rd party libraries; risk of response structure modification in the future JSF versions.
          2. Track all elements in the global JS array, find the ones that were removed from DOM and do the cleanup after AJAX update completion. Disadvantages - lot of.
          3. Request addition of such event to the specification/implementation and utilize it.


          Option #3 is something that the JSF EG should consider and look into as a possible addition for JSF 2.1. This is something that may be common in component sets and seems like a reasonable addition to the library.

          Did I summarize this correctly?

          • 2. Re: RichFaces 4.0 Core: AJAX - JavaScript components
            nbelaevski

             

            The problem is that for certain components generated in JavaScript we need to get an event when the current page unloads or closes. RichFaces currently does this via our JS libraries.

            As we always can register handlers for "onunload" event; I'd suggest to clarify that notifications for updates by AJAX requests are the key feature of this point.

            • 3. Re: RichFaces 4.0 Core: AJAX - JavaScript components
              nbelaevski

              I've created simplest test page that involves h:dataTable with 100 rows and jQuery rows coloring on hover. Here is the related code:

              <?xml version="1.0" encoding="UTF-8" ?>
              <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
              <html xmlns="http://www.w3.org/1999/xhtml"
               xmlns:f="http://java.sun.com/jsf/core"
               xmlns:h="http://java.sun.com/jsf/html"
               xmlns:ui="http://java.sun.com/jsf/facelets"
               xmlns:a4j="http://richfaces.org/a4j">
              <f:view>
               <h:head>
               <h:outputScript name="jsf.js" library="javax.faces" />
               <h:outputScript name="jquery.js" />
               </h:head>
               <h:body>
               <h:form id="form">
               <h:panelGroup layout="block" id="panel">
               <h:dataTable id="table" value="#{randomListBean.dataList}" var="item">
               <h:column>
               #{item}
               </h:column>
               </h:dataTable>
               <script type="text/javascript">
               jQuery("tr").mouseover(function() {
               jQuery(this).css({'color': 'red'})
               }).mouseout(function() {
               jQuery(this).css({'color': 'black'})
               });
               </script>
               </h:panelGroup>
              
               <script type="text/javascript">
               function update() {
               jsf.ajax.request(window, null, {render: 'form:panel'});
               }
              
               setInterval(update, 1000);
               </script>
              
               </h:form>
               </h:body>
              </f:view>
              </html>

               

              package demo;
              import java.util.ArrayList;
              import java.util.List;
              import java.util.UUID;
              
              import javax.faces.bean.ManagedBean;
              import javax.faces.bean.RequestScoped;
              
              
              @ManagedBean(name = "randomListBean")
              @RequestScoped
              public class RandomListBean {
              
               private static final int LIST_SIZE = 100;
              
               private List<String> dataList = null;
              
               private void createDataList() {
               dataList = new ArrayList<String>(LIST_SIZE);
               for (int i = 0; i < LIST_SIZE; i++) {
               dataList.add(UUID.randomUUID().toString());
               }
               }
              
               public List<String> getDataList() {
               if (dataList == null) {
               createDataList();
               }
               return dataList;
               }
              
              }
              

               

              Results: memory consumption in bytes for FF 3.5 clean (add-ons disabled), Vista:
              JSF 2:
              jsf2-poll-memory.PNG
              RF 3.3.2.SR1:
              rf332sr1-poll-memory.PNG

               

              Message was edited by: Nick Belaevski

              • 4. Re: RichFaces 4.0 Core: AJAX - JavaScript components
                jbalunas

                Nick - this is great analysis. We need to get this in front of the EG.

                I'll point some of the guys to this. It obviously won't make 2.0, but something should be considered for the MR.

                • 5. Re: RichFaces 4.0 Core: AJAX - JavaScript components
                  nbelaevski
                  • 6. Re: RichFaces 4.0 Core: AJAX - JavaScript components
                    jbalunas

                    Excellent - now that you are on the EG you should be able to push for items like this directly.  I would bring this up on the mailing list and see if we can get something like this in the maintence release.

                     

                    Also I'm leary of calling this an enhancement because it will prevent a memory leak.