skip to Main Content

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: [email protected]
* @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>

3

Answers


  1. 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

    Login or Signup to reply.
  2. Finding the reason (Problem)

    I believe I’ve found the problem.
    When the a field doesn’t pass the validation rules it shows an error.
    The error is being append to the DOM as:

    This field is required.

    When I removed it using the console the counter worked.
    That made my wonder – maybe the checkCount function still works but the “output” (the span counter) doesn’t.

    So on line 72, I added:

    console.log(characterCount);
    

    Duplicated that scenario again – and it indeed printed the count.
    So the problem is that from some reason – when the ” error” appears it conflicts with the ” text counter”. Please notice, that after start writing again – it seems that the ” error” is gone – but the truth is that it still in the DOM, it’s just hidden using CSS.

    <span id="ZB_note-error" class="help-block" style="display: none;"></span>
    

    Then, I added the following code on Line 92:

         console.debug( options.counterCssClass );
         console.debug( $(element).next("." + options.counterCssClass).html());
    

    Guess what was the output.
    For the first debug line: text-counter (that’s good)
    For the second debug line: undefined (not good)

    How to solve it?

    Solution 1: You’re using the next() function wrongly.

    Description: Get the immediately following sibling of each element in
    the set of matched elements. If a selector is provided, it retrieves
    the next sibling only if it matches that selector.

    When the ” error” element is being added, the text-counter field is no longer apply to the next() rule. Consider change it with something like: .parent().find('.text-counter') in case each field+text-counter have a common parent.

    Solution 2: When a user starts typing, remove the ” error” element from the DOM.

    Login or Signup to reply.
  3. I have made a code pen with exactly this functionality here.

    Codepen

    I will also add and discuss the code here, it really is not that hard.

    $( document ).ready(function() {
      $('#text').on('keypress', function(e) {
      var count = $(this).val().length;
       if(count != 0) {
         count += 1;
       } else {
         count = count;
       }
      $('#characterCount').text(count);   
    })  
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    
    <div class="row">
      <div class="col m-2">
        <textarea id="text" rows="4" cols="50"></textarea>
      </div>
      <div class="col m-2">
        <textarea id="characterCount" rows="4" cols="50"></textarea>
      </div>
    </div>

    The document.ready function insures the function loads when the DOM is ready. You then have a function that fires whenever a key is pressed on the text area. You then have the count variable that is set to the length of the value of the current text ID. You then set the count to plus one, because it is zero index. You then represent the value on the other text area.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search