Skip to content

Commit 901e41e

Browse files
committed
feat(input) add support to for week
Add support to date filter for outputing week of year Partially closes angular#757
1 parent d026c99 commit 901e41e

File tree

4 files changed

+322
-15
lines changed

4 files changed

+322
-15
lines changed

src/ng/directive/input.js

+192-12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}$/;
1313
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
1414
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
1515
var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)$/;
16+
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
1617

1718
var inputType = {
1819

@@ -187,18 +188,18 @@ var inputType = {
187188
}
188189
</script>
189190
<form name="myForm" ng-controller="Ctrl as dateCtrl">
190-
Pick a date between in 2013:
191-
<input type="date" name="input" ng-model="value"
192-
placeholder="yyyy-MM-dd" min="2013-01-01T00:00:00" max="2013-12-31T00:00:00" required />
193-
<span class="error" ng-show="myForm.input.$error.required">
194-
Required!</span>
195-
<span class="error" ng-show="myForm.input.$error.datetimelocal">
196-
Not a valid date!</span>
197-
<tt>value = {{value}}</tt><br/>
198-
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
199-
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
200-
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
201-
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
191+
Pick a date between in 2013:
192+
<input type="datetime-local" name="input" ng-model="value"
193+
placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required />
194+
<span class="error" ng-show="myForm.input.$error.required">
195+
Required!</span>
196+
<span class="error" ng-show="myForm.input.$error.datetimelocal">
197+
Not a valid date!</span>
198+
<tt>value = {{value}}</tt><br/>
199+
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
200+
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
201+
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
202+
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
202203
</form>
203204
</doc:source>
204205
<doc:scenario>
@@ -222,6 +223,73 @@ var inputType = {
222223
</doc:example>
223224
*/
224225
'datetime-local': dateTimeLocalInputType,
226+
227+
/**
228+
* @ngdoc inputType
229+
* @name ng.directive:input.week
230+
*
231+
* @description
232+
* HTML5 or text input with week-of-the-year validation and transformation to Date. In browsers that do not yet support
233+
* the HTML5 week input, a text element will be used. The text must be entered in a valid ISO-8601
234+
* week format (yyyy-W##), for example: `2013-W02`. Will also accept a valid ISO
235+
* week string or Date object as model input, but will always output a Date object to the model.
236+
*
237+
* @param {string} ngModel Assignable angular expression to data-bind to.
238+
* @param {string=} name Property name of the form under which the control is published.
239+
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
240+
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
241+
* @param {string=} required Sets `required` validation error key if the value is not entered.
242+
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
243+
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
244+
* `required` when you want to data-bind to the `required` attribute.
245+
* @param {string=} ngChange Angular expression to be executed when input changes due to user
246+
* interaction with the input element.
247+
*
248+
* @example
249+
<doc:example>
250+
<doc:source>
251+
<script>
252+
function Ctrl($scope) {
253+
$scope.value = '2013-W01';
254+
}
255+
</script>
256+
<form name="myForm" ng-controller="Ctrl as dateCtrl">
257+
Pick a date between in 2013:
258+
<input type="week" name="input" ng-model="value"
259+
placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required />
260+
<span class="error" ng-show="myForm.input.$error.required">
261+
Required!</span>
262+
<span class="error" ng-show="myForm.input.$error.week">
263+
Not a valid date!</span>
264+
<tt>value = {{value}}</tt><br/>
265+
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
266+
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
267+
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
268+
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
269+
</form>
270+
</doc:source>
271+
<doc:scenario>
272+
it('should initialize to model', function() {
273+
expect(binding('value')).toEqual('2013-W01');
274+
expect(binding('myForm.input.$valid')).toEqual('true');
275+
});
276+
277+
it('should be invalid if empty', function() {
278+
input('value').enter('');
279+
expect(binding('value')).toEqual('');
280+
expect(binding('myForm.input.$valid')).toEqual('false');
281+
});
282+
283+
it('should be invalid if over max', function() {
284+
input('value').enter('2015-W01');
285+
expect(binding('value')).toEqual('');
286+
expect(binding('myForm.input.$valid')).toEqual('false');
287+
});
288+
</doc:scenario>
289+
</doc:example>
290+
*/
291+
'week': weekInputType,
292+
225293
/**
226294
* @ngdoc inputType
227295
* @name ng.directive:input.number
@@ -669,6 +737,118 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
669737
}
670738
}
671739

740+
741+
function weekInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
742+
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
743+
744+
ctrl.$parsers.push(function(value) {
745+
if(ctrl.$isEmpty(value)) {
746+
ctrl.$setValidity('week', true);
747+
return value;
748+
}
749+
750+
if(WEEK_REGEXP.test(value)) {
751+
ctrl.$setValidity('week', true);
752+
return new Date(getTime(value).time);
753+
}
754+
755+
ctrl.$setValidity('week', false);
756+
return undefined;
757+
});
758+
759+
ctrl.$formatters.push(function(value) {
760+
if(isDate(value)) {
761+
return $filter('date')(value, 'yyyy-Www');
762+
}
763+
return ctrl.$isEmpty(value) ? '' : ''+value;
764+
});
765+
766+
if(attr.min) {
767+
var minValidator = function(value) {
768+
var valTime = getTime(value),
769+
minTime = getTime(attr.min);
770+
771+
var valid = ctrl.$isEmpty(value) ||
772+
valTime.time >= minTime.time;
773+
774+
ctrl.$setValidity('min', valid);
775+
return valid ? value : undefined;
776+
};
777+
778+
ctrl.$parsers.push(minValidator);
779+
ctrl.$formatters.push(minValidator);
780+
}
781+
782+
if(attr.max) {
783+
var maxValidator = function(value) {
784+
var valTime = getTime(value),
785+
maxTime = getTime(attr.max);
786+
787+
var valid = ctrl.$isEmpty(value) ||
788+
valTime.time <= maxTime.time;
789+
790+
ctrl.$setValidity('max', valid);
791+
return valid ? value : undefined;
792+
};
793+
794+
ctrl.$parsers.push(maxValidator);
795+
ctrl.$formatters.push(maxValidator);
796+
}
797+
798+
function getFirstThursday(year) {
799+
var d = 1, date;
800+
while(true) {
801+
date = new Date(year, 0, d++);
802+
if(date.getDay() === 4) {
803+
return date;
804+
}
805+
}
806+
}
807+
808+
function getThisThursday(date) {
809+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + (4 - date.getDay()));
810+
}
811+
812+
var MILLISECONDS_PER_WEEK = 6.048e8;
813+
814+
function getWeek(date) {
815+
var firstThurs = getFirstThursday(date.getFullYear()),
816+
thisThurs = getThisThursday(date),
817+
diff = +thisThurs - +firstThurs;
818+
819+
return 1 + Math.round(diff / MILLISECONDS_PER_WEEK);
820+
}
821+
822+
function getTime(isoWeek) {
823+
if(isDate(isoWeek)) {
824+
return {
825+
year: isoWeek.getFullYear(),
826+
week: getWeek(isoWeek),
827+
time: +isoWeek
828+
};
829+
}
830+
831+
if(isString(isoWeek)) {
832+
WEEK_REGEXP.lastIndex = 0;
833+
var parts = WEEK_REGEXP.exec(isoWeek);
834+
if(parts) {
835+
var year = +parts[1],
836+
week = +parts[2],
837+
firstThurs = getFirstThursday(year),
838+
addDays = (week - 1) * 7;
839+
840+
return {
841+
time: +new Date(year, 0, firstThurs.getDate() + addDays),
842+
week: week,
843+
year: year
844+
};
845+
}
846+
}
847+
848+
return NaN;
849+
}
850+
}
851+
672852
function dateTimeLocalInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
673853
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
674854

src/ng/filter/filters.js

+35-2
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,35 @@ function timeZoneGetter(date) {
228228
return paddedZone;
229229
}
230230

231+
function getFirstThursday(year) {
232+
var d = 1,
233+
date = new Date(year, 0, d);
234+
while(date.getDay() !== 4) {
235+
d++;
236+
date = new Date(year, 0, d);
237+
}
238+
return date;
239+
}
240+
241+
function getThursdayThisWeek(date) {
242+
var day = date.getDay(),
243+
d = date.getDate();
244+
245+
return new Date(date.getFullYear(), date.getMonth(), d + (4 - day));
246+
}
247+
248+
function weekGetter(size) {
249+
return function(date) {
250+
var firstThurs = getFirstThursday(date.getFullYear()),
251+
thisThurs = getThursdayThisWeek(date);
252+
253+
var diff = +thisThurs - +firstThurs,
254+
result = 1 + Math.round(diff / 6.048e8);
255+
256+
return padNumber(result, size);
257+
};
258+
}
259+
231260
function ampmGetter(date, formats) {
232261
return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
233262
}
@@ -256,10 +285,12 @@ var DATE_FORMATS = {
256285
EEEE: dateStrGetter('Day'),
257286
EEE: dateStrGetter('Day', true),
258287
a: ampmGetter,
259-
Z: timeZoneGetter
288+
Z: timeZoneGetter,
289+
ww: weekGetter(2),
290+
w: weekGetter(1)
260291
};
261292

262-
var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,
293+
var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEw']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|w+))(.*)/,
263294
NUMBER_STRING = /^\-?\d+$/;
264295

265296
/**
@@ -294,6 +325,8 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+
294325
* * `'.sss' or ',sss'`: Millisecond in second, padded (000-999)
295326
* * `'a'`: am/pm marker
296327
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200)
328+
* * `'ww'`: ISO-8601 week of year (00-53)
329+
* * `'w'`: ISO-8601 week of year (0-53)
297330
*
298331
* `format` string can also be one of the following predefined
299332
* {@link guide/i18n localizable formats}:

test/ng/directive/inputSpec.js

+88
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,94 @@ describe('input', function() {
717717

718718
// INPUT TYPES
719719

720+
describe('week', function (){
721+
it('should set the view if the model is valid ISO8601 week', function() {
722+
compileInput('<input type="week" ng-model="secondWeek"/>');
723+
724+
scope.$apply(function(){
725+
scope.secondWeek = '2013-W02';
726+
});
727+
728+
expect(inputElm.val()).toBe('2013-W02');
729+
});
730+
731+
it('should set the view if the model is a valid Date object', function (){
732+
compileInput('<input type="week" ng-model="secondWeek"/>');
733+
734+
scope.$apply(function(){
735+
scope.secondWeek = new Date(2013, 0, 11);
736+
});
737+
738+
expect(inputElm.val()).toBe('2013-W02');
739+
});
740+
741+
it('should set the model undefined if the input is an invalid week string', function () {
742+
compileInput('<input type="week" ng-model="value"/>');
743+
744+
scope.$apply(function(){
745+
scope.value = new Date(2013, 0, 11);
746+
});
747+
748+
749+
expect(inputElm.val()).toBe('2013-W02');
750+
751+
try {
752+
//set to text for browsers with datetime-local validation.
753+
inputElm[0].setAttribute('type', 'text');
754+
} catch(e) {
755+
//for IE8
756+
}
757+
758+
changeInputValueTo('stuff');
759+
expect(inputElm.val()).toBe('stuff');
760+
expect(scope.value).toBeUndefined();
761+
expect(inputElm).toBeInvalid();
762+
});
763+
764+
765+
766+
describe('min', function (){
767+
beforeEach(function (){
768+
compileInput('<input type="week" ng-model="value" name="alias" min="2013-W01" />');
769+
scope.$digest();
770+
});
771+
772+
it('should invalidate', function (){
773+
changeInputValueTo('2012-W12');
774+
expect(inputElm).toBeInvalid();
775+
expect(scope.value).toBeFalsy();
776+
expect(scope.form.alias.$error.min).toBeTruthy();
777+
});
778+
779+
it('should validate', function (){
780+
changeInputValueTo('2013-W03');
781+
expect(inputElm).toBeValid();
782+
expect(+scope.value).toBe(+new Date(2013, 0, 17));
783+
expect(scope.form.alias.$error.min).toBeFalsy();
784+
});
785+
});
786+
787+
describe('max', function(){
788+
beforeEach(function (){
789+
compileInput('<input type="week" ng-model="value" name="alias" max="2013-W01" />');
790+
scope.$digest();
791+
});
792+
793+
it('should validate', function (){
794+
changeInputValueTo('2012-W01');
795+
expect(inputElm).toBeValid();
796+
expect(+scope.value).toBe(+new Date(2012, 0, 5));
797+
expect(scope.form.alias.$error.max).toBeFalsy();
798+
});
799+
800+
it('should invalidate', function (){
801+
changeInputValueTo('2013-W03');
802+
expect(inputElm).toBeInvalid();
803+
expect(scope.value).toBeUndefined();
804+
expect(scope.form.alias.$error.max).toBeTruthy();
805+
});
806+
});
807+
});
720808

721809
describe('datetime-local', function () {
722810
it('should set the view if the model is valid ISO8601 local datetime', function() {

0 commit comments

Comments
 (0)