Angular input validation with optional max min always invalid - angularjs

I am building a directive that adds some logic to the input['date'] element. This is what I have right now:
app.directive('calendarInput', function() {
'use strict';
return {
template : '<input type="date"' +
'ng-model="calendarInput"' +
'min="{{min}}"' +
'max="{{max}}" />'
replace: true,
scope: {
startdate : '#',
enddate : '#',
calendarInput: '='
},
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch('startdate', function (val) {
if (val) {
var date = new Date(val);
scope.min = date.toIsoString().split('T')[0];
}
});
scope.$watch('enddate', function (val) {
if (val) {
var date = new Date(val);
scope.max = date.toIsoString().split('T')[0];
}
});
}
};
});
The idea is to reuse this directive. Sometimes there will be a startdate only, sometimes a enddate only. Like so:
<div calendar-input="item.birthday" enddate="'1990-01-01'"></div>
Unfortunately this always results in an invalid form with the class ng-invalid-min. It's because I don't supply the startdate parameter.
How can I make min or max values optional?
Edit:
I am using Angular 1.3.9

Figured out a way to compile max and min attributes on demand.
app.directive('calendarInput', function($compile) {
'use strict';
return {
template : '<input type="date" class="datepicker" ng-model="omdCalendarInput" />',
replace: true,
scope: {
startdate : '#',
enddate : '#',
calendarInput: '='
},
restrict: 'CA',
link: function (scope, element, attrs) {
var dateInput = element.find('input') // <----
scope.$watch('startdate', function (val) {
if (val) {
var date = new Date(val);
scope.min = date.toIsoString().split('T')[0];
if (!dateInput.attr('min')) { // <----
dateInput.attr('min', '{{min}}'); // <----
$compile(dateInput)(scope); // <----
}
}
});
scope.$watch('enddate', function (val) {
if (val) {
var date = new Date(val);
scope.max = date.toIsoString().split('T')[0];
if (!dateInput.attr('max')) { // <----
dateInput.attr('max', '{{max}}'); // <----
$compile(dateInput)(scope); // <----
}
}
});
}
};
});
Relevant Link
EDIT: Improved the code above. Also I'm marking this as the correct answer, since nobody else has a solution.

This is great, just what I needed. Thank you.
Nit-pick - might want to change:
scope.min = date.toIsoString().split('T')[0];
to
scope.min = $format('date')(date, 'yyyy-MM-dd');
to make it more angularish.

Related

Remove $watch from this directive

I've wrote this directive to handle my date inputs: https://plnkr.co/edit/7hpc8u5pVc7iaNSwn7Zw?p=preview
app.directive('myDate', ['$filter', myDate]);
function myDate($filter) {
var directive = {
restrict: 'E',
template: template,
require: 'ngModel',
scope: {},
link: link
}
return directive;
function template(element, attrs) {
var template = '<input ng-model="date" ng-keyup="keyup($event.keyCode)" ui-mask="99/99/9999" type="text" ';
if (attrs.class) {
template += 'class="' + attrs.class + '"';
element.removeClass(attrs.class);
}
template += '/>';
return template;
}
function link(scope, element, attrs, ctrl) {
scope.keyup = function(key) {
if (key === 68) { // D key
scope.date = $filter('date')(new Date(), 'ddMMyyyy');
}
};
ctrl.$formatters.push(function(data) { // model to view
data = $filter('date')(data, 'ddMMyyyy');
return data;
});
ctrl.$parsers.push(function(data) { // view to model
var year = data.toString().substr(-4);
var month = data.toString().substr(2, 2);
var day = data.toString().substr(0, 2);
var sep = '-';
data = (year && month && day) ? Date.parse(year + sep + month + sep + day) : '';
return data;
});
scope.$watch('date', function() {
ctrl.$setViewValue(scope.date);
});
ctrl.$render = function() {
scope.date = ctrl.$viewValue;
};
}
}
Unfortunately I used $watch to keep my model updated... I would like to know if there is a better way to trigger $setViewValue(scope.date) without $watch; just to optimize it a bit.
Thank you!
Add this to your template:
ng-change="updateParent()"
And this to you link:
scope.updateParent = function(){
ctrl.$setViewValue(scope.date);
}

How to force date input bind to model as JavaScript date Date()

