Better jQuery Placeholder Plugin

A while back I made a post about a simple placeholder script I wrote. The idea was to provide the placeholder functionality that browsers like Chrome and Safari have, to browsers that don’t have it, like Firefox and IE. The script was very simple though and not smart in a few ways, including inability to handle password fields and an incorrect way of checking placeholder support in the browser. Since then I wrote an improved version of the script, in the form of a jQuery plugin. The highlights of this improved script include

  • proper use of jQuery’s plugin framework to create a fully chain-able plugin
  • support for password fields and text areas
  • correct check for placeholder support in browser
  • and just overall a better written piece of code

You can see a demo of the plugin in action and download the commented source file (4.7kb),  or the minified source file (1.7kb), or the clean (not minified but not commented) source file (2.6kb). Below I’ll go over some of the more interesting parts of script.

Update
Since the original post I’ve updated the script to version 1.0.3. I’ve removed an unnecessary var declaration at the top. Modified the tagName check to force lowercase tag names (some browsers return uppercase, others lowercase). Finally, based on Brad’s comments below I added a quick check to make sure that placeholder value is not mistaken for an actual value on page load (was an issue with Firefox 3.6 or lower, not an issue with Firefox 4 because it supports placeholders).

The basics

Marc Görtz gave me the idea for extending the jQuery.support method for the placeholder check, but the testing code itself was taken from modernizr.js

var testinput = document.createElement('input');
$.extend($.support, { placeholder: !!('placeholder' in testinput) });

You might’ve noticed that I’m not using jQuery methods for the testing itself (just the “extending $.support” part). This is because I wanted to keep these as fast as possible. Using $(‘<input/>’) would’ve been shorter but that function would have to run through a big chunk of jQuery doing parsing and all sorts of tests, but ending really at the exact same document.createElement code, so I just cut out the middleman as it were. I do this sort of thing couple of more times in code.

I’ve also added some options to the plugin, which I’ll mention below.

var settings = {
	'class': 'placeholder',
	'allowspaces': false,
	'dopass': true,
	'dotextarea': true,
	'checkafill': false
};

Textareas and inputs are handled the same way in terms of placeholder but textareas are not often used in that way, and so (as you can see above) I added a “dotextarea” flag. It’s a very trivial optimization that probably would never be even needed, but basically if you use the function on a collection of objects that includes a textarea but you don’t want to add the placeholder functionality you can use the flag to have the plugin skip the textarea. Of course it would skip it anyway if you didn’t specify the placeholder attribute for the textarea in the first place, but what if you wanted to give it the attribute but not the functionality… yeah I know, crazy, but I love having options so sue me :)

The catch with password fields

Now, the password fields require special attention. You can’t just stick the placeholder into the field’s value like you would for non-password inputs, all you’re going to see are a bunch of dots or asterisks. Instead, what I do is place a span on top of it with the placeholder string. The trick here is of course to make the text in the span look like it’s the field’s native text. At the same time I also wanted to allow the user to style the fields as they normally do and have the span match that style. Finally there are also some inconsistencies between how inputs and spans present the text, mainly there are padding and alignment issues. So here is the span creation code.

var span = $('',{ 'class': el.attr('class')+' '+settings['class'], text: ph, css: { border: 'none', cursor: 'text', background: 'transparent', position: 'absolute', top: el.position().top, left: el.position().left, lineHeight: el.height()+3+'px', paddingLeft:parseFloat(el.css('paddingLeft'))+2+'px' } }).insertAfter(el); 

As you can see the span inherits the input’s class thus getting all the font styles, but I remove any border or background styles to keep it just as pure text (and leave those up to the input itself). Of course you would also need to provide some styles for the “placeholder” class if you want to the font to be partially transparent, or maybe italic, or whatnot. The positioning, padding and line height are all there to make sure to align the span text as close as possible to the input text. It’s still not perfect and there are minor inconsistencies between the various browsers, but it’s close (1 pixel or 2 off here and there), and you can always adjust them further via CSS.

In any case, once the span is created and attached all you do is show or hide it.

The ghost in the machine

