<!DOCTYPE html>
<html ng-app="angularjs-starter">
<head lang="en">
<meta charset="utf-8">
<title>Custom Plunker</title>
<script src="https://raw.github.com/jquery/globalize/master/lib/globalize.js"></script>
<script src="//code.angularjs.org/1.1.1/angular.js"></script>
<link rel="stylesheet" href="style.css">
<script>
document.write('<base href="' + document.location + '" />');
</script>
<script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
<form name="dayForm">
<ng-form name="startSubForm">
<time-picker label="Start of the day" model="startMinutes"></time-picker>
</ng-form>
<ng-form name="endSubForm">
<time-picker label="End of the day" model="endMinutes" start></time-picker>
</ng-form>
<span>TODO: setup ng-show: Start time must be before end time!</span>
</form>
<pre>dayForm = {{ dayForm }}</pre>
<pre>dayForm.$valid = {{ dayForm.$valid }}</pre>
<pre>dayForm.$error = {{ dayForm.$error }}</pre>
<pre>startMinutes={{startMinutes}}</pre>
<pre>endMinutes={{endMinutes}}</pre>
<hr/>
<h2>Problems:</h2>
<ul>
<li>How to display error messages in the timePicker.html template? Should I pass formName as scope property and do something like this[formName].timePicker.$error.timeFormatter?</li>
<li>How to use startEndValidator to compare start/end times?</li>
</ul>
</body>
</html>
/* CSS goes here */
var app = angular.module('angularjs-starter', []);
app.controller('MainCtrl', function($scope) {
// Save hour in minutes on the model to avoid storing strings.
$scope.startMinutes = 10 * 60;
$scope.endMinutes = 18 * 60;
});
app.directive('timePicker', function() {
return {
scope: {
label: '@',
model: '='
},
restrict: 'E',
replace: true,
templateUrl: 'timePicker.html',
transclude: false
};
});
app.directive('timeValidator', function($timeConverter, $log) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
//model -> view
ngModel.$formatters.unshift(function(modelValue) {
$log.log(1, modelValue);
try {
var newViewValue = $timeConverter.timeToString(modelValue);
ngModel.$setValidity('timeValidator', true);
return newViewValue;
} catch(err) {
$log.log(1, err);
ngModel.$setValidity('timeValidator', false);
}
return modelValue;
});
//view -> model
ngModel.$parsers.unshift(function (viewValue) {
$log.log(2, viewValue);
try {
var newModelValue = $timeConverter.timeFromString(viewValue);
ngModel.$setValidity('timeValidator', true);
return newModelValue;
}
catch (err) {
ngModel.$setValidity('timeValidator', false);
// what should I return here?
// I don't want to change model values here as input is invalid
return ngModel.$modelValue;
}
});
// Listen for change events to enable binding
element.bind('blur keyup change', function () {
scope.$apply(read);
});
// Write data to the model
function read() {
var val = element.val();
ngModel.$setViewValue(val);
}
}
};
});
app.directive('startEndValidator', function() {
return {
require: 'ngModel',
link: function(scope, elm, attr, ctrl) {
var startWidget = elm.inheritedData('$formController')[attr.startEndValidator];
ctrl.$parsers.push(function(value) {
var startValue = startWidget.$modelValue;
var endValue = ctrl.$modelValue;
if (startValue < endValue ) {
ctrl.$setValidity('startEndValidator', true);
return value;
}
ctrl.$setValidity('startEndValidator', false);
// should I not return anything here?
});
/*
how to do the validation other way round?
startWidget.$parsers.push(function(value) {
ctrl.$setValidity('startEndValidator', value === ctrl.$viewValue);
return value;
});*/
}
};
});
app.service('$timeConverter', function() {
// format time from minutes to string
function timeToString(model) {
if (model == null) {
return null;
}
if (typeof model !== 'number') {
throw 'model must be a number!';
}
if (model <= 0) {
throw 'model must be greater than 0';
}
var minutes = model % 60;
model -= minutes;
var hours = model / 60;
var days = Math.floor(hours / 24);
hours = hours - days * 24;
var resultParts = [];
if (days > 0) {
resultParts.push(days);
resultParts.push(' ');
}
resultParts.push(Globalize.format(hours, 'd2'));
resultParts.push(':');
resultParts.push(Globalize.format(minutes, 'd2'));
return resultParts.join('');
}
// parse time string to number of minutes
function timeFromString(str) {
if (!str) {
return null;
}
var m = str.match(/^(\d{2})\:(\d{2})$/);
if (m == null) {
throw 'not a valid time format ' + str;
}
var hour = parseInt(m[1]),
minute = parseInt(m[2]);
if (hour < 0) {
throw 'hour (' + hour + ') cannot be < 0!';
}
if (hour > 23) {
throw 'hour (' + hour + ') cannot be > 23!';
}
if (minute < 0) {
throw 'minute (' + minute + ') cannot be < 0!';
}
if (minute > 59) {
throw 'minute (' + minute + ') cannot be > 59!';
}
return hour * 60 + minute;
}
return {
timeToString: timeToString,
timeFromString: timeFromString
};
});
<div>
<span>{{label}}</span>
<!-- plan here is to change type to hidden later on and use some better UI for timepicker -->
<input type="text" name="timePicker" ng-model="model" required time-validator start-end-validator=""/>
<span ng-show="timePicker.$error.required">***</span>
<span ng-show="timePicker.$error.timeFormatter">Incorrect time format!</span>
<pre>timePicker.$valid={{timePicker.$valid}}</pre>
<pre>timePicker.$error.required={{timePicker.$error.required}}</pre>
<pre>timePicker.$error.timeFormatter={{timePicker.$error.timeFormatter}}</pre>
</div>