Search code examples
javascriptjqueryjquery-validatecharactercount

jquery validate with a character counter plugin


I have a page that has fields that are validated using jquery-validate plugin, and wanted to include a twitter like character counter on the fields to see how many chars are left

Here is my demo http://jsfiddle.net/4k1vokgv/1/

$(document).ready(function() {
    $(".counter").characterCounter({
    counterCssClass: 'text-counter',
    limit: 1000,
    counterFormat: 'Characters Remaining: %1',
    });

    var validatorStrat = $("#strategyForm").validate({
      rules:{
        exampleInputEmail1: {
          required: true,
          },
        ZB_note: {
          required: true,
          maxlength: 140,
        },
        ZC_note: {
          required: true,
          maxlength: 140,
        },

      },
      submitHandler: function(form) {}
    });
});

Both character counters work fine until the issue, when jquery-validate fires a validation error (required, maxlength, etc), the character counter then stops working on any element that has an error.

I do not believe this is an issue with the character counter plugin itself. I think the error generation that jquery validate does somehow causes this.

Anyways, included the full snippet below, any help is greatly appreciated

/**
* Character Counter v1.0
* ======================
*
* Character Counter is a simple, Twitter style character counter.
*
* https://github.com/dtisgodsson/jquery-character-counter
*
* @author Darren Taylor
* @author Email: shout@darrenonthe.net
* @author Twitter: darrentaytay
* @author Website: http://darrenonthe.net
*
*/
(function($) {

    $.fn.characterCounter = function(options){

        var defaults = {
            exceeded: false,
            limit: 150,
            counterWrapper: 'span',
            counterCssClass: 'help-block',
            counterFormat: '%1',
            counterExceededCssClass: 'exceeded',
            onExceed: function(count) {},
            onDeceed: function(count) {},
            customFields: {},
        };

        var options = $.extend(defaults, options);

        return this.each(function() {
            $(this).after(generateCounter());
            bindEvents(this);
            checkCount(this);
        });

        function customFields(params)
        {
            var html='';

            for (var i in params)
            {
                html += ' ' + i + '="' + params[i] + '"';
            }

            return html;
        }

        function generateCounter()
        {
            var classString = options.counterCssClass;

            if(options.customFields.class)
            {
                classString += " " + options.customFields.class;
                delete options.customFields['class'];
            }

            return '<'+ options.counterWrapper +customFields(options.customFields)+' class="' + classString + '"></'+ options.counterWrapper +'>';
        }

        function renderText(count)
        {
            return options.counterFormat.replace(/%1/, count);
        }

        function checkCount(element)
        {
            var characterCount  = $(element).val().length;
            var remaining        = options.limit - characterCount;

            if( remaining < 0 )
            {
                $(element).next("." + options.counterCssClass).addClass(options.counterExceededCssClass);
                options.exceeded = true;
                options.onExceed(characterCount);
            }
            else
            {
                if(options.exceeded) {
                    $(element).next("." + options.counterCssClass).removeClass(options.counterExceededCssClass);
                    options.onDeceed(characterCount);
                    options.exceeded = false;
                }
            }

            $(element).next("." + options.counterCssClass).html(renderText(remaining));
        };    

        function bindEvents(element)
        {
            $(element)
                .bind("keyup", function () {
                    checkCount(element);
                })
                .bind("paste", function () {
                    var self = this;
                    setTimeout(function () { checkCount(self); }, 0);
                });
        }
    };

})(jQuery);

$.validator.setDefaults({
    errorElement: "span",
    errorClass: "help-block",
//	validClass: 'stay',
    highlight: function (element, errorClass, validClass) {
    $(element).addClass(errorClass); //.removeClass(errorClass);
        $(element).closest('.form-group').removeClass('has-success').addClass('has-error');
    },
    unhighlight: function (element, errorClass, validClass) {
    $(element).removeClass(errorClass); //.addClass(validClass);
        $(element).closest('.form-group').removeClass('has-error').addClass('has-success');
    },

    errorPlacement: function(error, element) {
        if(element.parent('.input-group').length) {
           error.insertAfter(element.parent());
         } else if (element.hasClass('select2')) {
           error.insertAfter(element.next('span'));
        } else {
            error.insertAfter(element);
        }

    }
});

$(document).ready(function() {
    $(".counter").characterCounter({
    counterCssClass: 'text-counter',
    limit: 140,
    counterFormat: 'Characters Remaining: %1',
    });
  
    var validatorStrat = $("#strategyForm").validate({
      rules:{
        exampleInputEmail1: {
          required: true,
          },
        ZB_note: {
          required: true,
          maxlength: 1000,
        },
        ZC_note: {
          required: true,
          maxlength: 1000,
        },

      },
      submitHandler: function(form) {}
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"/>
<script src="http://cdn.jsdelivr.net/jquery.validation/1.14.0/jquery.validate.js"></script>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css" rel="stylesheet"/>
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css" rel="stylesheet"/>
<script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>

<form role="form" id="strategyForm">
 <div class="form-group">
   <label for="exampleInputEmail1" class="control-label">Email address</label>
     <input type="email" class="form-control" name="exampleInputEmail1" placeholder="Enter email" />
 </div>
 <div class="form-group">
            <label class="control-label">What amount is to be solicited and when?</label>
            <textarea class="form-control counter" rows="1" id="ZB_note" name="ZB_note" ></textarea>
          </div>


          <div class="form-group">
            <label class="control-label">Who will be involved in the soliciation?</label>
            <textarea class="form-control counter" rows="1" id="ZC_note" name="ZC_note" ></textarea>
          </div>
 <button type="submit" class="btn btn-default">Submit</button>
</form>


Solution

  • Problem is due to where the error element is being inserted compared to the traverse you use to set the counter text.

    In your counter plugin you are looking for next() to set the count display using the following:

    $(element).next("." + options.counterCssClass).html(renderText(remaining));
    

    That is based on structure being:

    <inputElement/>
    <counterDisplay/>
    

    But the validator errorPlacment is doing:

    <inputElement/>
    <validatorError/>
    <counterDisplay/>
    

    So now the plugin next(classSelector) returns no matches

    You could simply use nextAll() instead of next() in plugin...or change the errorPlacement to something like :

    error.parent().append(element); 
    

    Demo using nextAll() in plugin