This script of course wasn’t created in a vacuum and I had a chance to see it in production environment, and that’s when I discovered an issue with it, which never would’ve occurred to me otherwise. If your browser auto-fills a remembered password, whether it’s the browser itself or a plugin (like onePass) that’s doing that, it of course fills in the value of the field but the span with the placeholder text remains. The only way I could think of fixing it was to put a repetitive test for the password field and if it gets a value filled in, then remove the placeholder span. Which is what the code below takes care of.

if (settings['checkafill']) {
	(function checkPass(){
		if (!valueEmpty(el.val()) && el.hasClass(settings['class'])) {
			el.focusin();
		}
		setTimeout(checkPass, 250);
	})();
}

Initially this was done via setInterval() but apparently it’s a bad function and thanks to a tip from Paul Irish I wrote the self calling function above to do the exact same thing but better. This functionality is off by default, since I feel like it’s a bit “hackish”, so you’ll need to explicitly set the “checkafill” flag to true to turn it on.

To trim or not to trim

Final little bit is about trimming the field values. Imagine the input of the field is nothing but spaces — 3, 4, whatever. Should that be considered a non-input and thus replaced by a placeholder when the field loses focus, or is that a valid input? I tend to lean towards the former, but I can also see the point in saying “if the user put in 3 spaces and nothing else, there’s a good reason for it, so leave it alone”, and that’s how Chrome and Safari handle it. So, I decided to add a flag for that as well — “allowspaces”. This flag is false by default meaning that as long as the input of a field is nothing but space characters it will be considered empty. If you set it to true then no trimming will be done on the value of the input field and strings of spaces only will be considered a valid input.

Finally

The usage for the plugin is simple — $(‘selector’).addPlaceholder({options}); — where ‘selector’ has to return <input> and/or <textarea> elements.

I tested the plugin in latest Chrome, Safari, as well as Firefox 3.6 and IE8, and it seems to work fine in all those.

And that’s about it. If you have any suggestions for improving the script let me know in the comments below.

