Saturday, April 21, 2012

Javascript : From - To Date Validation

Working with javascript(jQuery) and rails really makes me happy.
This time I wanted to add validation messages for from and to dates for nested attributes.

As per my previous post we have already seen the link_to_remove_association for adding confirmation message. But this time the situation is bit different. I want to add validation messages next to the fields of the nested attributes. So those validations run for each field.

Adding function and comparing 2 dates might not be much difficult but becomes tricky, because I am using simple_form_for which wraps in divs. So need to work around the after build html source for exact tree structure.

Well, the view code was something like below(haml file) and check the valid_from and valid_to date fields on which the javascript function needs to be added. So added a function call in onfocus and passed as a parameter input_from to the simple_form_for input.
.nested-fields
  %table.table-condensed
    %tbody
      %tr
        %td
          = f.input :name, :label => false, :input_html => {:class => 'name'}
        %td
          = f.input :description, :label => false, :input_html => {:class => 'description'}
        %td
          = f.input :valid_from, :as => :string, :label => false, :input_html => {:class => 'valid_from', :onfocus => "createDatePickerWithValidation(this, 'from', '#{t :selected_date_prior_to_today}', '#{t :selected_both_dates_prior_to_today}');"}
        %td
          = f.input :valid_to, :as => :string, :label => false, :input_html => { :class => 'valid_to', :onfocus => "createDatePickerWithValidation(this, 'to', '#{t :selected_date_prior_to_today}', '#{t :selected_both_dates_prior_to_today}');"}
        %td
          = link_to_remove_association "X", f, { :onClick => "confirm_for_remove_record(this, '#{t :confirm_for_remove_record}', 10)" }
        %td{:valign => "top"}
          %span{:style=>"display: none; color: #B94A48"}

In javascript file added below function (I usually create common.js).
/* createDatePickerWithValidation(field, type, field_message, combined_message)
*    Datepicker with warning messages for valid from and through dates 
*    warning_div :   is the div in which the warning message is displayed. 
*                    This needs to be searched dynamically for each record based 
*                    on the position of the clicked field.
*    other_field :   is the field seareched based on the clicked field. 
*                    If the clicked field is valid_from then other_field will be valid_to and vice versa.
*                    This is based on each records fields so that it will not conflict to other records.
*    field_message : is the combination of values of both the fields of each record.
*                    First value will always be the valid_from and other will be valid_to.
*/
function createDatePickerWithValidation(f, typ, message, both_fields_message) {
  $(f).datepicker({ dateFormat: 'yy-mm-dd',
    onSelect: function(date) {
      var todays_date = new Date();
      var input_date = new Date( date );
      var warning_div, other_field, field_message;

      if(typ=='from'){
        warning_div = $(f).closest("td").next().next().next().children();
        other_field = $(f).closest("td").next().children().children().children();
        field_message = date + " and " + other_field.val()
      }else{
        warning_div = $(f).closest("td").next().next().children();
        other_field = $(f).closest("td").prev().children().children().children();
        field_message = other_field.val() + " and " + date
      }
      /* Default warning message set */
      var msg = ""
      var other_field_date = new Date( other_field.val() );

      /* If both input dates are prior to today's date */
      if( todays_date > input_date && todays_date > other_field_date ){
        msg = field_message + both_fields_message
      }

      /* If clicked input is prior to todays date but other field is blank or after todays date */
      if( todays_date > input_date && ( todays_date <= other_field_date || other_field.val()=="" ) ){
        msg = date + message
      }

      /* If other field is prior to todays date but clicked input is blank or after todays date */
      if( todays_date > other_field_date && ( todays_date <= input_date || date=="" ) ){
        msg = other_field.val() + message
      }

      /* If message blank then remove the text and hide the div else add text and show div */
      if(msg==""){
        warning_div.html("").hide();
      }else{        
        warning_div.html(msg).show();
      }
    }
  });
  $(f).datepicker("show");
}
and in en.yml
  selected_date_prior_to_today: " date is prior to todays Date"
  selected_both_dates_prior_to_today: " dates are prior to todays Date"

Tuesday, April 17, 2012

Cocoon : link_to_remove_association : confirm box

Really happy when used the nathanvda/cocoon for adding nested model records.
This makes life easy. But only thing I missed was confirmation message.

We can actually add html_options to the method, but because of "remove_fields" class, this actually removes/ hides the fields on-click of the link.
So adding confirmation from html option was no use.

So for overriding, I added the following method in application_helper.rb. But you can also add it in new view helper class in lib and include it in application helper(just to keep it separate).
# Adding confirm message for association remove links.
# Original : f.hidden_field(:_destroy) + link_to(name, '#', :class => "remove_fields #{is_dynamic ? 'dynamic' : 'existing'}")
# Cocoon : ViewHelpers : link_to_remove_association
# link_to_remove_association(name, f, args={})
# name : to be displayed for link.
# f : form object
# args : defaults set to empty hash.
#        this holds the html options like onClick.
#        e.g : link_to_remove_association "name", f, { :onClick => "onclick();", onBlur => "onblur();" }
# extra_class : If no html options are passed from the view it will append 
#               the class to the class attribute of the link.
#               This will call the original functionality
# option_class : If class key added in view for styling purpose, this will add it to the args
# is_dynamic : is as per the original functionality. Lets us know if the records is new or existing.
def link_to_remove_association(name, f, args={})
  extra_class = (args.has_key?(:onclick) || args.has_key?(:onClick)) ? nil : "remove_fields"
  is_dynamic = f.object.new_record? ? 'dynamic' : 'existing'
  option_class = args.has_key?(:class) ? args[:class] : nil

  args.merge!({:class => "#{extra_class} #{is_dynamic} #{option_class}"})
  f.hidden_field(:_destroy) + link_to(name, '#this', args)
end
This worked out smoothly.
Now if I dont pass any option it will actually append the remove_fields class to the class option of the link.
This will run the original functionality of cocoon.
This is working for me as required functionality.

Now in javascript I have added function for confirming the removal.
So added
/* confirm_for_remove_record(field, confirm_message)
*  record : The whole row of the current clicked field.
*  record_input : method delete input.
*  class_name : added div class name for appending dialog.
*/
function confirm_for_remove_record(f, message, max_records){
  var record = $(f).closest(".nested-fields");  
  var record_input = $(f).prev();
  var class_name = "." + record_input.attr("id")

  /* Appending extra div for dialog box holder */
  $(f).parent().append( "<div class=" + record_input.attr('id') + ">" + message + "</div>" )
  
  /* Adding dialog box functionality to newly added div 
  *  If field is dynamic it will delete the entire div 
  *  else it will just hide and remove the class name ".nested_fields" from the div class attribute.
  *  If less than max_length then enable add_a_record link by removing the class "disable" from the anchor tag.
  */
  $( class_name ).dialog({
    draggable : false,
    resizable : false,
    modal : true,
    buttons : {
      "Remove" : function() {
        $( class_name ).dialog( "close" );
        $(record_input).val(1);
        
        if($(f).hasClass("dynamic")){
          $(record).remove();  
        }else{
          $(record).hide();
          $(record).removeClass("nested-fields");
        }

        if($(".nested-fields").length < 15 ){
          $("#add_a_record").removeClass("disabled");
        }        
        
      },
      "Don't Remove" : function() {
        $( class_name ).dialog( "close" );
      }
    }
  }).dialog( "open" );
}
And in view
link_to_remove_association "X", f, { :onclick => "confirm_for_remove_record(this, '#{t :confirm_for_remove_record}', 10)" }