ui bootstrap datepicker with unix timestamps - angularjs

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 "";
});
}

Related

How to access an attribute in the directive validator in AngularJS correctly

I'm making a validator which validates valid dates like MM/YYYY, but I didn't get how to access an attribute when the model changes:
<input id="my-date"
validate-short-date
data-max-date="{{thisMonth}}"
type="text"
name="myDate"
data-ng-model="myModelDate">
Here is the directive
.directive('validateShortDate', ['moment', function(moment) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, element, attr, ngModel) {
var maxDate = false;
var pattern, regex;
pattern = '^((0[0-9])|(1[0-2])|[1-9])\/(19|20)[0-9]{2}$';
regex = new RegExp(pattern, 'i');
if(!angular.isUndefined(attr.maxDate)) {
// GOT ONLY ONCE
maxDate = attr.maxDate;
}
ngModel.$validators.maxDate = function(modelValue) {
// maxDate var is undefined after the first time
if (maxDate && regex.test(modelValue)) {
var modelDate = moment(modelValue, 'MM/YYYY').format('YYYYMM');
return modelDate <= maxDate;
}
return true;
};
ngModel.$validators.valid = function(modelValue) {
return modelValue === '' || modelValue === null || angular.isUndefined(modelValue) || regex.test(modelValue);
};
}
};
}])
The validator ngModel.$validators.valid works perfect, but inside ngModel.$validators.maxDate i cannot get the attr.maxDate but the first time directive fires.
So how can I access to a custom attribute value every time I check the modelValue?
I'm not an expert with AngularJS and probably I'm missing something important.
The attrs argument in the link function provides you with a $observe method which you can use to attach a listener function for dynamic changes in an attribute value.
It is very simple to use inside of your link function:
attr.$observe('maxDate', function() {
scope.maxDate = attr.maxDate;
ngModel.$validate();
});
Here is a working Plunker
You can do like this for track the change in ng-model:-
HTML
<input id="my-date"
validate-short-date
data-max-date="{{thisMonth}}"
type="text"
name="myDate"
data-ng-model="myModelDate">
Angularjs code:-
app.directive('validateShortDate', ['moment', function(moment) {
return {
restrict: 'A',
require: 'ngModel',
link: function($scope, element, attr, ngModel) {
var maxDate = false;
var pattern, regex;
pattern = '^((0[0-9])|(1[0-2])|[1-9])\/(19|20)[0-9]{2}$';
regex = new RegExp(pattern, 'i');
if(!angular.isUndefined(attr.maxDate)) {
// GOT ONLY ONCE
maxDate = attr.maxDate;
}
ngModel.$validators.maxDate = function(modelValue) {
// maxDate var is undefined after the first time
if (maxDate && regex.test(modelValue)) {
var modelDate = moment(modelValue, 'MM/YYYY').format('YYYYMM');
return modelDate <= maxDate;
}
return true;
};
ngModel.$validators.valid = function(modelValue) {
return modelValue === '' || modelValue === null || angular.isUndefined(modelValue) || regex.test(modelValue);
};
}
$scope.$watch('ngModel',function(){
console.log(attr.dataMaxDate);
});
};
}])

Angular input validation with optional max min always invalid

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.

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 - ui-bootstrap - datepicker - Is there a way of detecting when the month has changed?