(20) Comments

  1. cosmonauti
    March 3, 2011

    Hey, I think I found a bug in your placeholder plugin. If the text is larger than the textarea and a scroll bar appears, you can’t use the scroll bar to scroll. Clicking immediately clears text. Not really the optimal thing to happen, as someone may want to read through text before beginning to edit.

    I’m using this in a context where a database fills the textarea so I don’t have any control over how long that text might be…

  2. Ilia
    March 3, 2011

    That’s an interesting problem cosmonauti.
    Obviously this happens because when you click the scrollbar the focus event fires, which triggers the disappearance of placeholder text.

    As far as I can tell there is no way to detect if I clicked on the scrollbar or the text area itself, at least not without going into complicated calculations that look at where your mouse is during the click and if it falls within the scrollbar area, which really over-complicates the plugin.

    One thing I noticed is that in Firefox and IE, when the scrollbar appears, you can use the mouse wheel to scroll through the text without focusing on the textfield. So that’s a bit of a solution, though not very obvious to your users.
    In Chrome you can’t do that because the placeholder text simply overflows the textarea (i.e. it doesn’t go over to 2nd line) and disappears. Obviously also not optimal.

    One possible solution would be to add title attribute to textarea with the placeholder text in it, that way, when the mouse hovers over it the tooltip will appear with the field’s explanation.

    Another way to handle this issue would be to not remove the placeholder text until the user starts typing. Essentially, replace the focus event with the keydown/keypress event. It’s probably best to do this only in specific cases where you need it (and in yours it’d be limited to textareas) because it might be bit confusing as it’s an unconventional behaviour.
    This variation of the script is something I’m actually going to address in another post as it’s actually somewhat of a neat idea, but I haven’t gotten around to it yet.

  3. Hans Czajkowski Jørgensen
    March 13, 2011

    Hey Ilia,

    I made some changes to the plugin, and would like to share them with you. Would you be comfortable if I put it up on GitHub?

    Specifically, I did made some changes to passPlacehold() [around line 83]:

    function passPlacehold(el, ph) {
    	el.addClass(settings['class']);
    	// setup the initial placeholder, i.e. span
    	var span = $('<span/>',{
    		'class': el.attr('class')+' '+settings['class'],	// 'inherit' class from the input field + the placeholder class
    		text: ph,
    		css: {
    			border:		'none',					// since inherited styles from input, "clean" them for the span: remove border
    			cursor:		'text',					// give text cursor so it looks like part of the input field
    			background:	'transparent',			// clear background to be transparent
    			position:	'absolute',				// position the span appropriately
    			top:		el.position().top,
    			left:		el.position().left,
    			paddingTop: el.css('paddingTop'),
    			paddingRight: el.css('paddingRight'),
    			paddingBottom: el.css('paddingBottom'),
    			paddingLeft:parseFloat(el.css('paddingLeft'))+1+'px'	// ditto as above
    		}
    	})
      span.click(function(e) {
    		if (el.hasClass(settings['class'])) {
    			span.hide();
    			el.removeClass(settings['class']);
    			el.focus();
    		}
      })
    

    The function and the rest of the code are unchanged after this point.

    • Hans Czajkowski Jørgensen
      March 13, 2011

      Oops. ‘Specifically, I did made some changes to passPlacehold() [around line 83]:’ should read ‘Specifically, I made some changes to passPlacehold() [around line 83]:’.

      I’m a stickler, I know :-)

      • Hans Czajkowski Jørgensen
        March 13, 2011

        Changelog:
        2011-03-13 (Hans)
        – Added inheritance of password input padding to placeholder span
        – Removed fake top padding
        – Made placeholder span clickable, so clicking the placeholder
        text will remove the placeholder and focus the password input

    • Ilia
      March 13, 2011

      Hans, feel free to put it up on GitHub. I’m glad you found this useful. I am curious about the changes though.

      As for “padding inheritance”, this should be taken care of by inheriting the class from the password field itself. Was that not happening for you?
      I’m also curious as to why you added click event to span. I mean I understand the reasoning, but in my testing, clicking on span “bubbled through” to the password field, triggering the focusin event (making the span click handler unnecessary). So I take it that wasn’t the case for you?

      What browser are you using to test this? Because if those are browser inconsistencies I should add some sort of handler for them in the code.

      P.S. Post a link to the GitHub repo for this once it’s all setup :)

  4. Hans Czajkowski Jørgensen
    March 14, 2011

    Hey Ilia
    I’ll make my code available to you to reproduce the error.

    As yet, nothing definite as to how or why. There are a lot of variables to consider in my code(jquery version, modernizr, selectivizr etc.).

    I had it in IE6, IE7, IE8 and in FF3.6.15.

    Since I am using yepnope via modernizr to select which ‘regressive enhancement’ to load, I was considering moving the placeholder detection out to yepnope, if it makes sense at all. Checking up on that.

    I’ll message you here once I set it up on github.

    • Ilia
      March 14, 2011

      If you’re using yepnope it does make sense to delegate placeholder detection to it. You can just remove all the $.support.placeholder related code and only call up the plugin if yepnope says placehoder is not supported.

  5. Brad
    April 5, 2011

    Hi, Thanks for the plugin. I really like this plugin and how easy it is to use but I found a bug (or at least it appears to be).

    I loaded up your plugin on my local machine and navigate to my website. Everything is there. I see the placeholder and can click in and out of the box to make it appear and disappear. However, if I do not enter anything into the box with the placeholder and simply refresh the page then the text that was the placeholder now becomes text as if it were entered (aka, it is black and not gray).

    I thought maybe it was my setup so I went to your demo paged (linked in your artilce above) loaded it up and clicked refresh and got the same results (the password fields seem to be immune to this).

    Is there any way to fix this?

    • Brad
      April 5, 2011

      FYI, I’m using Firefox 3.6.16 on linux and Firefox 3.5.5 on Windows

    • Brad
      April 5, 2011

      I did a little poking around with firebug and found that when you first load the page the placeholder query script inserts the “placeholder” class:

      then once the page is refreshed the placehodler class is removed:

      Not sure if that helps you in troubleshooting any but that is what I have found so far.

    • Ilia
      April 5, 2011

      That’s not a bug, that’s a feature… no, I’m serious. :)
      Firefox, on reload, leaves the value of input fields intact unless specifically set to nothing, i.e. <input ... value=""> and even in that case I’m not entirely sure that behaviour is consistent (I think I recently saw it ignore value=”” but it could’ve been something else in the code, didn’t bother to investigate further).
      Since for non-password fields the placeholder text is just the value of the field, when you reload the code looks at the field and sees that it has value and assumes it’s a valid value that you don’t want “cleared” so it leaves it alone.

      I didn’t want to use javascript to force blank value on page load because that seemed like the kind of thing that can cause conflicts with other code some developer might be using. Having said that though it occurs to me that it can be fixed rather easily if not elegantly, simply add a comparison between current value of input field and what the placeholder text is, if they’re the same, chances are (especially on page load), it is indeed the placeholder.

      So basically change if ( valueEmpty(el.val()) ) to if ( valueEmpty(el.val()) || el.val()==ph ) and it should take care of this.

      • Brad
        April 6, 2011

        Awesome, that fix the “issue”. Thanks a million!

  6. Sener
    July 12, 2011

    Hi, first I need to say its very usefull and stable, thank you =)

    When you try to click password fields; span is cover the hit area and it became unclikleable in ie.. anyway I just changed little part of css maybe someone need it. I keeped span area small as possible. so user can click span’s left,up or down space area. I tested all ie versions.

    thanks again..

    css: {
    border: ‘none’,
    cursor: ‘text’,
    background: ‘none’, // <- none is better than transparent..
    position: ’absolute’,
    top: el.position().top+4,
    left: el.position().left+2,
    // lineHeight: el.height()+‘px’, // <- no need this..
    paddingLeft:parseFloat(el.css(‘paddingLeft’))+‘px’
    }

  7. BAL
    October 15, 2011

    Ok–stupid question here. I’m looking at the source for your demo page:

    http://www.iliadraznin.com/examples/better-jquery-placeholder-script/

    In the first form, the first input field has no visible placeholder. I’m looking on Chrome 14. In the source, the placeholder is “username”. Am I missing something basic here?

  8. sethwill
    January 25, 2012

    Yeah, I am on the same page as BAL.

    It appears that this is broken, since it doesn’t show “username” placeholder in the latest versions of chrome or firefox.

  9. elderotaku
    March 9, 2012

    I have a question, wondering if anyone has run into this problem.  I am using this plugin on a textarea which uses unobtrusive validation. The placeholder is considered text and therefor the required validator returns true. Created a custom validator to get around that, but the placeholder plugin causes the onchange event to fire onblur, so then the validation runs when I don’t want it to. Does anyone have a work around for this?
     
    THanks

    • vincentbouyssou
      February 7, 2013

      @elderotaku 
      Hi, I ran into the same problem and added the following code at the end of the placeHolder js — I mean after the
      })(jQuery);
       
      Basically, it controls input values and compare it to the placeholder attribute, and empties the field if needed.
       
      $(function() {     // let’s delete placeholder-like values when submitting
          if(!$.support.placeholder){
              $(“form”).submit(function(){
                  //alert(‘test’);
                  $(“input[type=‘text’]:visible, textarea:visible”, this).each( function(){
                      if($(this).val()==$(this).attr(‘placeholder’)){
                          $(this).val(“”);
                      }
                  });
              });
          }
      });
       
      Please feel free to comment if it’s not the best way to fix that behaviour.

  10. pakage
    September 21, 2012

    Great plugin! Best I’ve found so far. When I tried to implement it, i found that clicking on the span that gets inserted on top of the input field for the password placeholder wasn’t triggering the focus event, i had to click on the input field itself. I’ve added the following function to the passPlacehold function which fixes the problem:
     
    span.mousedown(function(){                           
         el.focus();                       
    });
     
    hope this helps!

  11. cursors
    February 21, 2013

    Added this line (117 in the commented version) to fix the problem with the span blocking clicking into a password field:span.click( function(){ el.focus(); } )

Trackbacks

Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>