I'm trying to get input from text boxes to bind to scope variables as actual JavaScript Date objects, not strings. The inputs are generated dynamically so I'm unable to cast/convert before the values are sent to the server.
So far, I have created a directive that uses moment.js to parse the value on the scope, and convert it to a Date() object. The problem seems to be that angular converts the value back to a string immediately after. I guess it rechecks the html input and overwrites the Date() object set in the directive.
Here is a working Plunkr demonstrating the issue
(function () {
'use strict';
angular.module('app', ['ng'])
.controller('myController', ['$scope', function() {
$scope.testObj = null;
}])
.directive('dateBinding', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: false,
link: function (scope, element, attrs, ngModel) {
var parseFormat = attrs.dateBinding;
scope.$watch(
function() {
console.log('watching model', ngModel.$modelValue);
return ngModel.$modelValue;
},
function (val) {
console.log('recieved model', val);
if (val && typeof val == 'string') {
console.log('attempting parse date', val);
if(moment(val, parseFormat).isValid())
{
console.log('string is valid date');
ngModel.$modelValue = moment(val, parseFormat).toDate();
console.log('completed value assignment', ngModel.$modelValue);
console.log('model is of type ' + typeof ngModel.$modelValue);
console.log('model is date', (ngModel.$modelValue instanceof Date));
}
else
{
console.log('string is not a valid date');
}
}
}
);
}
};
})
} ());
You can see the behaviour by opening the console in a browser while running the plunkr. The line 'completed value assignment' shows that at least momentarily, ngModel.$modelValue (from $scope.testObj) is a Date() object.
The final line in the output below shows the watch firing again, and the model value is once again a string as it appears in the html input.
How can I have the value persist as a Date object (once a valid date can be parsed).
You have to use the $parsers and $formatters pipelines, described in the docs of ngModelController. The code would be:
.directive('dateBinding', function () {
return {
restrict: 'A',
require: 'ngModel',
scope: false,
link: function (scope, element, attrs, ngModel) {
var parseFormat = attrs.dateBinding;
function parse(value) {
var m = moment(value, parseFormat);
if( m && m.isValid() ) {
ngModel.$setValidity('dateBinding', true);
return m.toDate();
}
else {
ngModel.$setValidity('dateBinding', false);
return; // undefined
}
}
function format(value) {
if( value && value instanceof Date ) {
return moment(d).format(parseFormat);
}
else {
return '';
}
}
ngModel.$formatters.push(format);
ngModel.$parsers.unshift(parse);
}
};
});
See (and play with) the forked plunk: http://plnkr.co/edit/VboH2iq6HRlaDhX3g1AY?p=preview

angular directive encapsulating a delay for ng-change

