Index: third_party/pkg/angular/lib/directive/ng_model.dart |
diff --git a/third_party/pkg/angular/lib/directive/ng_model.dart b/third_party/pkg/angular/lib/directive/ng_model.dart |
index 712bc7783abc897ba2a380adc5aa0eb03abc9e29..b71273c31e0b889d7c28c46416709ff034126637 100644 |
--- a/third_party/pkg/angular/lib/directive/ng_model.dart |
+++ b/third_party/pkg/angular/lib/directive/ng_model.dart |
@@ -1,6 +1,22 @@ |
part of angular.directive; |
/** |
+ * NgModelConverter is the class interface for performing transformations on |
+ * the viewValue and modelValue properties on a model. A new converter can be created |
+ * by implementing the NgModelConverter class and then attaching to a model via the |
+ * provided setter. |
+ */ |
+abstract class NgModelConverter { |
+ String get name; |
+ parse(value) => value; |
+ format(value) => value; |
+} |
+ |
+class _NoopModelConverter extends NgModelConverter { |
+ final name = 'ng-noop'; |
+} |
+ |
+/** |
* Ng-model directive is responsible for reading/writing to the model. |
* The directive itself is headless. (It does not know how to render or what |
* events to listen for.) It is meant to be used with other directives which |
@@ -10,155 +26,291 @@ part of angular.directive; |
* knows how to (in)validate the model and the form in which it is declared |
* (to be implemented) |
*/ |
-@NgDirective(selector: '[ng-model]') |
-class NgModel extends NgControl implements NgAttachAware { |
- final NgForm _form; |
- final AstParser _parser; |
+@Decorator(selector: '[ng-model]') |
+class NgModel extends NgControl implements AttachAware { |
+ final Scope _scope; |
- BoundGetter getter = ([_]) => null; |
BoundSetter setter = (_, [__]) => null; |
- var _lastValue; |
- String _exp; |
- final _validators = <NgValidatable>[]; |
+ String _expression; |
+ var _originalValue, _viewValue, _modelValue; |
+ bool _alwaysProcessViewValue; |
+ bool _toBeValidated = false; |
+ Function render = (value) => null; |
- Watch _removeWatch; |
+ final _validators = <NgValidator>[]; |
+ NgModelConverter _converter; |
+ Watch _watch; |
bool _watchCollection; |
- Function render = (value) => null; |
- NgModel(Scope _scope, dom.Element _element, Injector injector, |
- NgForm this._form, this._parser, NodeAttrs attrs) |
- : super(_scope, _element, injector) |
+ NgModel(this._scope, NgElement element, Injector injector, NodeAttrs attrs, |
+ Animate animate) |
+ : super(element, injector, animate) |
{ |
- _exp = attrs["ng-model"]; |
+ _expression = attrs["ng-model"]; |
watchCollection = false; |
+ |
+ //Since the user will never be editing the value of a select element then |
+ //there is no reason to guard the formatter from changing the DOM value. |
+ _alwaysProcessViewValue = element.node.tagName == 'SELECT'; |
+ converter = new _NoopModelConverter(); |
+ markAsUntouched(); |
+ markAsPristine(); |
} |
- process(value, [_]) { |
+ void _processViewValue(value) { |
validate(); |
- _scope.rootScope.domWrite(() => render(value)); |
+ _viewValue = converter.format(value); |
+ _scope.rootScope.domWrite(() => render(_viewValue)); |
} |
- attach() { |
+ void attach() { |
watchCollection = false; |
- _scope.on('resetNgModel').listen((e) => reset()); |
} |
- reset() { |
- untouched = true; |
- modelValue = _lastValue; |
+ /** |
+ * Resets the model value to it's original (pristine) value. If the model has been interacted |
+ * with by the user at all then the model will be also reset to an "untouched" state. |
+ */ |
+ void reset() { |
+ markAsUntouched(); |
+ _processViewValue(_originalValue); |
+ modelValue = _originalValue; |
+ } |
+ |
+ void onSubmit(bool valid) { |
+ super.onSubmit(valid); |
+ if (valid) _originalValue = modelValue; |
+ } |
+ |
+ void markAsUntouched() { |
+ removeInfoState(this, NgControl.NG_TOUCHED); |
+ } |
+ |
+ void markAsTouched() { |
+ addInfoState(this, NgControl.NG_TOUCHED); |
+ } |
+ |
+ void markAsPristine() { |
+ removeInfoState(this, NgControl.NG_DIRTY); |
+ } |
+ |
+ void markAsDirty() { |
+ addInfoState(this, NgControl.NG_DIRTY); |
+ } |
+ |
+ /** |
+ * Flags the model to be set for validation upon the next digest. This operation is useful |
+ * to optimize validations incase multiple validations are triggered one after the other. |
+ */ |
+ void validateLater() { |
+ if (_toBeValidated) return; |
+ _toBeValidated = true; |
+ _scope.rootScope.runAsync(() { |
+ if (_toBeValidated) { |
+ validate(); |
+ } |
+ }); |
+ } |
+ |
+ /** |
+ * Returns the associated converter that is used with the model. |
+ */ |
+ NgModelConverter get converter => _converter; |
+ set converter(NgModelConverter c) { |
+ _converter = c; |
+ _processViewValue(modelValue); |
} |
@NgAttr('name') |
- get name => _name; |
- set name(value) { |
+ String get name => _name; |
+ void set name(value) { |
_name = value; |
_parentControl.addControl(this); |
} |
// TODO(misko): could we get rid of watch collection, and just always watch the collection? |
- get watchCollection => _watchCollection; |
- set watchCollection(value) { |
+ bool get watchCollection => _watchCollection; |
+ void set watchCollection(value) { |
if (_watchCollection == value) return; |
+ |
+ var onChange = (value, [_]) { |
+ if (_alwaysProcessViewValue || _modelValue != value) { |
+ _modelValue = value; |
+ _processViewValue(value); |
+ } |
+ }; |
+ |
_watchCollection = value; |
- if (_removeWatch!=null) _removeWatch.remove(); |
+ if (_watch!=null) _watch.remove(); |
if (_watchCollection) { |
- _removeWatch = _scope.watch( |
- _parser(_exp, collection: true), |
- (changeRecord, _) { |
- var value = changeRecord is CollectionChangeRecord ? changeRecord.iterable: changeRecord; |
- process(value); |
- }); |
- } else if (_exp != null) { |
- _removeWatch = _scope.watch(_exp, process); |
+ _watch = _scope.watch(_expression, (changeRecord, _) { |
+ onChange(changeRecord is CollectionChangeRecord |
+ ? changeRecord.iterable |
+ : changeRecord); |
+ }, |
+ collection: true); |
+ } else if (_expression != null) { |
+ _watch = _scope.watch(_expression, onChange); |
} |
} |
// TODO(misko): getters/setters need to go. We need AST here. |
@NgCallback('ng-model') |
- set model(BoundExpression boundExpression) { |
- getter = boundExpression; |
+ void set model(BoundExpression boundExpression) { |
setter = boundExpression.assign; |
- |
_scope.rootScope.runAsync(() { |
- _lastValue = modelValue; |
+ _modelValue = boundExpression(); |
+ _originalValue = modelValue; |
+ _processViewValue(_modelValue); |
}); |
} |
- // TODO(misko): right now viewValue and modelValue are the same, |
- // but this needs to be changed to support converters and form validation |
- get viewValue => modelValue; |
- set viewValue(value) => modelValue = value; |
+ /** |
+ * Applies the given [error] to the model. |
+ */ |
+ void addError(String error) { |
+ this.addErrorState(this, error); |
+ } |
+ |
+ /** |
+ * Removes the given [error] from the model. |
+ */ |
+ void removeError(String error) { |
+ this.removeErrorState(this, error); |
+ } |
+ |
+ /** |
+ * Adds the given [info] state to the model. |
+ */ |
+ void addInfo(String info) { |
+ this.addInfoState(this, info); |
+ } |
+ |
+ /** |
+ * Removes the given [info] state from the model. |
+ */ |
+ void removeInfo(String info) { |
+ this.removeInfoState(this, info); |
+ } |
+ |
+ get viewValue => _viewValue; |
+ void set viewValue(value) { |
+ _viewValue = value; |
+ modelValue = value; |
+ } |
- get modelValue => getter(); |
- set modelValue(value) => setter(value); |
+ get modelValue => _modelValue; |
+ void set modelValue(value) { |
+ try { |
+ value = converter.parse(value); |
+ } catch(e) { |
+ value = null; |
+ } |
+ _modelValue = value; |
+ setter(value); |
- get validators => _validators; |
+ if (modelValue == _originalValue) { |
+ markAsPristine(); |
+ } else { |
+ markAsDirty(); |
+ } |
+ } |
+ |
+ /** |
+ * Returns the list of validators that are registered on the model. |
+ */ |
+ List<NgValidator> get validators => _validators; |
/** |
- * Executes a validation on the form against each of the validation present on the model. |
+ * Executes a validation on the model against each of the validators present on the model. |
+ * Once complete, the model will either be set as valid or invalid. |
*/ |
- validate() { |
+ void validate() { |
+ _toBeValidated = false; |
if (validators.isNotEmpty) { |
validators.forEach((validator) { |
- setValidity(validator.name, validator.isValid(viewValue)); |
+ if (validator.isValid(modelValue)) { |
+ removeError(validator.name); |
+ } else { |
+ addError(validator.name); |
+ } |
}); |
- } else { |
- valid = true; |
} |
- } |
- setValidity(String name, bool valid) { |
- this.updateControlValidity(this, name, valid); |
+ if (invalid) { |
+ addInfo(NgControl.NG_INVALID); |
+ } else { |
+ removeInfo(NgControl.NG_INVALID); |
+ } |
} |
/** |
* Registers a validator into the model to consider when running validate(). |
*/ |
- addValidator(NgValidatable v) { |
+ void addValidator(NgValidator v) { |
validators.add(v); |
- validate(); |
+ validateLater(); |
} |
/** |
* De-registers a validator from the model. |
*/ |
- removeValidator(NgValidatable v) { |
+ void removeValidator(NgValidator v) { |
validators.remove(v); |
- validate(); |
+ validateLater(); |
} |
} |
/** |
* Usage: |
* |
- * <input type="checkbox" ng-model="flag"> |
+ * <input type="checkbox" |
+ * ng-model="expr" |
+ * [ng-true-value="t_expr"] |
+ * [ng-false-value="f_expr"] |
+ * > |
+ * |
+ * This creates a two way databinding between the `ng-model` expression |
+ * and the checkbox input element state. |
+ * |
+ * If the optional `ng-true-value` is absent then: if the model expression |
+ * evaluates to true or to a nonzero [:num:], then the checkbox is checked; |
+ * otherwise, it is unchecked. |
+ * |
+ * If `ng-true-value="t_expr"` is present, then: if the model expression |
+ * evaluates to the same value as `t_expr` then the checkbox is checked; |
+ * otherwise, it is unchecked. |
+ * |
+ * When the checkbox is checked, the model is set to the value of `t_expr` if |
+ * present, true otherwise. When unchecked, it is set to the value of |
+ * `f_expr` if present, false otherwise. |
* |
- * This creates a two way databinding between the boolean expression specified |
- * in ng-model and the checkbox input element in the DOM. If the ng-model value |
- * is falsy (i.e. one of `false`, `null`, and `0`), then the checkbox is |
- * unchecked. Otherwise, it is checked. Likewise, when the checkbox is checked, |
- * the model value is set to true. When unchecked, it is set to false. |
+ * Also see [NgTrueValue] and [NgFalseValue]. |
*/ |
-@NgDirective(selector: 'input[type=checkbox][ng-model]') |
-class InputCheckboxDirective { |
- final dom.InputElement inputElement; |
+@Decorator(selector: 'input[type=checkbox][ng-model]') |
+class InputCheckbox { |
+ final dom.CheckboxInputElement inputElement; |
final NgModel ngModel; |
final NgTrueValue ngTrueValue; |
final NgFalseValue ngFalseValue; |
final Scope scope; |
- InputCheckboxDirective(dom.Element this.inputElement, this.ngModel, |
- this.scope, this.ngTrueValue, this.ngFalseValue) { |
+ InputCheckbox(dom.Element this.inputElement, this.ngModel, |
+ this.scope, this.ngTrueValue, this.ngFalseValue) { |
ngModel.render = (value) { |
- inputElement.checked = ngTrueValue.isValue(inputElement, value); |
+ scope.rootScope.domWrite(() { |
+ inputElement.checked = ngTrueValue.isValue(value); |
+ }); |
}; |
- inputElement.onChange.listen((value) { |
- ngModel.dirty = true; |
- ngModel.viewValue = inputElement.checked |
- ? ngTrueValue.readValue(inputElement) |
- : ngFalseValue.readValue(inputElement); |
- }); |
+ inputElement |
+ ..onChange.listen((_) { |
+ ngModel.viewValue = inputElement.checked |
+ ? ngTrueValue.value : ngFalseValue.value; |
+ }) |
+ ..onBlur.listen((e) { |
+ ngModel.markAsTouched(); |
+ }); |
} |
} |
@@ -169,55 +321,52 @@ class InputCheckboxDirective { |
* <textarea ng-model="myModel"></textarea> |
* |
* This creates a two-way binding between any string-based input element |
- * (both <input> and <textarea>) so long as the ng-model attribute is |
+ * (both `<input>` and `<textarea>`) so long as the ng-model attribute is |
* present on the input element. Whenever the value of the input element |
* changes then the matching model property on the scope will be updated |
* as well as the other way around (when the scope property is updated). |
* |
*/ |
-@NgDirective(selector: 'textarea[ng-model]') |
-@NgDirective(selector: 'input[type=text][ng-model]') |
-@NgDirective(selector: 'input[type=password][ng-model]') |
-@NgDirective(selector: 'input[type=url][ng-model]') |
-@NgDirective(selector: 'input[type=email][ng-model]') |
-@NgDirective(selector: 'input[type=search][ng-model]') |
-class InputTextLikeDirective { |
+@Decorator(selector: 'textarea[ng-model]') |
+@Decorator(selector: 'input[type=text][ng-model]') |
+@Decorator(selector: 'input[type=password][ng-model]') |
+@Decorator(selector: 'input[type=url][ng-model]') |
+@Decorator(selector: 'input[type=email][ng-model]') |
+@Decorator(selector: 'input[type=search][ng-model]') |
+class InputTextLike { |
final dom.Element inputElement; |
final NgModel ngModel; |
final Scope scope; |
String _inputType; |
get typedValue => (inputElement as dynamic).value; |
- set typedValue(value) => (inputElement as dynamic).value = (value == null) ? |
- '' : |
- value.toString(); |
+ void set typedValue(value) { |
+ (inputElement as dynamic).value = (value == null) ? '' : value.toString(); |
+ } |
- InputTextLikeDirective(this.inputElement, this.ngModel, this.scope) { |
+ InputTextLike(this.inputElement, this.ngModel, this.scope) { |
ngModel.render = (value) { |
- if (value == null) value = ''; |
- |
- var currentValue = typedValue; |
- if (value != currentValue && !(value is num && currentValue is num && |
- value.isNaN && currentValue.isNaN)) { |
- typedValue = value; |
- } |
+ scope.rootScope.domWrite(() { |
+ if (value == null) value = ''; |
+ |
+ var currentValue = typedValue; |
+ if (value != currentValue && !(value is num && currentValue is num && |
+ value.isNaN && currentValue.isNaN)) { |
+ typedValue = value; |
+ } |
+ }); |
}; |
inputElement |
..onChange.listen(processValue) |
..onInput.listen(processValue) |
..onBlur.listen((e) { |
- if (ngModel.touched == null || ngModel.touched == false) { |
- ngModel.touched = true; |
- } |
+ ngModel.markAsTouched(); |
}); |
} |
- processValue([_]) { |
+ void processValue([_]) { |
var value = typedValue; |
- if (value != ngModel.viewValue) { |
- ngModel.dirty = true; |
- ngModel.viewValue = value; |
- } |
+ if (value != ngModel.viewValue) ngModel.viewValue = value; |
ngModel.validate(); |
} |
} |
@@ -233,20 +382,23 @@ class InputTextLikeDirective { |
* |
* This creates a two-way binding between the input and the named model property |
* (e.g., myModel in the example above). When processing the input, its value is |
- * read as a [num], via the [dom.InputElement.valueAsNumber] field. If the input |
- * text does not represent a number, then the model is appropriately set to |
- * [double.NAN]. Setting the model property to [null] will clear the input. |
+ * read as a [:num:], via the [dom.InputElement.valueAsNumber] field. If the |
+ * input text does not represent a number, then the model is appropriately set |
+ * to [double.NAN]. Setting the model property to [null] will clear the input. |
* Setting the model to [double.NAN] will have no effect (input will be left |
* unchanged). |
*/ |
-@NgDirective(selector: 'input[type=number][ng-model]') |
-@NgDirective(selector: 'input[type=range][ng-model]') |
-class InputNumberLikeDirective { |
+@Decorator(selector: 'input[type=number][ng-model]') |
+@Decorator(selector: 'input[type=range][ng-model]') |
+class InputNumberLike { |
final dom.InputElement inputElement; |
final NgModel ngModel; |
final Scope scope; |
- num get typedValue => inputElement.valueAsNumber; |
+ |
+ // We can't use [inputElement.valueAsNumber] due to http://dartbug.com/15788 |
+ num get typedValue => num.parse(inputElement.value, (v) => double.NAN); |
+ |
void set typedValue(num value) { |
// [chalin, 2014-02-16] This post |
// http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.html |
@@ -256,26 +408,214 @@ class InputNumberLikeDirective { |
if (value == null) { |
inputElement.value = null; |
} else { |
- inputElement.valueAsNumber = value; |
+ // We can't use inputElement.valueAsNumber due to http://dartbug.com/15788 |
+ inputElement.value = "$value"; |
} |
} |
- InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.scope) { |
+ InputNumberLike(dom.Element this.inputElement, this.ngModel, this.scope) { |
ngModel.render = (value) { |
- if (value != typedValue |
- && (value == null || value is num && !value.isNaN)) { |
- typedValue = value; |
- } |
+ scope.rootScope.domWrite(() { |
+ if (value != typedValue |
+ && (value == null || value is num && !value.isNaN)) { |
+ typedValue = value; |
+ } |
+ }); |
}; |
inputElement |
..onChange.listen(relaxFnArgs(processValue)) |
- ..onInput.listen(relaxFnArgs(processValue)); |
+ ..onInput.listen(relaxFnArgs(processValue)) |
+ ..onBlur.listen((e) { |
+ ngModel.markAsTouched(); |
+ }); |
} |
- processValue() { |
+ void processValue() { |
num value = typedValue; |
if (value != ngModel.viewValue) { |
- ngModel.dirty = true; |
+ scope.eval(() => ngModel.viewValue = value); |
+ } |
+ ngModel.validate(); |
+ } |
+} |
+ |
+/** |
+ * This directive affects which IDL attribute will be used to read the value of |
+ * date/time related input directives. Recognized values for this directive are: |
+ * |
+ * - [DATE]: [dom.InputElement].valueAsDate will be read. |
+ * - [NUMBER]: [dom.InputElement].valueAsNumber will be read. |
+ * - [STRING]: [dom.InputElement].value will be read. |
+ * |
+ * The default is [DATE]. Use other settings, e.g., when an app needs to support |
+ * browsers that treat date-like inputs as text (in such a case the [STRING] |
+ * kind would be appropriate) or, for browsers that fail to conform to the |
+ * HTML5 standard in their processing of date-like inputs. |
+ */ |
+@Decorator(selector: 'input[type=date][ng-model][ng-bind-type]') |
+@Decorator(selector: 'input[type=time][ng-model][ng-bind-type]') |
+@Decorator(selector: 'input[type=datetime][ng-model][ng-bind-type]') |
+@Decorator(selector: 'input[type=datetime-local][ng-model][ng-bind-type]') |
+@Decorator(selector: 'input[type=month][ng-model][ng-bind-type]') |
+@Decorator(selector: 'input[type=week][ng-model][ng-bind-type]') |
+class NgBindTypeForDateLike { |
+ static const DATE = 'date'; |
+ static const NUMBER = 'number'; |
+ static const STRING = 'string'; |
+ static const DEFAULT = DATE; |
+ static const VALID_VALUES = const <String>[DATE, NUMBER, STRING]; |
+ |
+ final dom.InputElement inputElement; |
+ String _idlAttrKind = DEFAULT; |
+ |
+ NgBindTypeForDateLike(dom.Element this.inputElement); |
+ |
+ @NgAttr('ng-bind-type') |
+ void set idlAttrKind(final String _kind) { |
+ String kind = _kind == null ? DEFAULT : _kind.toLowerCase(); |
+ if (!VALID_VALUES.contains(kind)) |
+ throw "Unsupported ng-bind-type attribute value '$_kind'; " |
+ "it should be one of $VALID_VALUES"; |
+ _idlAttrKind = kind; |
+ } |
+ |
+ String get idlAttrKind => _idlAttrKind; |
+ |
+ dynamic get inputTypedValue { |
+ switch (idlAttrKind) { |
+ case DATE: return inputValueAsDate; |
+ case NUMBER: return inputElement.valueAsNumber; |
+ default: return inputElement.value; |
+ } |
+ } |
+ |
+ void set inputTypedValue(dynamic inputValue) { |
+ if (inputValue is DateTime) { |
+ inputValueAsDate = inputValue; |
+ } else if (inputValue is num) { |
+ inputElement.valueAsNumber = inputValue; |
+ } else { |
+ inputElement.value = inputValue; |
+ } |
+ } |
+ |
+ /// Input's `valueAsDate` normalized to UTC (per HTML5 std). |
+ DateTime get inputValueAsDate { |
+ DateTime dt; |
+ // Wrap in try-catch due to |
+ // https://code.google.com/p/dart/issues/detail?id=17625 |
+ try { |
+ dt = inputElement.valueAsDate; |
+ } catch (e) { |
+ dt = null; |
+ } |
+ return (dt != null && !dt.isUtc) ? dt.toUtc() : dt; |
+ } |
+ |
+ /// Set input's `valueAsDate`. Argument is normalized to UTC if necessary |
+ /// (per HTML standard). |
+ void set inputValueAsDate(DateTime dt) { |
+ inputElement.valueAsDate = (dt != null && !dt.isUtc) ? dt.toUtc() : dt; |
+ } |
+} |
+ |
+/** |
+ * **Background: Standards and Browsers** |
+ * |
+ * According to the |
+ * [HTML5 Standard](http://www.w3.org/TR/html5/forms.html#the-input-element), |
+ * the [dom.InputElement.valueAsDate] and [dom.InputElement.valueAsNumber] IDL |
+ * attributes should be available for all date/time related input types, |
+ * except for `datetime-local` which is limited to |
+ * [dom.InputElement.valueNumber]. Of course, all input types support |
+ * [dom.InputElement.value] which yields a [String]; |
+ * [dom.InputElement.valueAsDate] yields a [DateTime] and |
+ * [dom.InputElement.valueNumber] yields a [num]. |
+ * |
+ * But not all browsers currently support date/time related inputs and of |
+ * those that do, some deviate from the standard. Hence, this directive |
+ * allows developers to control the IDL attribute that will be used |
+ * to read the value of a date/time input. This is achieved via the subordinate |
+ * 'ng-bind-type' directive; see [NgBindTypeForDateLike] for details. |
+ * |
+ * **Usage**: |
+ * |
+ * <input type="date|datetime|datetime-local|month|time|week" |
+ * [ng-bind-type="date"] |
+ * ng-model="myModel"> |
+ * |
+ * **Model**: |
+ * |
+ * dynamic myModel; // one of DateTime | num | String |
+ * |
+ * This directive creates a two-way binding between the input and a model |
+ * property. The subordinate 'ng-bind-type' directive determines which input |
+ * IDL attribute is read (see [NgBindTypeForDateLike] for details) and |
+ * hence the type of the read values. The type of the model property value |
+ * determines which IDL attribute is written to: [DateTime] and [num] values |
+ * are assigned to [dom.InputElement.valueAsDate] and |
+ * [dom.InputElement.valueNumber], respectively; [String] and `null` values |
+ * are assigned to [dom.InputElement.value]. Setting the model to `null` will |
+ * clear the input if it is currently valid, otherwise, invalid input is left |
+ * untouched (so that the user has an opportunity to correct it). To clear the |
+ * input unconditionally, set the model property to the empty string (''). |
+ * |
+ * **Notes**: |
+ * - As prescribed by the HTML5 standard, [DateTime] values returned by the |
+ * `valueAsDate` IDL attribute are meant to be in UTC. |
+ * - As of the HTML5 Editor's Draft 29 March 2014, datetime-local is no longer |
+ * part of the standard. Other date related input are also at risk of being |
+ * dropped. |
+ */ |
+ |
+@Decorator(selector: 'input[type=date][ng-model]', |
+ module: InputDateLike.moduleFactory) |
+@Decorator(selector: 'input[type=time][ng-model]', |
+ module: InputDateLike.moduleFactory) |
+@Decorator(selector: 'input[type=datetime][ng-model]', |
+ module: InputDateLike.moduleFactory) |
+@Decorator(selector: 'input[type=datetime-local][ng-model]', |
+ module: InputDateLike.moduleFactory) |
+@Decorator(selector: 'input[type=month][ng-model]', |
+ module: InputDateLike.moduleFactory) |
+@Decorator(selector: 'input[type=week][ng-model]', |
+ module: InputDateLike.moduleFactory) |
+class InputDateLike { |
+ static Module moduleFactory() => new Module()..factory(NgBindTypeForDateLike, |
+ (Injector i) => new NgBindTypeForDateLike(i.get(dom.Element))); |
+ final dom.InputElement inputElement; |
+ final NgModel ngModel; |
+ final Scope scope; |
+ NgBindTypeForDateLike ngBindType; |
+ |
+ InputDateLike(dom.Element this.inputElement, this.ngModel, this.scope, |
+ this.ngBindType) { |
+ if (inputElement.type == 'datetime-local') { |
+ ngBindType.idlAttrKind = NgBindTypeForDateLike.NUMBER; |
+ } |
+ ngModel.render = (value) { |
+ scope.rootScope.domWrite(() { |
+ if (!eqOrNaN(value, typedValue)) typedValue = value; |
+ }); |
+ }; |
+ inputElement |
+ ..onChange.listen(relaxFnArgs(processValue)) |
+ ..onInput.listen(relaxFnArgs(processValue)) |
+ ..onBlur.listen((e) { |
+ ngModel.markAsTouched(); |
+ }); |
+ } |
+ |
+ dynamic get typedValue => ngBindType.inputTypedValue; |
+ |
+ void set typedValue(dynamic value) { |
+ ngBindType.inputTypedValue = value; |
+ } |
+ |
+ void processValue() { |
+ var value = typedValue; |
+ // print("processValue: value=$value, model=${ngModel.viewValue}"); |
+ if (!eqOrNaN(value, ngModel.viewValue)) { |
scope.eval(() => ngModel.viewValue = value); |
} |
ngModel.validate(); |
@@ -287,7 +627,7 @@ class _UidCounter { |
static final int CHAR_9 = "9".codeUnitAt(0); |
static final int CHAR_A = "A".codeUnitAt(0); |
static final int CHAR_Z = "Z".codeUnitAt(0); |
- List charCodes = [CHAR_0, CHAR_0, CHAR_0]; |
+ final charCodes = [CHAR_0, CHAR_0, CHAR_0]; |
String next() { |
for (int i = charCodes.length - 1; i >= 0; i--) { |
@@ -310,64 +650,79 @@ class _UidCounter { |
final _uidCounter = new _UidCounter(); |
/** |
- * Use `ng-value` directive with `<input type="radio">` or `<option>` to |
- * allow binding to values other then strings. This is needed since the |
- * `value` attribute on DOM element `<input type="radio" value="foo">` can |
- * only be a string. With `ng-value` one can bind to any object. |
+ * Usage: |
+ * |
+ * <input type=radio ng-model=model [ng-value=expr]> |
+ * |
+ * <option [ng-value=expr]>...</option> |
+ * |
+ * Example: |
+ * |
+ * <select ng-model="robot"> |
+ * <option ng-repeat="r in robots" ng-value="r">{{r.name}}</option> |
+ * </select> |
+ * |
+ * When present, the value of this `ng-value` one-way attribute is assigned to |
+ * the `ng-model` property when the corresponding radio element or option is |
+ * selected. Note that `expr` can be not any type; i.e., it is not restricted |
+ * to [String]. |
*/ |
-@NgDirective(selector: '[ng-value]') |
+@Decorator(selector: 'input[type=radio][ng-model][ng-value]') |
+@Decorator(selector: 'option[ng-value]') |
class NgValue { |
+ static Module _module = new Module()..type(NgValue); |
+ static Module moduleFactory() => _module; |
+ |
final dom.Element element; |
- @NgOneWay('ng-value') |
- var value; |
+ var _value; |
NgValue(this.element); |
- readValue(dom.Element element) { |
- assert(this.element == null || element == this.element); |
- return this.element == null ? (element as dynamic).value : value; |
- } |
+ @NgOneWay('ng-value') |
+ void set value(val) { this._value = val; } |
+ dynamic get value => _value == null ? (element as dynamic).value : _value; |
} |
/** |
- * `ng-true-value` allows you to select any expression to be set to |
- * `ng-model` when checkbox is selected on `<input type="checkbox">`. |
+ * Usage: |
+ * |
+ * <input type=checkbox |
+ * ng-model=model |
+ * [ng-true-value=expr]> |
+ * |
+ * The initial value of the expression bound to this directive is assigned to |
+ * the model when the input is checked. Note that the expression can be of any |
+ * type, not just [String]. Also see [InputCheckboxDirective], [NgFalseValue]. |
*/ |
-@NgDirective(selector: '[ng-true-value]') |
+@Decorator(selector: 'input[type=checkbox][ng-model][ng-true-value]') |
class NgTrueValue { |
final dom.Element element; |
@NgOneWay('ng-true-value') |
- var value; |
- |
- NgTrueValue(this.element); |
+ var value = true; |
- readValue(dom.Element element) { |
- assert(this.element == null || element == this.element); |
- return this.element == null ? true : value; |
- } |
+ NgTrueValue([this.element]); |
- isValue(dom.Element element, value) { |
- assert(this.element == null || element == this.element); |
- return this.element == null ? toBool(value) : value == this.value; |
- } |
+ bool isValue(val) => element == null ? toBool(val) : val == value; |
} |
/** |
- * `ng-false-value` allows you to select any expression to be set to |
- * `ng-model` when checkbox is deselected<input type="checkbox">`. |
+ * Usage: |
+ * |
+ * <input type=checkbox |
+ * ng-model=model |
+ * [ng-false-value=expr]> |
+ * |
+ * The initial value of the expression bound to this directive is assigned to |
+ * the model when the input is unchecked. Note that the expression can be of any |
+ * type, not just [String]. Also see [InputCheckboxDirective], [NgTrueValue]. |
*/ |
-@NgDirective(selector: '[ng-false-value]') |
+@Decorator(selector: 'input[type=checkbox][ng-model][ng-false-value]') |
class NgFalseValue { |
final dom.Element element; |
@NgOneWay('ng-false-value') |
- var value; |
- |
- NgFalseValue(this.element); |
+ var value = false; |
- readValue(dom.Element element) { |
- assert(this.element == null || element == this.element); |
- return this.element == null ? false : value; |
- } |
+ NgFalseValue([this.element]); |
} |
/** |
@@ -376,39 +731,44 @@ class NgFalseValue { |
* <input type="radio" ng-model="category"> |
* |
* This creates a two way databinding between the expression specified in |
- * ng-model and the range input elements in the DOM. If the ng-model value is |
+ * ng-model and the range input elements in the DOM. If the ng-model value is |
* set to a value not corresponding to one of the radio elements, then none of |
* the radio elements will be check. Otherwise, only the corresponding input |
* element in the group is checked. Likewise, when a radio button element is |
* checked, the model is updated with its value. Radio buttons that have a |
* `name` attribute are left alone. Those that are missing the attribute will |
* have a unique `name` assigned to them. This sequence goes `001`, `001`, ... |
- * `009`, `00A`, `00Z`, `010`, … and so on using more than 3 characters for the |
+ * `009`, `00A`, `00Z`, `010`, and so on using more than 3 characters for the |
* name when the counter overflows. |
*/ |
-@NgDirective(selector: 'input[type=radio][ng-model]') |
-class InputRadioDirective { |
+@Decorator( |
+ selector: 'input[type=radio][ng-model]', |
+ module: NgValue.moduleFactory) |
+class InputRadio { |
final dom.RadioButtonInputElement radioButtonElement; |
final NgModel ngModel; |
final NgValue ngValue; |
final Scope scope; |
- InputRadioDirective(dom.Element this.radioButtonElement, this.ngModel, |
- this.scope, this.ngValue, NodeAttrs attrs) { |
+ InputRadio(dom.Element this.radioButtonElement, this.ngModel, |
+ this.scope, this.ngValue, NodeAttrs attrs) { |
// If there's no "name" set, we'll set a unique name. This ensures |
// less surprising behavior about which radio buttons are grouped together. |
if (attrs['name'] == '' || attrs['name'] == null) { |
attrs["name"] = _uidCounter.next(); |
} |
ngModel.render = (value) { |
- radioButtonElement.checked = (value == ngValue.readValue(radioButtonElement)); |
+ scope.rootScope.domWrite(() { |
+ radioButtonElement.checked = (value == ngValue.value); |
+ }); |
}; |
- radioButtonElement.onClick.listen((_) { |
- if (radioButtonElement.checked) { |
- ngModel.dirty = true; |
- ngModel.viewValue = ngValue.readValue(radioButtonElement); |
- } |
- }); |
+ radioButtonElement |
+ ..onClick.listen((_) { |
+ if (radioButtonElement.checked) ngModel.viewValue = ngValue.value; |
+ }) |
+ ..onBlur.listen((e) { |
+ ngModel.markAsTouched(); |
+ }); |
} |
} |
@@ -418,18 +778,18 @@ class InputRadioDirective { |
* <span contenteditable= ng-model="name"> |
* |
* This creates a two way databinding between the expression specified in |
- * ng-model and the html element in the DOM. If the ng-model value is |
+ * ng-model and the html element in the DOM. If the ng-model value is |
* `null`, it is treated as equivalent to the empty string for rendering |
* purposes. |
*/ |
-@NgDirective(selector: '[contenteditable][ng-model]') |
-class ContentEditableDirective extends InputTextLikeDirective { |
- ContentEditableDirective(dom.Element inputElement, NgModel ngModel, |
- Scope scope) |
+@Decorator(selector: '[contenteditable][ng-model]') |
+class ContentEditable extends InputTextLike { |
+ ContentEditable(dom.Element inputElement, NgModel ngModel, Scope scope) |
: super(inputElement, ngModel, scope); |
- // The implementation is identical to InputTextLikeDirective but use innerHtml instead of value |
- get typedValue => (inputElement as dynamic).innerHtml; |
- set typedValue(String value) => |
- (inputElement as dynamic).innerHtml = (value == null) ? '' : value; |
+ // The implementation is identical to InputTextLike but use innerHtml instead of value |
+ String get typedValue => (inputElement as dynamic).innerHtml; |
+ void set typedValue(String value) { |
+ (inputElement as dynamic).innerHtml = (value == null) ? '' : value; |
+ } |
} |