Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Commit 90e0e07

Browse files
chalinmhevery
authored andcommitted
feat(ng-model): support input type=date | datetime and all other date/time variants
- Add support for input elements of type `date|datetime|datetime-local|month|time|week`. - Remove non-UTF-8 characters from some other API comments. Closes #747
1 parent 8877cd4 commit 90e0e07

File tree

5 files changed

+688
-3
lines changed

5 files changed

+688
-3
lines changed

lib/directive/module.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,14 @@ class NgDirectiveModule extends Module {
6262
value(NgRepeat, null);
6363
value(NgShow, null);
6464
value(InputTextLike, null);
65+
value(InputDateLike, null);
6566
value(InputNumberLike, null);
6667
value(InputRadio, null);
6768
value(InputCheckbox, null);
6869
value(InputSelect, null);
6970
value(OptionValue, null);
7071
value(ContentEditable, null);
72+
value(NgBindTypeForDateLike, null);
7173
value(NgModel, null);
7274
value(NgValue, null);
7375
value(NgTrueValue, new NgTrueValue());

lib/directive/ng_model.dart

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,192 @@ class InputNumberLike {
439439
}
440440
}
441441

442+
/**
443+
* This directive affects which IDL attribute will be used to read the value of
444+
* date/time related input directives. Recognized values for this directive are:
445+
*
446+
* - [DATE]: [dom.InputElement].valueAsDate will be read.
447+
* - [NUMBER]: [dom.InputElement].valueAsNumber will be read.
448+
* - [STRING]: [dom.InputElement].value will be read.
449+
*
450+
* The default is [DATE]. Use other settings, e.g., when an app needs to support
451+
* browsers that treat date-like inputs as text (in such a case the [STRING]
452+
* kind would be appropriate) or, for browsers that fail to conform to the
453+
* HTML5 standard in their processing of date-like inputs.
454+
*/
455+
@NgDirective(selector: 'input[type=date][ng-model][ng-bind-type]')
456+
@NgDirective(selector: 'input[type=time][ng-model][ng-bind-type]')
457+
@NgDirective(selector: 'input[type=datetime][ng-model][ng-bind-type]')
458+
@NgDirective(selector: 'input[type=datetime-local][ng-model][ng-bind-type]')
459+
@NgDirective(selector: 'input[type=month][ng-model][ng-bind-type]')
460+
@NgDirective(selector: 'input[type=week][ng-model][ng-bind-type]')
461+
class NgBindTypeForDateLike {
462+
static const
463+
DATE = 'date',
464+
NUMBER = 'number',
465+
STRING = 'string',
466+
DEFAULT = DATE;
467+
static const VALID_VALUES = const <String>[DATE, NUMBER, STRING];
468+
469+
final dom.InputElement inputElement;
470+
String _idlAttrKind = DEFAULT;
471+
472+
NgBindTypeForDateLike(dom.Element this.inputElement);
473+
474+
@NgAttr('ng-bind-type')
475+
void set idlAttrKind(final String _kind) {
476+
String kind = _kind == null ? DEFAULT : _kind.toLowerCase();
477+
if (!VALID_VALUES.contains(kind))
478+
throw "Unsupported ng-bind-type attribute value '$_kind'; "
479+
"it should be one of $VALID_VALUES";
480+
_idlAttrKind = kind;
481+
}
482+
483+
String get idlAttrKind => _idlAttrKind;
484+
485+
dynamic get inputTypedValue {
486+
switch (idlAttrKind) {
487+
case DATE: return inputValueAsDate;
488+
case NUMBER: return inputElement.valueAsNumber;
489+
default: return inputElement.value;
490+
}
491+
}
492+
493+
void set inputTypedValue(dynamic inputValue) {
494+
if (inputValue is DateTime) {
495+
inputValueAsDate = inputValue;
496+
} else if (inputValue is num) {
497+
inputElement.valueAsNumber = inputValue;
498+
} else {
499+
inputElement.value = inputValue;
500+
}
501+
}
502+
503+
/// Input's `valueAsDate` normalized to UTC (per HTML5 std).
504+
DateTime get inputValueAsDate {
505+
DateTime dt;
506+
// Wrap in try-catch due to
507+
// https://code.google.com/p/dart/issues/detail?id=17625
508+
try {
509+
dt = inputElement.valueAsDate;
510+
} catch (e) {
511+
dt = null;
512+
}
513+
return (dt != null && !dt.isUtc) ? dt.toUtc() : dt;
514+
}
515+
516+
/// Set input's `valueAsDate`. Argument is normalized to UTC if necessary
517+
/// (per HTML standard).
518+
void set inputValueAsDate(DateTime dt) {
519+
inputElement.valueAsDate = (dt != null && !dt.isUtc) ? dt.toUtc() : dt;
520+
}
521+
}
522+
523+
/**
524+
* **Background: Standards and Browsers**
525+
*
526+
* According to the
527+
* [HTML5 Standard](http://www.w3.org/TR/html5/forms.html#the-input-element),
528+
* the [dom.InputElement.valueAsDate] and [dom.InputElement.valueAsNumber] IDL
529+
* attributes should be available for all date/time related input types,
530+
* except for `datetime-local` which is limited to
531+
* [dom.InputElement.valueNumber]. Of course, all input types support
532+
* [dom.InputElement.value] which yields a [String];
533+
* [dom.InputElement.valueAsDate] yields a [DateTime] and
534+
* [dom.InputElement.valueNumber] yields a [num].
535+
*
536+
* But not all browsers currently support date/time related inputs and of
537+
* those that do, some deviate from the standard. Hence, this directive
538+
* allows developers to control the IDL attribute that will be used
539+
* to read the value of a date/time input. This is achieved via the subordinate
540+
* 'ng-bind-type' directive; see [NgBindTypeForDateLike] for details.
541+
*
542+
* **Usage**:
543+
*
544+
* <input type="date|datetime|datetime-local|month|time|week"
545+
* [ng-bind-type="date"]
546+
* ng-model="myModel">
547+
*
548+
* **Model**:
549+
*
550+
* dynamic myModel; // one of DateTime | num | String
551+
*
552+
* This directive creates a two-way binding between the input and a model
553+
* property. The subordinate 'ng-bind-type' directive determines which input
554+
* IDL attribute is read (see [NgBindTypeForDateLike] for details) and
555+
* hence the type of the read values. The type of the model property value
556+
* determines which IDL attribute is written to: [DateTime] and [num] values
557+
* are assigned to [dom.InputElement.valueAsDate] and
558+
* [dom.InputElement.valueNumber], respectively; [String] and `null` values
559+
* are assigned to [dom.InputElement.value]. Setting the model to `null` will
560+
* clear the input if it is currently valid, otherwise, invalid input is left
561+
* untouched (so that the user has an opportunity to correct it). To clear the
562+
* input unconditionally, set the model property to the empty string ('').
563+
*
564+
* **Notes**:
565+
* - As prescribed by the HTML5 standard, [DateTime] values returned by the
566+
* `valueAsDate` IDL attribute are meant to be in UTC.
567+
* - As of the HTML5 Editor's Draft 29 March 2014, datetime-local is no longer
568+
* part of the standard. Other date related input are also at risk of being
569+
* dropped.
570+
*/
571+
572+
@NgDirective(selector: 'input[type=date][ng-model]',
573+
module: InputDateLike.moduleFactory)
574+
@NgDirective(selector: 'input[type=time][ng-model]',
575+
module: InputDateLike.moduleFactory)
576+
@NgDirective(selector: 'input[type=datetime][ng-model]',
577+
module: InputDateLike.moduleFactory)
578+
@NgDirective(selector: 'input[type=datetime-local][ng-model]',
579+
module: InputDateLike.moduleFactory)
580+
@NgDirective(selector: 'input[type=month][ng-model]',
581+
module: InputDateLike.moduleFactory)
582+
@NgDirective(selector: 'input[type=week][ng-model]',
583+
module: InputDateLike.moduleFactory)
584+
class InputDateLike {
585+
static Module moduleFactory() => new Module()..factory(NgBindTypeForDateLike,
586+
(Injector i) => new NgBindTypeForDateLike(i.get(dom.Element)));
587+
final dom.InputElement inputElement;
588+
final NgModel ngModel;
589+
final Scope scope;
590+
NgBindTypeForDateLike ngBindType;
591+
592+
InputDateLike(dom.Element this.inputElement, this.ngModel, this.scope,
593+
this.ngBindType) {
594+
if (inputElement.type == 'datetime-local') {
595+
ngBindType.idlAttrKind = NgBindTypeForDateLike.NUMBER;
596+
}
597+
ngModel.render = (value) {
598+
scope.rootScope.domWrite(() {
599+
if (!eqOrNaN(value, typedValue)) {
600+
typedValue = value;
601+
}
602+
});
603+
};
604+
inputElement
605+
..onChange.listen(relaxFnArgs(processValue))
606+
..onInput.listen(relaxFnArgs(processValue))
607+
..onBlur.listen((e) {
608+
ngModel.markAsTouched();
609+
});
610+
}
611+
612+
dynamic get typedValue => ngBindType.inputTypedValue;
613+
614+
void set typedValue(dynamic value) {
615+
ngBindType.inputTypedValue = value;
616+
}
617+
618+
void processValue() {
619+
var value = typedValue;
620+
// print("processValue: value=$value, model=${ngModel.viewValue}");
621+
if (!eqOrNaN(value, ngModel.viewValue)) {
622+
scope.eval(() => ngModel.viewValue = value);
623+
}
624+
ngModel.validate();
625+
}
626+
}
627+
442628
class _UidCounter {
443629
static final int CHAR_0 = "0".codeUnitAt(0);
444630
static final int CHAR_9 = "9".codeUnitAt(0);
@@ -548,14 +734,14 @@ class NgFalseValue {
548734
* <input type="radio" ng-model="category">
549735
*
550736
* This creates a two way databinding between the expression specified in
551-
* ng-model and the range input elements in the DOM.  If the ng-model value is
737+
* ng-model and the range input elements in the DOM. If the ng-model value is
552738
* set to a value not corresponding to one of the radio elements, then none of
553739
* the radio elements will be check. Otherwise, only the corresponding input
554740
* element in the group is checked. Likewise, when a radio button element is
555741
* checked, the model is updated with its value. Radio buttons that have a
556742
* `name` attribute are left alone. Those that are missing the attribute will
557743
* have a unique `name` assigned to them. This sequence goes `001`, `001`, ...
558-
* `009`, `00A`, `00Z`, `010`, and so on using more than 3 characters for the
744+
* `009`, `00A`, `00Z`, `010`, and so on using more than 3 characters for the
559745
* name when the counter overflows.
560746
*/
561747
@NgDirective(
@@ -598,7 +784,7 @@ class InputRadio {
598784
* <span contenteditable= ng-model="name">
599785
*
600786
* This creates a two way databinding between the expression specified in
601-
* ng-model and the html element in the DOM.  If the ng-model value is
787+
* ng-model and the html element in the DOM. If the ng-model value is
602788
* `null`, it is treated as equivalent to the empty string for rendering
603789
* purposes.
604790
*/

lib/utils.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,10 @@ final Set<String> RESERVED_WORDS = new Set<String>.from(const [
118118
"while",
119119
"with"
120120
]);
121+
122+
/// Returns true iff o is [double.NAN].
123+
/// In particular, returns false if o is null.
124+
bool isNaN(Object o) => o is num && o.isNaN;
125+
126+
/// Returns true iff o1 == o2 or both are [double.NAN].
127+
bool eqOrNaN(Object o1, Object o2) => o1 == o2 || (isNaN(o1) && isNaN(o2));

test/angular_spec.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ main() {
182182
"angular.directive.NgRepeat",
183183
"angular.directive.NgShow",
184184
"angular.directive.InputTextLike",
185+
"angular.directive.InputDateLike",
185186
"angular.directive.InputNumberLike",
186187
"angular.directive.InputRadio",
187188
"angular.directive.InputCheckbox",

0 commit comments

Comments
 (0)