Masked Input 1.3.1

A long overdue release of my Masked Input Plugin is available. This is a bugfix release that adresses some of the bigger issues out there:

  • jQuery 1.9 compatibility.
  • Fixed browser lockup when window loses focus.
  • Android issues.
  • No longer preventing event bubbling.
  • Making sure we call completed handler when pasting a value.
  • Fixed bug trying to set caret on hidden elements.
  • Fixed cursor positioning bug related to bounds check.

You can see everything in detail over at the 1.3.1 milestone on github.

I had planned to roll in more bugfixes, but jQuery 1.9 releasing forced my hand a bit. :) If you see any problems, please report them as a github issue. I also managed to get this published in the newly revamped jQuery plugins site. Thank you to everyone who reported issues and to those who submitted patches.

 

Testing jQuery plugins with Node.js and Jasmine

I'm easing into Node.js and I wanted to share some of the stuff I'm learning. One of my focal points lately has been javascript testing. I finally got a test suite in place for my masked input plugin and I was happy with the results. I used Jasmine and the additional Jasmine-species BDD grammar. For me the next step is to be able to get a quick feedback loop on my tests without going to a browser.

So, let's start with a simple jQuery plugin.

// placeholder.js
(function($) {
	$.fn.placeholder=function(description){
		return this.each(function(){
			var input=$(this);
			input
				.bind("blur.placeholder",function(){
					input.val(input.val()||description);
				})
				.bind("focus.placeholder",function(){
					if(input.val()==description)
						input.val('');
				})
				.trigger("blur.placeholder");
		});
	};
})(jQuery);

This is a plugin that a million people have written before in various forms. This plugin provides a description of the input inside as the value of the input until it has focus. When focused, it disappears. If the user types in their own value and leaves (blur) the input, their text stays. If they type nothing, then we put the description back in.

With that knowledge, I stabbed together a few jasmine specifications in a subdirectory named "specs".

//empty.spec.js
var input=jQuery("<input />").appendTo("body").placeholder("foo");
	
describe('No value',function(){
	describe('when calling placeholder plugin', function () {
		it('should show placeholder value', function () {
			expect(input.val()).toEqual("foo");
		});
	});

	describe('when focusing input without user value', function () {
		runs(function(){input.focus();})
		it('should be empty', function () {			
			expect(input.val()).toEqual("");
		});
	});

	describe('when leaving input without user value', function () {
		runs(function(){input.focus().blur();})
		it('should show placeholder value', function () {			
			expect(input.val()).toEqual("foo");
		});
	});
});

and

//value.spec.js
var input=jQuery("<input />").appendTo("body").val("bacon").placeholder("foo");
	
describe('User supplied value',function(){
	describe('when calling placeholder plugin', function () {
		it("should have the user's value", function () {
			expect(input.val()).toEqual("bacon");
		});
	});

	describe('when focusing input with user value', function () {
		runs(function(){input.focus();})
		it("should contain the user's value", function () {			
			expect(input.val()).toEqual("bacon");
		});
	});

	describe('when leaving input with user value', function () {
		runs(function(){input.focus().blur();})
		it("should contain the user's value", function () {			
			expect(input.val()).toEqual("bacon");
		});
	});
});

Now, these may not be the well written tests, but they get the job done. If you've never seen this before, I highly recommend heading over to the jasmine website and looking through the docs.

Alright, we have a plugin and we have specifications. Let's write a Node.js script to wrap this up. First I'll post the script and then break it down afterwards.

//runspecs.js

//fake browser window
global.window = require("jsdom")
		.jsdom()
		.createWindow();
global.jQuery = require("jquery");

//Test framework
var jasmine=require('jasmine-node');
for(var key in jasmine) {
  global[key] = jasmine[key];
}

//What we're testing
require("./placeholder.js")

jasmine.executeSpecsInFolder(__dirname + '/specs', function(runner, log){  
    process.exit(runner.results().failedCount?1:0);
}, true, true);

The first thing I want to draw attention to are all of the 'require' statements in the script. These are references to node packages I've installed (except the placeholder.js one, that's the thing in the first code snippet). In order to run this script, you'll need to use the node package manager(npm) to install them on your local machine.

The first statement uses jsdom to create a mock browser window. jQuery needs this in order to do it's magic. What you'll notice though is that I'm assigning this to a property off of the global variable. By default variables you declare are only available to the script in which they are declared. By tacking this on to the global object, node will provide this window variable to all other modules or scripts we load up.

Next we load up jQuery and assign it to the global scope too. I'm doing this because my plugins reference jQuery as a global. Here's the template I've been using when writing plugins:

(function($){
   //good stuff goes here
})(jQuery)

After that, we have a few statements to load up the jasmine test framework. The for loop pulls out all of the test syntax (describe,it,runs,beforeEach,afterEach,etc) hanging off the jasmine variable and makes it a global as well.