I have a search input field with a requery function bound to the ng-change.
<input ng-model="search" ng-change="updateSearch()">
However this fires too quickly on every character. So I end up doing something like this alot:
$scope.updateSearch = function(){
$timeout.cancel(searchDelay);
searchDelay = $timeout(function(){
$scope.requery($scope.search);
},300);
}
So that the request is only made 300ms after the user has stopped typing. Is there any solution to wrap this in a directive?
As of angular 1.3 this is way easier to accomplish, using ngModelOptions:
<input ng-model="search" ng-change="updateSearch()" ng-model-options="{debounce:3000}">
Syntax: {debounce: Miliseconds}
To solve this problem, I created a directive called ngDelay.
ngDelay augments the behavior of ngChange to support the desired delayed behavior, which provides updates whenever the user is inactive, rather than on every keystroke. The trick was to use a child scope, and replace the value of ngChange to a function call that includes the timeout logic and executes the original expression on the parent scope. The second trick was to move any ngModel bindings to the parent scope, if present. These changes are all performed in the compile phase of the ngDelay directive.
Here's a fiddle which contains an example using ngDelay:
http://jsfiddle.net/ZfrTX/7/ (Written and edited by me, with help from mainguy and Ryan Q)
You can find this code on GitHub thanks to brentvatne. Thanks Brent!
For quick reference, here's the JavaScript for the ngDelay directive:
app.directive('ngDelay', ['$timeout', function ($timeout) {
return {
restrict: 'A',
scope: true,
compile: function (element, attributes) {
var expression = attributes['ngChange'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
attributes['ngChange'] = '$$delay.execute()';
return {
post: function (scope, element, attributes) {
scope.$$delay = {
expression: expression,
delay: scope.$eval(attributes['ngDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
$timeout(function () {
if (Date.now() - state.then >= state.delay)
scope.$parent.$eval(expression);
}, state.delay);
}
};
}
}
}
};
}]);
And if there are any TypeScript wonks, here's the TypeScript using the angular definitions from DefinitelyTyped:
components.directive('ngDelay', ['$timeout', ($timeout: ng.ITimeoutService) => {
var directive: ng.IDirective = {
restrict: 'A',
scope: true,
compile: (element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
var expression = attributes['ngChange'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) attributes['ngModel'] = '$parent.' + ngModel;
attributes['ngChange'] = '$$delay.execute()';
return {
post: (scope: IDelayScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) => {
scope.$$delay = {
expression: <string>expression,
delay: <number>scope.$eval(attributes['ngDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
$timeout(function () {
if (Date.now() - state.then >= state.delay)
scope.$parent.$eval(expression);
}, state.delay);
}
};
}
}
}
};
return directive;
}]);
interface IDelayScope extends ng.IScope {
$$delay: IDelayState;
}
interface IDelayState {
delay: number;
expression: string;
execute(): void;
then?: number;
action?: ng.IPromise<any>;
}
This works perfectly for me: JSFiddle
var app = angular.module('app', []);
app.directive('delaySearch', function ($timeout) {
return {
restrict: 'EA',
template: ' <input ng-model="search" ng-change="modelChanged()">',
link: function ($scope, element, attrs) {
$scope.modelChanged = function () {
$timeout(function () {
if ($scope.lastSearch != $scope.search) {
if ($scope.delayedMethod) {
$scope.lastSearch = $scope.search;
$scope.delayedMethod({ search: $scope.search });
}
}
}, 300);
}
},
scope: {
delayedMethod:'&'
}
}
});
Using the directive
In your controller:
app.controller('ctrl', function ($scope,$timeout) {
$scope.requery = function (search) {
console.log(search);
}
});
In your view:
<div ng-app="app">
<div ng-controller="ctrl">
<delay-search delayed-method="requery(search)"></delay-search>
</div>
</div>
I know i'm late to the game but,hopefully this will help anyone still using 1.2.
Pre ng-model-options i found this worked for me, as ngchange will not fire when the value is invalid.
this is a slight variation on #doug's answer as it uses ngKeypress which doesn't care what state the model is in.
function delayChangeDirective($timeout) {
var directive = {
restrict: 'A',
priority: 10,
controller: delayChangeController,
controllerAs: "$ctrl",
scope: true,
compile: function compileHandler(element, attributes) {
var expression = attributes['ngKeypress'];
if (!expression)
return;
var ngModel = attributes['ngModel'];
if (ngModel) {
attributes['ngModel'] = '$parent.' + ngModel;
}
attributes['ngKeypress'] = '$$delay.execute()';
return {
post: postHandler,
};
function postHandler(scope, element, attributes) {
scope.$$delay = {
expression: expression,
delay: scope.$eval(attributes['ngKeypressDelay']),
execute: function () {
var state = scope.$$delay;
state.then = Date.now();
if (scope.promise) {
$timeout.cancel(scope.promise);
}
scope.promise = $timeout(function() {
delayedActionHandler(scope, state, expression);
scope.promise = null;
}, state.delay);
}
};
}
}
};
function delayedActionHandler(scope, state, expression) {
var now = Date.now();
if (now - state.then >= state.delay) {
scope.$parent.$eval(expression);
}
};
return directive;
};

ui bootstrap datepicker with unix timestamps

I am trying to use ui-bootstrap datepicker binded to unix timestamp datas.
to do so, i would like to use this directive which transforms the unix timestamp to a javascript date from ng-model.
here is the code (plunker)
<div ng-model="date" date-format>
<datepicker min="minDate" show-weeks="false"></datepicker>
</div>
and the directive
.directive('dateFormat', function() {
return {
require: 'ngModel',
link: function(scope, element, attr, ngModelCtrl) {
ngModelCtrl.$formatters.unshift(function(timestamp) {
if (timestamp) return new Date( timestamp * 1000 );
else return "";
});
ngModelCtrl.$parsers.push(function(date) {
if (date instanceof Date) return Math.floor( date.getTime() / 1000 );
else return "";
});
}
};
})
it's possible to select a date. The unix timestamp is correct but then, the calendar switches to 1970…
is there a solution to make this work and use ui bootstrap's datepicker with unix timestamp datas ?
It's kinda late, but I also had this problem - I don't want to keep additional value in my model just for datepicker/timepicker. Followed this great tutorial on ngModelController by Christopher Nadeau
This works in AngularJS 1.2.16:
.directive('timestampFormat', function() {
// Directive that converts timestamp back and forth from
// seconds to Date object
return {
scope: true, // isolated scope
require: 'ngModel',
link: function(scope, element, attr, ngModelCtrl) {
ngModelCtrl.$formatters.push(function (modelValue) {
// returns $viewValue
return {
timestamp: (modelValue ? new Date(modelValue*1000) : "")
};
});
scope.$watch('timestamp', function () {
ngModelCtrl.$setViewValue({ timestamp: scope.timestamp });
});
ngModelCtrl.$parsers.push(function (viewValue) {
// returns $modelValue
if (viewValue.timestamp instanceof Date) return Math.floor( viewValue.timestamp.getTime() / 1000 );
else return "";
});
ngModelCtrl.$render = function () {
// renders timestamp to the view.
if (!ngModelCtrl.$viewValue) ngModelCtrl.$viewValue = { timestamp: ""};
scope.timestamp = ngModelCtrl.$viewValue.timestamp;
};
}
};
});
Now in view you can access it as a timestamp variable.
<div ng-model="yourModelValue" timestamp-format>
{{timestamp}}
<timepicker ng-model="timestamp" ...>
your don't need multiplied and divided
link: function(scope, element, attr, ngModelCtrl) {
ngModelCtrl.$formatters.unshift(function(timestamp) {
if (timestamp) {
var date = new Date( timestamp );
console.log('timestamp to date: ', date);
return date;
} else return "";
});
ngModelCtrl.$parsers.push(function(date) {
if (date instanceof Date) {
timestamp = date.getTime();
console.log('date to timestamp: ', timestamp);
return timestamp;
} else return "";
});
}

angularjs + typeahead input field initial vaule

I have this directive, which is basicly wrapper for typeahead plugin from bootstrap. Everything is working like a charm. But now I have to set initial vaule in typeahead's input field. The value is passed as a string in attrs.init. But I don't know how to insert it into text field.
angular.module('rcApp')
.directive('rcAutocomplete', ['$injector', function ($injector) {
return {
scope: {
model: '#',
search: '#',
key: '#',
show: '#',
init: '#',
ngModel: '='
},
template: '<input type="text">',
replace: true,
restrict: 'E',
require: 'ngModel',
link: function (scope, element, attrs) {
// inject model service
var service = $injector.get(attrs.model);
// define search function
var searchFunction = attrs.search;
// holds picked object id
scope.id = 0;
// holds objects that matched query, mapped by "show" value
scope.map = {};
element.on('focusout ac.itempicked', function () {
scope.$apply(function () {
scope.ngModel = scope.id;
});
});
// launch typehead plugin
element.typeahead(
{
source: function (query, process) {
// clear cache
scope.id = 0;
scope.map = {};
service[searchFunction](query).then(function (result) {
var dataValues = [];
var fieldsToShow = scope.show.split('|');
$.each(result.data, function (index, dataItem) {
// generate key-show string
var valueHash = '';
for (var i = 0; i < fieldsToShow.length; i++) {
valueHash += dataItem[fieldsToShow[i]] + ' ';
}
valueHash = $.trim(valueHash);
// map results
scope.map[valueHash] = dataItem;
// prepare return strings
dataValues.push(valueHash);
});
// return content
process(dataValues);
});
},
updater: function (item) {
if (typeof scope.key === 'undefined') {
scope.id = scope.map[item];
}
else {
scope.id = scope.map[item][scope.key];
}
element.trigger('ac.itempicked');
return item;
}
}
);
}
};
}]);
** UPDATE **
Solution, that worked for me is adding code like this to link function:
// init value
if (typeof attrs.init !== 'undefined') {
window.setTimeout(function () {
element.val(attrs.init);
scope.$apply();
}, 10);
}
But still I don't quite understand why "element.val(attrs.init);" don't updated the view, and calling scope.$apply() did, but throw an "$digest already in progress" error. Wrapping it in window.setTimeout helped, but this also is a hack for me....
It's got to be a way to make it cleaner/simpler...

Resources