11 Replies Latest reply on May 1, 2009 12:18 AM by gonorrhea

    determining dirty fields and prompting user

    gonorrhea

      what is the best practice in Seam/RF/JSF to prompt a user when a user fills in a form and clicks on a link to navigate away from the current JSF page (click s:link for example)?


      goal being to let the user know that they have unsaved work (message on same page or modalPanel).


      is it possible to do this?  how?


      it seems to me you could use javascript onclick event handler for any and all links to get the current values for each HtmlInputText field, for example, and check the original value that was read from entity/db.  If at least one of them doesn't match, then display warning.


      this seems complicated, mechanical and inelegant.


      what's the standard solution to this, if any?

        • 1. Re: determining dirty fields and prompting user

          There isn't a standard solution, but there are two approaches I have used in the past:



          • If you happen to be using Hibernate with an SMPC, you can check if the Session.isDirty() using a4j and display a modal if dirty.  (Note that in certain cases you will get false positives here, but handles the 90% case)

          • A more complicated solution is to use straight JavaScript (as you were stating) plus a server-side boolean to set a dirty flag onchange.  You can make this a bit more elegant using JQuery.



          Hope that helps.

          • 2. Re: determining dirty fields and prompting user

            Jacob Orshalick wrote on Apr 30, 2009 14:16:


            There isn't a standard solution, but there are two approaches I have used in the past:


            • If you happen to be using Hibernate with an SMPC, you can check if the Session.isDirty() using a4j and display a modal if dirty.  (Note that in certain cases you will get false positives here, but handles the 90% case)





            This only works if you have already sent the changes to the server (and the changes were valid, because if not Hibernate validation will get in the way preventing you changes from reaching your POJOs in the server)
            And , of course, fails to detect if have partially captured new POJOs, we can thank all this on the entityManager not having transactional validation (so you can not call entityManager.persist for a new POJO with partial information), to fix this, please vote for EJB-419



            • A more complicated solution is to use straight JavaScript (as you were stating) plus a server-side boolean to set a dirty flag onchange.  You can make this a bit more elegant using JQuery.





            Yes, switching to a client side programming model seems like the sensible choice. The server side component seems to be inherently limited to properly deal with this kind of problem.



            Hope that helps.


            • 3. Re: determining dirty fields and prompting user

              This only works if you have already sent the changes to the server (and the changes were valid, because if not Hibernate validation will get in the way preventing you changes from reaching your POJOs in the server).

              Yup, absolutely.  The solution I used performed an ajax post prior to navigation and if changes were submitted, asked the user if they wanted to save or discard. If any validation issues occurred, they were immediately presented back to the user (this was the desired behavior).



              And , of course, fails to detect if have partially captured new POJOs, we can thank all this on the entityManager not having transactional validation (so you can not call entityManager.persist for a new POJO with partial information), to fix this, please vote for EJB-419

              Yeah, I definitely see your point on new POJOs but in my case new POJOs were attached to existing entities through an association so it flipped the dirty bit (e.g. no need to call persist).

              • 4. Re: determining dirty fields and prompting user
                gonorrhea

                I stick with the EntityManager interface most of the time.  It does not have isDirty().  So we can use (Session)entityManager.getDelegate() to get the Session interface instance.


                https://www.hibernate.org/hib_docs/v3/api/org/hibernate/Session.html



                isDirty()       Does this session contain any changes which must be synchronized with the database? In other words, would any DML operations be executed if we flushed this session?

                So the question is: does the SMPC become dirty if the user does not submit the form (i.e. JSF update model values phase is not executed) and navigates away from the page using essentially an HTTP GET request via s:link, etc.?


                And how exactly does the Session interface become dirty?  Is it when the setter method on a managed entity class already in persistent state is executed?  What if there is no entity class involved in the form (i.e. the getter/setter values are associated with an SFSB directly?)


                There seems to be no coverage of isDirty() in the Hibernate 3.3.1 ref doc or JPA/Hibernate book...


                It seems to me that it would be nice if there was a Seam or RF tag you could embed inside a particular HtmlForm that would handle this for you (for any and all input-able fields in the form), and you would simply identify the appropriate message for a facesMessages instance or identify a particular rich:modalPanel to be displayed with appropriate message...


                Is this concept possible to implement as a a4j/RF/Seam UI component?

                • 5. Re: determining dirty fields and prompting user
                  gonorrhea


                  This only works if you have already sent the changes to the server (and the changes were valid, because if not Hibernate validation will get in the way preventing you changes from reaching your POJOs in the server).


                  Jacob Orshalick wrote on Apr 30, 2009 17:29:

                  Yup, absolutely.  The solution I used performed an ajax post prior to navigation and if changes were submitted, asked the user if they wanted to save or discard. If any validation issues occurred, they were immediately presented back to the user (this was the desired behavior).



                  It seems counter-intuitive to me to literally execute ajax post (or any post for that matter) immediately after a user clicks on s:link (HTTP GET request instantiated).


                  Then they may see a validation error when they didn't actually submit the form intentionally.  Which becomes a training issue b/c most websites (e.g. eBay, amazon) don't implement this behavior...


                  It seems to me it's better not to provide this unsaved/warning functionality.  If it happens, too bad.  Go back and re-input the data in the same or new LRC or session...


                  And if for some reason you're not using JPA or Hibernate for ORM persistence (say it's JDBC with or without stored procs) then it becomes even more difficult to manage, no?


                  • 6. Re: determining dirty fields and prompting user

                    It seems counter-intuitive to me to literally execute ajax post (or any post for that matter) immediately after a user clicks on s:link (HTTP GET request instantiated).

                    As I said, only posts so an s:link was not used in the situation, only commandLinks to navigate.  I'm always a fan of the simplest solution to fit the specific problem so perhaps solution 2 is a better fit for your requirements case ;)



                    Is this concept possible to implement as a a4j/RF/Seam UI component?

                    You could create a component that decorates embedded input elements with an onchange event.  Of course the links would also need to be decorated to use the dirty flag that gets set onchange.


                    Before you head down that path, I would check out jQuery, allows you to easily add this type of js behavior across html elements.

                    • 7. Re: determining dirty fields and prompting user
                      gonorrhea

                      Here is a javascript example solution from http://www.4guysfromrolla.com/demos/OnBeforeUnloadDemo3.htm.  Note that the element id's are hard-coded in the beginning of the script.  The demo works.  This may be a good solution esp. for projects not using Hibernate as persistence provider.


                      <script language="JavaScript">
                        var ids = new Array('name', 'gender', 'sendEmail', 'radVanilla', 'radChocolate', 'radStrawberry');
                        var values = new Array('', '', '', '', '', '');
                        
                        function populateArrays()
                        {
                          // assign the default values to the items in the values array
                          for (var i = 0; i < ids.length; i++)
                          {
                            var elem = document.getElementById(ids[i]);
                            if (elem)
                              if (elem.type == 'checkbox' || elem.type == 'radio')
                                values[i] = elem.checked;
                              else
                                values[i] = elem.value;
                          }      
                        }
                      
                      
                      
                        var needToConfirm = true;
                        
                        window.onbeforeunload = confirmExit;
                        function confirmExit()
                        {
                          if (needToConfirm)
                          {
                            // check to see if any changes to the data entry fields have been made
                            for (var i = 0; i < values.length; i++)
                            {
                              var elem = document.getElementById(ids[i]);
                              if (elem)
                                if ((elem.type == 'checkbox' || elem.type == 'radio')
                                        && values[i] != elem.checked)
                                  return "You have attempted to leave this page.  If you have made any changes to the fields without clicking the Save button, your changes will be lost.  Are you sure you want to exit this page?";
                                else if (!(elem.type == 'checkbox' || elem.type == 'radio') &&
                                        elem.value != values[i])
                                  return "You have attempted to leave this page.  If you have made any changes to the fields without clicking the Save button, your changes will be lost.  Are you sure you want to exit this page?";
                            }
                      
                            // no changes - return nothing      
                          }
                        }
                      </script>
                      
                      ...
                      
                      <form ...>
                        <b>What is your name:</b> <input type="text" id="name" name="name" /><br />
                        <b>What is your gender?</b>
                        <select id="gender" name="gender">
                          <option value="Male">Male</option>
                          <option value="Female">Female</option>
                        </select><br />
                        
                        <input type="checkbox" name="sendEmail" id="sendEmail" /> <b>Send me your newsletter!</b>
                        <br />
                        <b>What is your favorite type of ice cream?</b><br />
                        <input type="radio" id="radVanilla" name="iceCream" checked="checked" /> Vanilla<br />
                        <input type="radio" id="radChocolate" name="iceCream" /> Chocolate<br />
                        <input type="radio" id="radStrawberry" name="iceCream" /> Strawberry<br />
                        <p>
                        <input type="Submit" value="Save" onclick="needToConfirm = false;" />
                      </form>
                      
                      <script language="JavaScript">
                        populateArrays();
                      </script>
                      

                      • 8. Re: determining dirty fields and prompting user
                        gonorrhea

                        The other problem is how to handle multiple forms per JSF page (perhaps a loop for all the forms?).  There is a lot of custom Javascript code that would be required...

                        • 9. Re: determining dirty fields and prompting user

                          Just use:



                          1. This jQuery solution or..

                          2. This other jQuery solution.

                          3. Or, create you own jQuery based solution.



                          As I was saying, the server side component model is too limited. JSF is no competition for jQuery. ;-)

                          • 10. Re: determining dirty fields and prompting user
                            gonorrhea

                            thx for the links, those were helpful/interesting and looked like a much simpler/cleaner potential solution than mine.  looks like Bauer made heavy use of jQuery in the wiki project from the Seam distro.  I'll check that out to see how to integrate jQuery with RF/Seam...

                            • 11. Re: determining dirty fields and prompting user
                              gonorrhea

                              Ok this is what I have as a POC that is functional.  Only problem is I need to figure out how to substitute the Javascript alert popup box with a rich:modalPanel.  I searched Practical RF PDF and there is no reference to 'jquery' in that book.  At this point I would consider this a sufficient solution.  The highlighting of fields upon entry (onkeydown/onkeyup event handler) is interesting as well.  I'm sure most clients will not request this behavior as a functional requirement but it's good to know a solution is available and how to implement it.


                              TestJQuery.xhtml:


                              <!DOCTYPE composition 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:h="http://java.sun.com/jsf/html">
                                   
                                        <head>                                                                  
                                              <script type="text/javascript" src="jquery-1.3.2.js"></script>
                                              <script type="text/javascript" src="jquery.formobserver.js"></script>              
                                              <script type="text/javascript">
                                                $(document).ready(function(){
                                                     $('#MyForm').FormObserve();
                                                   });                                       
                                              </script>   
                                              
                                              <style>
                                                       .changed {
                                                         background-color: red;
                                                       }
                                              </style>                                                                            
                                         </head>        
                                         
                                         <body>                
                                              <a href="http://www.google.com">Google Link</a>
                                              <h:form id="MyForm">
                                                   <h:panelGrid columns="2">
                                                        Input1: <h:inputText value="here's default value1" />
                                                        Input2: <h:inputText value="here's default value2" />
                                                        Input3: <h:inputText value="here's default value3" />
                                                   </h:panelGrid>
                                                  <h:commandButton value="submit"/>                  
                                             </h:form>
                                         </body>
                               </html>



                              jquery.formobserver.js:


                              /**
                               *  jquery.popupt
                               *  (c) 2008 Semooh (http://semooh.jp/)
                               *
                               *  Dual licensed under the MIT (MIT-LICENSE.txt)
                               *  and GPL (GPL-LICENSE.txt) licenses.
                               *
                               **/
                              (function($){
                                $.fn.extend({
                                  FormObserve: function(opt){
                                    opt = $.extend({
                                      changeClass: "changed",
                                      filterExp: "",
                                      msg: "Unsaved changes will be lost.\nReally continue?"
                                    }, opt || {});
                              
                                    var fs = $(this);
                                    fs.each(function(){
                                      this.reset();
                                      var f = $(this);
                                      var is = f.find(':input');
                                      f.FormObserve_save();
                                      setInterval(function(){
                                        is.each(function(){
                                          var node = $(this);
                                          var def = $.data(node.get(0), 'FormObserve_Def');
                                          if(node.FormObserve_ifVal() == def){
                                            if(opt.changeClass) node.removeClass(opt.changeClass);
                                          }else{
                                            if(opt.changeClass) node.addClass(opt.changeClass);
                                          }
                                        });
                                      }, 1);
                                    });
                              
                                    function beforeunload(e){
                                      var changed = false;
                                      fs.each(function(){
                                        if($(this).find(':input').FormObserve_isChanged()){
                                          changed = true;
                                          return false;
                                        }
                                      });
                                      if(changed){
                                        e = e || window.event;
                                        e.returnValue = opt.msg;
                                      }
                                    }
                                    if(window.attachEvent){
                                        window.attachEvent('onbeforeunload', beforeunload);
                                    }else if(window.addEventListener){
                                        window.addEventListener('beforeunload', beforeunload, true);
                                    }
                                  },
                                  FormObserve_save: function(){
                                    var node = $(this);
                                    if(node.is('form')){
                                      node.find(':input').each(function(){
                                        $(this).FormObserve_save();
                                      });
                                    } else if(node.is(':input')){
                                      $.data(node.get(0), 'FormObserve_Def', node.FormObserve_ifVal());
                                    }
                                  },
                                  FormObserve_isChanged: function(){
                                    var changed = false;
                                    this.each(function() {
                                      var node = $(this);
                                      if(node.eq(':input')){
                                        var def = $.data(node.get(0), 'FormObserve_Def');
                                        if(typeof def != 'undefined' && def != node.FormObserve_ifVal()){
                                          changed = true;
                                          return false;
                                        }
                                      }
                                    });
                                    return changed;
                                  },
                                  FormObserve_ifVal: function(){
                                    var node = $(this.get(0));
                                    if(node.is(':radio,:checkbox')){
                                      var r = node.attr('checked');
                                    }else if(node.is(':input')){
                                      var r = node.val();
                                    }
                                    return r;
                                  }
                                });
                              })(jQuery);