Now things are getting good. We're finally ready to include the thing we're actually testing. This require statement will reference the path to your plugin script relative to the directory of the node script.

The final statement calls a method to invoke the jasmine specs. Notice that we're not supplying any specific file names here. Instead, we just point it to a directory which contains the specs. It does the rest. When it's done, we see if there were any failed tests and then supply an appropriate exit code.

To get this whole deal started, we simply type node runspecs.js at our terminal prompt and see how we did. When I run it, this is what I see:

$ node runspecs.js 
(node) process.compile should not be used. Use require('vm').runInThisContext instead.
Started
......

Spec No value when calling placeholder plugin
Spec No value when focusing input without user value
Spec No value when leaving input without user value
Spec No value
Spec User supplied value when calling placeholder plugin
Spec User supplied value when focusing input with user value
Spec User supplied value when leaving input with user value
Spec User supplied value
Finished in 0.003 seconds
8 tests, 6 assertions, 0 failures

Success!

Masked Input Plugin 1.3

There is now a new version of my Masked Input Plugin for jQuery.  This is primarily a bugfix release. The biggest deal for me with this release has been the addition of a test suite.  In order to do so, I ended up writing a keystroke simulator which I call keymasher. It's not a perfect way to test, but it's better than nothing.

As a product of adding the tests, I found a few inconsistent issues with the delete and backspace handling which are now resolved. I was also able to take advantage of some event normalization that jQuery provides which didn't exist when I first wrote this plugin.  That plus a switch to UglifyJS has resulted in a smaller compressed file size (3.26KB for v1.3 vs 3.46KB for v1.2.2).

Bugfixes:

  • Fixed completed callback bug.
  • Fixed IE bug requiring charAt() instead of array notation to access char within string.
  • Fixed delete key handling with cursor at literal character.
  • Fixed infinite focus loop bug with multiple masked inputs on a page.
  • Fixed raw value returning mask placeholders when input empty.

Enhancements:

  • Now gracefully handle it when mask() gets called multiple times by calling unmask() on behalf of the user.

As always, if you have encounter any issues, please feel free to report them over on my github project. I'll do my best to  fix them in a timely manner. Also, if this plugin has helped you out, feel free to throw a few bucks my way by clicking the donate button at the top of the page.

State of the Masked Input Plugin

This post should be titled, "Dude, Where the Hell have You Been?" I'm sorry I have ignored this project for so long.

New Home
First, let me get started by saying that I moved the source to github a while back.  It now lives here. If you look at the commit history, I moved it there months ago, fixed a few outstanding bugs, added a feature that someone needed for a specific project and then just left it. I'm picking it back up somewhat, but we'll talk about that in a minute.

Email Bankruptcy
There have been a TON of emails from you guys; so many that I haven't been able to keep up.  Most of the emails have been about two bugs: an off by one goof I made for the completed function and forgetting to use charAt() to access a char in a string. These bugs are fixed in the github repo right now (I think). I'm calling my email situation a total loss.  Sorry to those that have emailed me and not gotten a response.

Birth of a New Project
Part of the reason I stalled on this project was a lack of tests.  For a while I wasn't sure how to even test this thing given that it is purely driven from user input.  Up to this point I had just been opening up a test page in every browser I could think of and running through a few things manually.  A couple of weeks ago I sat down one night and spiked out a rough version of a keystroke simulator which I'm now calling KeyMasher. Please be kind, it's still very rough. Once I get it more polished, I'll put up an official project page on my blog here.  I had found a few other projects which do this and jquery.autotype seemed to be the closest fit. Unfortunately I couldn't get it to work with my specific needs, so I've now written my own with a syntax I feel more comfortable with.  I've already worked out a few tests using this against my masked input plugin.

I'm Just One Guy
After I get a half way acceptable set of tests around it, then I can feel a bit more confident about what I change.  I would like to be able to implement some of the features I've seen come across my email. It will take some time to get everything to a place where it should have already been.  Please be patient, I'll get there. :)

Silly User Script

I just wrote the most stupid user script ever. I noticed yesterday that my Masked Input Plugin for jQuery has over a quarter million downloads. I've never stumbled on a site and noticed my plugin being used, so I decided to write this user script to let me know. Maybe one day it will alert me and I'll get a nice surprise.

// ==UserScript==
// @name	My jQuery Plugin Detection
// ==/UserScript==

window.addEventListener("load", function(e) { 	
	if(location.hostname != 'localhost' && this.jQuery){
		if(this.jQuery.fn.mask)
			alert("masked input!");			
		if(this.jQuery.fn.Watermark)
			alert("watermark!");
	}
}, false);

Also, if you are using any of my plugins on a public site, I'd love to know what some of them are!

Next Page »