Thursday, October 17, 2013

AngularJS Placeholder for IE8/9 + Validation

After 5 hours looking how to fix IE8 bug with placeholder and required attribute,
I manage to find the problem and the solution.

The key problem is that our $formatter is executed before the model binder finish loading.
which caused the placeholder to be applied as a value.
You can guess, this lead to by passing your required validation.

So the key solution is to let the model binder finish their work first and then we do watermark of our control.

Key of the day : $timeout

Feel free to see the whole angular placeholder from my gist
https://gist.github.com/kkurni/7018564
MyApp.directive('placeholder', function($timeout) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
//check whether it support placeholder and cache it
scope.supportsPlaceholders = scope.supportsPlaceholders || function() {
return "placeholder" in document.createElement("input");
};
if (scope.supportsPlaceholders()) {
return;
}
//this function is used to move the caret to the left
var caretTo = function(el, index) {
if (el.createTextRange) {
var range = el.createTextRange();
range.move("character", index);
range.select();
} else if (el.selectionStart != null) {
el.focus();
el.setSelectionRange(index, index);
}
};
var setPlaceholder = function () {
element.val(attr.placeholder)
};
var clearPlaceholder = function () {
element.val('');
};
element.bind('focus', function() {
//on focus, set the caret into 0 position
if (element.val() == attr.placeholder) {
caretTo(element[0], 0);
}
});
element.bind('click', function() {
//on click, set the caret into 0 position
if (element.val() == attr.placeholder) {
caretTo(element[0], 0);
}
});
element.bind('keydown', function(event) {
//disable left or right key code when there is placeholder
if (element.val() == attr.placeholder) {
if (event.keyCode == 37 || event.keyCode == 39) {
event.preventDefault();
}
}
});
element.bind('keypress', function() {
//on key press, clear the placeholder
if (element.val() == attr.placeholder) {
clearPlaceholder();
}
});
element.bind('blur', function () {
//on blur, we just set the placeholder if there is no value
if (element.val() == '') setPlaceholder();
});
//formatter is executed when the model is changed
ctrl.$formatters.unshift(function (val) {
if (!val) {
//this timeout is needed so that the validation can finish their binding before we put our placeholder value
$timeout(function(){
setPlaceholder();
return attr.placeholder;
});
}
return val;
});
}
};
});
describe('placeholder', function() {
var element, $scope, $timeout, $compile;
beforeEach(module('MyApp'));
beforeEach(angular.mock.inject(function($rootScope, _$compile_, _$timeout_) {
$scope = $rootScope.$new();
$timeout = _$timeout_;
$compile = _$compile_;
$scope.supportsPlaceholders = function() {
return false;
};
element = angular.element('<input type="text" placeholder="enter placeholder" ng-model="model" />');
$compile(element)($scope);
$scope.$digest();
$scope = element.scope();
}));
describe('$formatter', function() {
it('should set element value to placeholder if model is undefined', function() {
$timeout.flush();
expect(element.val()).toEqual('enter placeholder');
});
it('should set element value to model value if model is defined', function() {
$timeout.flush();//flush the first time out
$scope.model = 'some model';
$compile(element)($scope);
$scope.$digest();
expect(element.val()).toEqual('some model');
});
});
describe('on keypress', function() {
it('should clear element value if it is the same as a placeholder', function() {
element.triggerHandler('keypress');
expect(element.val()).toEqual('');
});
});
});