I'd like to set the date to the 1st of the currently selected month when it's changed.
is there a way without monitoring click event on the month's buttons?
tia
Sam
Datepicker's scope has a property 'activeDateId' that you can watch. It changes when you switch months.
I think the best way is to create a 'decorator' on the datepicker directive so you can add functionality to it. You can access its scope if you override the directive's 'compile' function.
You do this in a module.config function:
$provide.decorator('datepickerDirective', function($delegate) {
var directive = $delegate[0];
/* Override compile */
var link = directive.link;
directive.compile = function() {
return function(scope, element, attrs, ctrl) {
link.apply(this, arguments);
scope.$watch('activeDateId', function() {
console.log('switched');
});
}
};
return $delegate;
});
EDIT
I ran into an issue with code similar to this where if you switch the month and the 2 months have their 1st date on the same day, the $watch won't fire. You can get around this by watching the value of the property 'activeDate' in Datepicker's controller:
scope.$watch(function() {
return ctrl.activeDate.getTime();
}, function() {
console.log('switched');
});
It's unfortunate that the datepicker directive doesn't have a binding to track the month/year change. Here is what I came up with as a work around.
In the html...
<datepicker month-changed="changeMonth($month, $year)" />
In the controller...
$scope.changeMonth = function(month, year) {
console.log(month, year);
// set your date here if you need to
// in my case, i needed to request some results via ajax
};
The decorator
app.config(function($provide) {
$provide.decorator('datepickerDirective', function($delegate) {
var directive = $delegate[0],
link = directive.link;
angular.extend(directive.scope, { 'monthChanged': '&' });
directive.compile = function() {
return function(scope, element, attrs, ctrl) {
link.apply(this, arguments);
scope.$watch(function() {
return ctrl[0].activeDate.getTime();
}, function(newVal, oldVal) {
if (scope.datepickerMode == 'day') {
oldVal = moment(oldVal).format('MM-YYYY');
newVal = moment(newVal).format('MM-YYYY');
if (oldVal !== newVal) {
var arr = newVal.split('-');
scope.monthChanged({ '$month': arr[0], '$year': arr[1] });
}
}
});
};
};
return $delegate;
});
});
Another decorator
Here is another decorator that also works but probably a bit more resource intensive. All I do here is override the isActive() method already used in the directive so my method is called instead. I'm only posting this as an example of the possible ways to work around some of the restrictions in 3rd party directives. I don't necessarily recommend this solution.
app.config(function($provide) {
$provide.decorator('datepickerDirective', function($delegate) {
var directive = $delegate[0],
link = directive.link;
angular.extend(directive.scope, { 'monthChanged': '&' });
directive.compile = function() {
return function(scope, element, attrs, ctrl) {
link.apply(this, arguments);
var activeMonth, activeYear, activeFn = scope.isActive;
scope.isActive = function(dateObj) {
if (scope.datepickerMode == 'day' && !dateObj.secondary) {
var oldMonth = activeMonth, oldYear = activeYear,
newMonth = dateObj.date.getMonth() + 1;
newYear = dateObj.date.getFullYear();
if (oldMonth !== newMonth || oldYear !== newYear) {
activeMonth = newMonth;
activeYear = newYear;
scope.monthChanged({ '$month': newMonth, '$year': newYear });
}
}
activeFn(dateObj);
};
};
};
return $delegate;
});
});
Put a $watch on the model the date picker is bound to:
<datepicker ng-model="data.date" min-date="minDate" show-weeks="true" class="well well-sm"></datepicker>
$scope.$watch('data.date', function(){
//date has changed
});
I had the same issue when I was trying to set the ng-model to first day of January when they select a year. #Rob and #spongessuck answers really helped me. This is how I solved it. Whenever you select a year or month, the variable activeDate gets set. I am using $setViewValue to set the model to activeDate by watching variable datepickerMode which changes whenever you select a year, month or date.
`
app.config(function ($provide) {
$provide.decorator('datepickerDirective', function ($delegate) {
var directive = $delegate[0],
link = directive.link;
directive.compile = function () {
return function (scope, element, attrs, ctrl) {
link.apply(this, arguments);
scope.$watch('datepickerMode', function (newValue, oldValue) {
if (newValue!=oldValue && newValue == 'month') {
ctrl[1].$setViewValue(ctrl[0].activeDate);
}
});
};
};
return $delegate;
});
});
`
Hope this helps someone.

Datepicker directive causes infdig (infinite digest loop) error

This AngularJS 1.2.21 directive is used as an universal solution to be used in browsers supporting type="date" and not supporting it.
directive('datePicker', function(){
var el = document.createElement('input');
el.setAttribute('type','date');
var typeDateSupport = (el.type === 'date');
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
if (typeDateSupport) {
elm.attr('type', 'date');
elm.attr('placeholder', null);
/* Type date takes care of the right format. The display value is localized, the real value is always YYYY-MM-DD,
so that we do not need to add any parsers and formatters */
} else {
elm.attr('type', 'text');
elm.attr('readonly', 'readonly');
elm.datepicker({
dateFormat: 'yy-mm-dd', // TODO: internationalize this
onSelect: function(date) {
scope.$apply(function() {
ctrl.$setViewValue(date);
});
}
});
if (attrs.hasOwnProperty('max')) {
elm.datepicker('option', 'maxDate', $.datepicker.parseDate('yy-mm-dd', attrs.max));
}
ctrl.$formatters.unshift(function(value) {
if (angular.isUndefined(value)) {
return undefined;
}
return moment.utc(value).tz(Config.timeZone).format('YYYY-MM-DD');
});
ctrl.$parsers.unshift(function(value) {
if (angular.isUndefined(value)) {
return undefined;
}
var tmp = moment.tz(value, Config.timeZone);
ctrl.$setValidity('date', tmp.isValid());
return tmp.isValid()? tmp : undefined;
return value;
});
}
}
};
})
The code causes this error: Error: [$rootScope:infdig] in IE 11 (not supporting type="date"). I have actually no idea, why....
Could somebody help?
Remove the call to scope.$apply. It's usually related to such errors.

Resources