| OLD | NEW |
| 1 part of angular.directive; | 1 part of angular.directive; |
| 2 | 2 |
| 3 /** | 3 /** |
| 4 * NgModelConverter is the class interface for performing transformations on |
| 5 * the viewValue and modelValue properties on a model. A new converter can be cr
eated |
| 6 * by implementing the NgModelConverter class and then attaching to a model via
the |
| 7 * provided setter. |
| 8 */ |
| 9 abstract class NgModelConverter { |
| 10 String get name; |
| 11 parse(value) => value; |
| 12 format(value) => value; |
| 13 } |
| 14 |
| 15 class _NoopModelConverter extends NgModelConverter { |
| 16 final name = 'ng-noop'; |
| 17 } |
| 18 |
| 19 /** |
| 4 * Ng-model directive is responsible for reading/writing to the model. | 20 * Ng-model directive is responsible for reading/writing to the model. |
| 5 * The directive itself is headless. (It does not know how to render or what | 21 * The directive itself is headless. (It does not know how to render or what |
| 6 * events to listen for.) It is meant to be used with other directives which | 22 * events to listen for.) It is meant to be used with other directives which |
| 7 * provide the rendering and listening capabilities. The directive itself | 23 * provide the rendering and listening capabilities. The directive itself |
| 8 * knows how to convert the view-value into model-value and vice versa by | 24 * knows how to convert the view-value into model-value and vice versa by |
| 9 * allowing others to register converters (To be implemented). It also | 25 * allowing others to register converters (To be implemented). It also |
| 10 * knows how to (in)validate the model and the form in which it is declared | 26 * knows how to (in)validate the model and the form in which it is declared |
| 11 * (to be implemented) | 27 * (to be implemented) |
| 12 */ | 28 */ |
| 13 @NgDirective(selector: '[ng-model]') | 29 @Decorator(selector: '[ng-model]') |
| 14 class NgModel extends NgControl implements NgAttachAware { | 30 class NgModel extends NgControl implements AttachAware { |
| 15 final NgForm _form; | 31 final Scope _scope; |
| 16 final AstParser _parser; | 32 |
| 17 | |
| 18 BoundGetter getter = ([_]) => null; | |
| 19 BoundSetter setter = (_, [__]) => null; | 33 BoundSetter setter = (_, [__]) => null; |
| 20 | 34 |
| 21 var _lastValue; | 35 String _expression; |
| 22 String _exp; | 36 var _originalValue, _viewValue, _modelValue; |
| 23 final _validators = <NgValidatable>[]; | 37 bool _alwaysProcessViewValue; |
| 24 | 38 bool _toBeValidated = false; |
| 25 Watch _removeWatch; | 39 Function render = (value) => null; |
| 40 |
| 41 final _validators = <NgValidator>[]; |
| 42 NgModelConverter _converter; |
| 43 Watch _watch; |
| 26 bool _watchCollection; | 44 bool _watchCollection; |
| 27 Function render = (value) => null; | 45 |
| 28 | 46 NgModel(this._scope, NgElement element, Injector injector, NodeAttrs attrs, |
| 29 NgModel(Scope _scope, dom.Element _element, Injector injector, | 47 Animate animate) |
| 30 NgForm this._form, this._parser, NodeAttrs attrs) | 48 : super(element, injector, animate) |
| 31 : super(_scope, _element, injector) | |
| 32 { | 49 { |
| 33 _exp = attrs["ng-model"]; | 50 _expression = attrs["ng-model"]; |
| 34 watchCollection = false; | 51 watchCollection = false; |
| 35 } | 52 |
| 36 | 53 //Since the user will never be editing the value of a select element then |
| 37 process(value, [_]) { | 54 //there is no reason to guard the formatter from changing the DOM value. |
| 55 _alwaysProcessViewValue = element.node.tagName == 'SELECT'; |
| 56 converter = new _NoopModelConverter(); |
| 57 markAsUntouched(); |
| 58 markAsPristine(); |
| 59 } |
| 60 |
| 61 void _processViewValue(value) { |
| 38 validate(); | 62 validate(); |
| 39 _scope.rootScope.domWrite(() => render(value)); | 63 _viewValue = converter.format(value); |
| 40 } | 64 _scope.rootScope.domWrite(() => render(_viewValue)); |
| 41 | 65 } |
| 42 attach() { | 66 |
| 67 void attach() { |
| 43 watchCollection = false; | 68 watchCollection = false; |
| 44 _scope.on('resetNgModel').listen((e) => reset()); | 69 } |
| 45 } | 70 |
| 46 | 71 /** |
| 47 reset() { | 72 * Resets the model value to it's original (pristine) value. If the model has
been interacted |
| 48 untouched = true; | 73 * with by the user at all then the model will be also reset to an "untouched
" state. |
| 49 modelValue = _lastValue; | 74 */ |
| 75 void reset() { |
| 76 markAsUntouched(); |
| 77 _processViewValue(_originalValue); |
| 78 modelValue = _originalValue; |
| 79 } |
| 80 |
| 81 void onSubmit(bool valid) { |
| 82 super.onSubmit(valid); |
| 83 if (valid) _originalValue = modelValue; |
| 84 } |
| 85 |
| 86 void markAsUntouched() { |
| 87 removeInfoState(this, NgControl.NG_TOUCHED); |
| 88 } |
| 89 |
| 90 void markAsTouched() { |
| 91 addInfoState(this, NgControl.NG_TOUCHED); |
| 92 } |
| 93 |
| 94 void markAsPristine() { |
| 95 removeInfoState(this, NgControl.NG_DIRTY); |
| 96 } |
| 97 |
| 98 void markAsDirty() { |
| 99 addInfoState(this, NgControl.NG_DIRTY); |
| 100 } |
| 101 |
| 102 /** |
| 103 * Flags the model to be set for validation upon the next digest. This operat
ion is useful |
| 104 * to optimize validations incase multiple validations are triggered one afte
r the other. |
| 105 */ |
| 106 void validateLater() { |
| 107 if (_toBeValidated) return; |
| 108 _toBeValidated = true; |
| 109 _scope.rootScope.runAsync(() { |
| 110 if (_toBeValidated) { |
| 111 validate(); |
| 112 } |
| 113 }); |
| 114 } |
| 115 |
| 116 /** |
| 117 * Returns the associated converter that is used with the model. |
| 118 */ |
| 119 NgModelConverter get converter => _converter; |
| 120 set converter(NgModelConverter c) { |
| 121 _converter = c; |
| 122 _processViewValue(modelValue); |
| 50 } | 123 } |
| 51 | 124 |
| 52 @NgAttr('name') | 125 @NgAttr('name') |
| 53 get name => _name; | 126 String get name => _name; |
| 54 set name(value) { | 127 void set name(value) { |
| 55 _name = value; | 128 _name = value; |
| 56 _parentControl.addControl(this); | 129 _parentControl.addControl(this); |
| 57 } | 130 } |
| 58 | 131 |
| 59 // TODO(misko): could we get rid of watch collection, and just always watch th
e collection? | 132 // TODO(misko): could we get rid of watch collection, and just always watch th
e collection? |
| 60 get watchCollection => _watchCollection; | 133 bool get watchCollection => _watchCollection; |
| 61 set watchCollection(value) { | 134 void set watchCollection(value) { |
| 62 if (_watchCollection == value) return; | 135 if (_watchCollection == value) return; |
| 136 |
| 137 var onChange = (value, [_]) { |
| 138 if (_alwaysProcessViewValue || _modelValue != value) { |
| 139 _modelValue = value; |
| 140 _processViewValue(value); |
| 141 } |
| 142 }; |
| 143 |
| 63 _watchCollection = value; | 144 _watchCollection = value; |
| 64 if (_removeWatch!=null) _removeWatch.remove(); | 145 if (_watch!=null) _watch.remove(); |
| 65 if (_watchCollection) { | 146 if (_watchCollection) { |
| 66 _removeWatch = _scope.watch( | 147 _watch = _scope.watch(_expression, (changeRecord, _) { |
| 67 _parser(_exp, collection: true), | 148 onChange(changeRecord is CollectionChangeRecord |
| 68 (changeRecord, _) { | 149 ? changeRecord.iterable |
| 69 var value = changeRecord is CollectionChangeRecord ? changeRecord.it
erable: changeRecord; | 150 : changeRecord); |
| 70 process(value); | 151 }, |
| 71 }); | 152 collection: true); |
| 72 } else if (_exp != null) { | 153 } else if (_expression != null) { |
| 73 _removeWatch = _scope.watch(_exp, process); | 154 _watch = _scope.watch(_expression, onChange); |
| 74 } | 155 } |
| 75 } | 156 } |
| 76 | 157 |
| 77 // TODO(misko): getters/setters need to go. We need AST here. | 158 // TODO(misko): getters/setters need to go. We need AST here. |
| 78 @NgCallback('ng-model') | 159 @NgCallback('ng-model') |
| 79 set model(BoundExpression boundExpression) { | 160 void set model(BoundExpression boundExpression) { |
| 80 getter = boundExpression; | |
| 81 setter = boundExpression.assign; | 161 setter = boundExpression.assign; |
| 82 | |
| 83 _scope.rootScope.runAsync(() { | 162 _scope.rootScope.runAsync(() { |
| 84 _lastValue = modelValue; | 163 _modelValue = boundExpression(); |
| 164 _originalValue = modelValue; |
| 165 _processViewValue(_modelValue); |
| 85 }); | 166 }); |
| 86 } | 167 } |
| 87 | 168 |
| 88 // TODO(misko): right now viewValue and modelValue are the same, | 169 /** |
| 89 // but this needs to be changed to support converters and form validation | 170 * Applies the given [error] to the model. |
| 90 get viewValue => modelValue; | 171 */ |
| 91 set viewValue(value) => modelValue = value; | 172 void addError(String error) { |
| 92 | 173 this.addErrorState(this, error); |
| 93 get modelValue => getter(); | 174 } |
| 94 set modelValue(value) => setter(value); | 175 |
| 95 | 176 /** |
| 96 get validators => _validators; | 177 * Removes the given [error] from the model. |
| 97 | 178 */ |
| 98 /** | 179 void removeError(String error) { |
| 99 * Executes a validation on the form against each of the validation present on
the model. | 180 this.removeErrorState(this, error); |
| 181 } |
| 182 |
| 183 /** |
| 184 * Adds the given [info] state to the model. |
| 185 */ |
| 186 void addInfo(String info) { |
| 187 this.addInfoState(this, info); |
| 188 } |
| 189 |
| 190 /** |
| 191 * Removes the given [info] state from the model. |
| 192 */ |
| 193 void removeInfo(String info) { |
| 194 this.removeInfoState(this, info); |
| 195 } |
| 196 |
| 197 get viewValue => _viewValue; |
| 198 void set viewValue(value) { |
| 199 _viewValue = value; |
| 200 modelValue = value; |
| 201 } |
| 202 |
| 203 get modelValue => _modelValue; |
| 204 void set modelValue(value) { |
| 205 try { |
| 206 value = converter.parse(value); |
| 207 } catch(e) { |
| 208 value = null; |
| 209 } |
| 210 _modelValue = value; |
| 211 setter(value); |
| 212 |
| 213 if (modelValue == _originalValue) { |
| 214 markAsPristine(); |
| 215 } else { |
| 216 markAsDirty(); |
| 217 } |
| 218 } |
| 219 |
| 220 /** |
| 221 * Returns the list of validators that are registered on the model. |
| 222 */ |
| 223 List<NgValidator> get validators => _validators; |
| 224 |
| 225 /** |
| 226 * Executes a validation on the model against each of the validators present o
n the model. |
| 227 * Once complete, the model will either be set as valid or invalid. |
| 100 */ | 228 */ |
| 101 validate() { | 229 void validate() { |
| 230 _toBeValidated = false; |
| 102 if (validators.isNotEmpty) { | 231 if (validators.isNotEmpty) { |
| 103 validators.forEach((validator) { | 232 validators.forEach((validator) { |
| 104 setValidity(validator.name, validator.isValid(viewValue)); | 233 if (validator.isValid(modelValue)) { |
| 234 removeError(validator.name); |
| 235 } else { |
| 236 addError(validator.name); |
| 237 } |
| 105 }); | 238 }); |
| 239 } |
| 240 |
| 241 if (invalid) { |
| 242 addInfo(NgControl.NG_INVALID); |
| 106 } else { | 243 } else { |
| 107 valid = true; | 244 removeInfo(NgControl.NG_INVALID); |
| 108 } | 245 } |
| 109 } | |
| 110 | |
| 111 setValidity(String name, bool valid) { | |
| 112 this.updateControlValidity(this, name, valid); | |
| 113 } | 246 } |
| 114 | 247 |
| 115 /** | 248 /** |
| 116 * Registers a validator into the model to consider when running validate(). | 249 * Registers a validator into the model to consider when running validate(). |
| 117 */ | 250 */ |
| 118 addValidator(NgValidatable v) { | 251 void addValidator(NgValidator v) { |
| 119 validators.add(v); | 252 validators.add(v); |
| 120 validate(); | 253 validateLater(); |
| 121 } | 254 } |
| 122 | 255 |
| 123 /** | 256 /** |
| 124 * De-registers a validator from the model. | 257 * De-registers a validator from the model. |
| 125 */ | 258 */ |
| 126 removeValidator(NgValidatable v) { | 259 void removeValidator(NgValidator v) { |
| 127 validators.remove(v); | 260 validators.remove(v); |
| 128 validate(); | 261 validateLater(); |
| 129 } | 262 } |
| 130 } | 263 } |
| 131 | 264 |
| 132 /** | 265 /** |
| 133 * Usage: | 266 * Usage: |
| 134 * | 267 * |
| 135 * <input type="checkbox" ng-model="flag"> | 268 * <input type="checkbox" |
| 136 * | 269 * ng-model="expr" |
| 137 * This creates a two way databinding between the boolean expression specified | 270 * [ng-true-value="t_expr"] |
| 138 * in ng-model and the checkbox input element in the DOM. If the ng-model value | 271 * [ng-false-value="f_expr"] |
| 139 * is falsy (i.e. one of `false`, `null`, and `0`), then the checkbox is | 272 * > |
| 140 * unchecked. Otherwise, it is checked. Likewise, when the checkbox is checked, | 273 * |
| 141 * the model value is set to true. When unchecked, it is set to false. | 274 * This creates a two way databinding between the `ng-model` expression |
| 275 * and the checkbox input element state. |
| 276 * |
| 277 * If the optional `ng-true-value` is absent then: if the model expression |
| 278 * evaluates to true or to a nonzero [:num:], then the checkbox is checked; |
| 279 * otherwise, it is unchecked. |
| 280 * |
| 281 * If `ng-true-value="t_expr"` is present, then: if the model expression |
| 282 * evaluates to the same value as `t_expr` then the checkbox is checked; |
| 283 * otherwise, it is unchecked. |
| 284 * |
| 285 * When the checkbox is checked, the model is set to the value of `t_expr` if |
| 286 * present, true otherwise. When unchecked, it is set to the value of |
| 287 * `f_expr` if present, false otherwise. |
| 288 * |
| 289 * Also see [NgTrueValue] and [NgFalseValue]. |
| 142 */ | 290 */ |
| 143 @NgDirective(selector: 'input[type=checkbox][ng-model]') | 291 @Decorator(selector: 'input[type=checkbox][ng-model]') |
| 144 class InputCheckboxDirective { | 292 class InputCheckbox { |
| 145 final dom.InputElement inputElement; | 293 final dom.CheckboxInputElement inputElement; |
| 146 final NgModel ngModel; | 294 final NgModel ngModel; |
| 147 final NgTrueValue ngTrueValue; | 295 final NgTrueValue ngTrueValue; |
| 148 final NgFalseValue ngFalseValue; | 296 final NgFalseValue ngFalseValue; |
| 149 final Scope scope; | 297 final Scope scope; |
| 150 | 298 |
| 151 InputCheckboxDirective(dom.Element this.inputElement, this.ngModel, | 299 InputCheckbox(dom.Element this.inputElement, this.ngModel, |
| 152 this.scope, this.ngTrueValue, this.ngFalseValue) { | 300 this.scope, this.ngTrueValue, this.ngFalseValue) { |
| 153 ngModel.render = (value) { | 301 ngModel.render = (value) { |
| 154 inputElement.checked = ngTrueValue.isValue(inputElement, value); | 302 scope.rootScope.domWrite(() { |
| 303 inputElement.checked = ngTrueValue.isValue(value); |
| 304 }); |
| 155 }; | 305 }; |
| 156 inputElement.onChange.listen((value) { | 306 inputElement |
| 157 ngModel.dirty = true; | 307 ..onChange.listen((_) { |
| 158 ngModel.viewValue = inputElement.checked | 308 ngModel.viewValue = inputElement.checked |
| 159 ? ngTrueValue.readValue(inputElement) | 309 ? ngTrueValue.value : ngFalseValue.value; |
| 160 : ngFalseValue.readValue(inputElement); | 310 }) |
| 161 }); | 311 ..onBlur.listen((e) { |
| 312 ngModel.markAsTouched(); |
| 313 }); |
| 162 } | 314 } |
| 163 } | 315 } |
| 164 | 316 |
| 165 /** | 317 /** |
| 166 * Usage: | 318 * Usage: |
| 167 * | 319 * |
| 168 * <input type="text|url|password|email" ng-model="myModel"> | 320 * <input type="text|url|password|email" ng-model="myModel"> |
| 169 * <textarea ng-model="myModel"></textarea> | 321 * <textarea ng-model="myModel"></textarea> |
| 170 * | 322 * |
| 171 * This creates a two-way binding between any string-based input element | 323 * This creates a two-way binding between any string-based input element |
| 172 * (both <input> and <textarea>) so long as the ng-model attribute is | 324 * (both `<input>` and `<textarea>`) so long as the ng-model attribute is |
| 173 * present on the input element. Whenever the value of the input element | 325 * present on the input element. Whenever the value of the input element |
| 174 * changes then the matching model property on the scope will be updated | 326 * changes then the matching model property on the scope will be updated |
| 175 * as well as the other way around (when the scope property is updated). | 327 * as well as the other way around (when the scope property is updated). |
| 176 * | 328 * |
| 177 */ | 329 */ |
| 178 @NgDirective(selector: 'textarea[ng-model]') | 330 @Decorator(selector: 'textarea[ng-model]') |
| 179 @NgDirective(selector: 'input[type=text][ng-model]') | 331 @Decorator(selector: 'input[type=text][ng-model]') |
| 180 @NgDirective(selector: 'input[type=password][ng-model]') | 332 @Decorator(selector: 'input[type=password][ng-model]') |
| 181 @NgDirective(selector: 'input[type=url][ng-model]') | 333 @Decorator(selector: 'input[type=url][ng-model]') |
| 182 @NgDirective(selector: 'input[type=email][ng-model]') | 334 @Decorator(selector: 'input[type=email][ng-model]') |
| 183 @NgDirective(selector: 'input[type=search][ng-model]') | 335 @Decorator(selector: 'input[type=search][ng-model]') |
| 184 class InputTextLikeDirective { | 336 class InputTextLike { |
| 185 final dom.Element inputElement; | 337 final dom.Element inputElement; |
| 186 final NgModel ngModel; | 338 final NgModel ngModel; |
| 187 final Scope scope; | 339 final Scope scope; |
| 188 String _inputType; | 340 String _inputType; |
| 189 | 341 |
| 190 get typedValue => (inputElement as dynamic).value; | 342 get typedValue => (inputElement as dynamic).value; |
| 191 set typedValue(value) => (inputElement as dynamic).value = (value == null) ? | 343 void set typedValue(value) { |
| 192 '' : | 344 (inputElement as dynamic).value = (value == null) ? '' : value.toString(); |
| 193 value.toString(); | 345 } |
| 194 | 346 |
| 195 InputTextLikeDirective(this.inputElement, this.ngModel, this.scope) { | 347 InputTextLike(this.inputElement, this.ngModel, this.scope) { |
| 196 ngModel.render = (value) { | 348 ngModel.render = (value) { |
| 197 if (value == null) value = ''; | 349 scope.rootScope.domWrite(() { |
| 350 if (value == null) value = ''; |
| 198 | 351 |
| 199 var currentValue = typedValue; | 352 var currentValue = typedValue; |
| 200 if (value != currentValue && !(value is num && currentValue is num && | 353 if (value != currentValue && !(value is num && currentValue is num && |
| 201 value.isNaN && currentValue.isNaN)) { | 354 value.isNaN && currentValue.isNaN)) { |
| 202 typedValue = value; | 355 typedValue = value; |
| 203 } | 356 } |
| 357 }); |
| 204 }; | 358 }; |
| 205 inputElement | 359 inputElement |
| 206 ..onChange.listen(processValue) | 360 ..onChange.listen(processValue) |
| 207 ..onInput.listen(processValue) | 361 ..onInput.listen(processValue) |
| 208 ..onBlur.listen((e) { | 362 ..onBlur.listen((e) { |
| 209 if (ngModel.touched == null || ngModel.touched == false) { | 363 ngModel.markAsTouched(); |
| 210 ngModel.touched = true; | |
| 211 } | |
| 212 }); | 364 }); |
| 213 } | 365 } |
| 214 | 366 |
| 215 processValue([_]) { | 367 void processValue([_]) { |
| 216 var value = typedValue; | 368 var value = typedValue; |
| 217 if (value != ngModel.viewValue) { | 369 if (value != ngModel.viewValue) ngModel.viewValue = value; |
| 218 ngModel.dirty = true; | |
| 219 ngModel.viewValue = value; | |
| 220 } | |
| 221 ngModel.validate(); | 370 ngModel.validate(); |
| 222 } | 371 } |
| 223 } | 372 } |
| 224 | 373 |
| 225 /** | 374 /** |
| 226 * Usage: | 375 * Usage: |
| 227 * | 376 * |
| 228 * <input type="number|range" ng-model="myModel"> | 377 * <input type="number|range" ng-model="myModel"> |
| 229 * | 378 * |
| 230 * Model: | 379 * Model: |
| 231 * | 380 * |
| 232 * num myModel; | 381 * num myModel; |
| 233 * | 382 * |
| 234 * This creates a two-way binding between the input and the named model property | 383 * This creates a two-way binding between the input and the named model property |
| 235 * (e.g., myModel in the example above). When processing the input, its value is | 384 * (e.g., myModel in the example above). When processing the input, its value is |
| 236 * read as a [num], via the [dom.InputElement.valueAsNumber] field. If the input | 385 * read as a [:num:], via the [dom.InputElement.valueAsNumber] field. If the |
| 237 * text does not represent a number, then the model is appropriately set to | 386 * input text does not represent a number, then the model is appropriately set |
| 238 * [double.NAN]. Setting the model property to [null] will clear the input. | 387 * to [double.NAN]. Setting the model property to [null] will clear the input. |
| 239 * Setting the model to [double.NAN] will have no effect (input will be left | 388 * Setting the model to [double.NAN] will have no effect (input will be left |
| 240 * unchanged). | 389 * unchanged). |
| 241 */ | 390 */ |
| 242 @NgDirective(selector: 'input[type=number][ng-model]') | 391 @Decorator(selector: 'input[type=number][ng-model]') |
| 243 @NgDirective(selector: 'input[type=range][ng-model]') | 392 @Decorator(selector: 'input[type=range][ng-model]') |
| 244 class InputNumberLikeDirective { | 393 class InputNumberLike { |
| 245 final dom.InputElement inputElement; | 394 final dom.InputElement inputElement; |
| 246 final NgModel ngModel; | 395 final NgModel ngModel; |
| 247 final Scope scope; | 396 final Scope scope; |
| 248 | 397 |
| 249 num get typedValue => inputElement.valueAsNumber; | 398 |
| 399 // We can't use [inputElement.valueAsNumber] due to http://dartbug.com/15788 |
| 400 num get typedValue => num.parse(inputElement.value, (v) => double.NAN); |
| 401 |
| 250 void set typedValue(num value) { | 402 void set typedValue(num value) { |
| 251 // [chalin, 2014-02-16] This post | 403 // [chalin, 2014-02-16] This post |
| 252 // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.h
tml | 404 // http://lists.whatwg.org/pipermail/whatwg-whatwg.org/2010-January/024829.h
tml |
| 253 // suggests that setting `valueAsNumber` to null should clear the field, but | 405 // suggests that setting `valueAsNumber` to null should clear the field, but |
| 254 // it does not. [TODO: put BUG/ISSUE number here]. We implement a | 406 // it does not. [TODO: put BUG/ISSUE number here]. We implement a |
| 255 // workaround by setting `value`. Clean-up once the bug is fixed. | 407 // workaround by setting `value`. Clean-up once the bug is fixed. |
| 256 if (value == null) { | 408 if (value == null) { |
| 257 inputElement.value = null; | 409 inputElement.value = null; |
| 258 } else { | 410 } else { |
| 259 inputElement.valueAsNumber = value; | 411 // We can't use inputElement.valueAsNumber due to http://dartbug.com/15788 |
| 260 } | 412 inputElement.value = "$value"; |
| 261 } | 413 } |
| 262 | 414 } |
| 263 InputNumberLikeDirective(dom.Element this.inputElement, this.ngModel, this.sco
pe) { | 415 |
| 416 InputNumberLike(dom.Element this.inputElement, this.ngModel, this.scope) { |
| 264 ngModel.render = (value) { | 417 ngModel.render = (value) { |
| 265 if (value != typedValue | 418 scope.rootScope.domWrite(() { |
| 266 && (value == null || value is num && !value.isNaN)) { | 419 if (value != typedValue |
| 267 typedValue = value; | 420 && (value == null || value is num && !value.isNaN)) { |
| 268 } | 421 typedValue = value; |
| 422 } |
| 423 }); |
| 269 }; | 424 }; |
| 270 inputElement | 425 inputElement |
| 271 ..onChange.listen(relaxFnArgs(processValue)) | 426 ..onChange.listen(relaxFnArgs(processValue)) |
| 272 ..onInput.listen(relaxFnArgs(processValue)); | 427 ..onInput.listen(relaxFnArgs(processValue)) |
| 273 } | 428 ..onBlur.listen((e) { |
| 274 | 429 ngModel.markAsTouched(); |
| 275 processValue() { | 430 }); |
| 431 } |
| 432 |
| 433 void processValue() { |
| 276 num value = typedValue; | 434 num value = typedValue; |
| 277 if (value != ngModel.viewValue) { | 435 if (value != ngModel.viewValue) { |
| 278 ngModel.dirty = true; | |
| 279 scope.eval(() => ngModel.viewValue = value); | 436 scope.eval(() => ngModel.viewValue = value); |
| 280 } | 437 } |
| 281 ngModel.validate(); | 438 ngModel.validate(); |
| 439 } |
| 440 } |
| 441 |
| 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 @Decorator(selector: 'input[type=date][ng-model][ng-bind-type]') |
| 456 @Decorator(selector: 'input[type=time][ng-model][ng-bind-type]') |
| 457 @Decorator(selector: 'input[type=datetime][ng-model][ng-bind-type]') |
| 458 @Decorator(selector: 'input[type=datetime-local][ng-model][ng-bind-type]') |
| 459 @Decorator(selector: 'input[type=month][ng-model][ng-bind-type]') |
| 460 @Decorator(selector: 'input[type=week][ng-model][ng-bind-type]') |
| 461 class NgBindTypeForDateLike { |
| 462 static const DATE = 'date'; |
| 463 static const NUMBER = 'number'; |
| 464 static const STRING = 'string'; |
| 465 static const DEFAULT = DATE; |
| 466 static const VALID_VALUES = const <String>[DATE, NUMBER, STRING]; |
| 467 |
| 468 final dom.InputElement inputElement; |
| 469 String _idlAttrKind = DEFAULT; |
| 470 |
| 471 NgBindTypeForDateLike(dom.Element this.inputElement); |
| 472 |
| 473 @NgAttr('ng-bind-type') |
| 474 void set idlAttrKind(final String _kind) { |
| 475 String kind = _kind == null ? DEFAULT : _kind.toLowerCase(); |
| 476 if (!VALID_VALUES.contains(kind)) |
| 477 throw "Unsupported ng-bind-type attribute value '$_kind'; " |
| 478 "it should be one of $VALID_VALUES"; |
| 479 _idlAttrKind = kind; |
| 480 } |
| 481 |
| 482 String get idlAttrKind => _idlAttrKind; |
| 483 |
| 484 dynamic get inputTypedValue { |
| 485 switch (idlAttrKind) { |
| 486 case DATE: return inputValueAsDate; |
| 487 case NUMBER: return inputElement.valueAsNumber; |
| 488 default: return inputElement.value; |
| 489 } |
| 490 } |
| 491 |
| 492 void set inputTypedValue(dynamic inputValue) { |
| 493 if (inputValue is DateTime) { |
| 494 inputValueAsDate = inputValue; |
| 495 } else if (inputValue is num) { |
| 496 inputElement.valueAsNumber = inputValue; |
| 497 } else { |
| 498 inputElement.value = inputValue; |
| 499 } |
| 500 } |
| 501 |
| 502 /// Input's `valueAsDate` normalized to UTC (per HTML5 std). |
| 503 DateTime get inputValueAsDate { |
| 504 DateTime dt; |
| 505 // Wrap in try-catch due to |
| 506 // https://code.google.com/p/dart/issues/detail?id=17625 |
| 507 try { |
| 508 dt = inputElement.valueAsDate; |
| 509 } catch (e) { |
| 510 dt = null; |
| 511 } |
| 512 return (dt != null && !dt.isUtc) ? dt.toUtc() : dt; |
| 513 } |
| 514 |
| 515 /// Set input's `valueAsDate`. Argument is normalized to UTC if necessary |
| 516 /// (per HTML standard). |
| 517 void set inputValueAsDate(DateTime dt) { |
| 518 inputElement.valueAsDate = (dt != null && !dt.isUtc) ? dt.toUtc() : dt; |
| 519 } |
| 520 } |
| 521 |
| 522 /** |
| 523 * **Background: Standards and Browsers** |
| 524 * |
| 525 * According to the |
| 526 * [HTML5 Standard](http://www.w3.org/TR/html5/forms.html#the-input-element), |
| 527 * the [dom.InputElement.valueAsDate] and [dom.InputElement.valueAsNumber] IDL |
| 528 * attributes should be available for all date/time related input types, |
| 529 * except for `datetime-local` which is limited to |
| 530 * [dom.InputElement.valueNumber]. Of course, all input types support |
| 531 * [dom.InputElement.value] which yields a [String]; |
| 532 * [dom.InputElement.valueAsDate] yields a [DateTime] and |
| 533 * [dom.InputElement.valueNumber] yields a [num]. |
| 534 * |
| 535 * But not all browsers currently support date/time related inputs and of |
| 536 * those that do, some deviate from the standard. Hence, this directive |
| 537 * allows developers to control the IDL attribute that will be used |
| 538 * to read the value of a date/time input. This is achieved via the subordinate |
| 539 * 'ng-bind-type' directive; see [NgBindTypeForDateLike] for details. |
| 540 * |
| 541 * **Usage**: |
| 542 * |
| 543 * <input type="date|datetime|datetime-local|month|time|week" |
| 544 * [ng-bind-type="date"] |
| 545 * ng-model="myModel"> |
| 546 * |
| 547 * **Model**: |
| 548 * |
| 549 * dynamic myModel; // one of DateTime | num | String |
| 550 * |
| 551 * This directive creates a two-way binding between the input and a model |
| 552 * property. The subordinate 'ng-bind-type' directive determines which input |
| 553 * IDL attribute is read (see [NgBindTypeForDateLike] for details) and |
| 554 * hence the type of the read values. The type of the model property value |
| 555 * determines which IDL attribute is written to: [DateTime] and [num] values |
| 556 * are assigned to [dom.InputElement.valueAsDate] and |
| 557 * [dom.InputElement.valueNumber], respectively; [String] and `null` values |
| 558 * are assigned to [dom.InputElement.value]. Setting the model to `null` will |
| 559 * clear the input if it is currently valid, otherwise, invalid input is left |
| 560 * untouched (so that the user has an opportunity to correct it). To clear the |
| 561 * input unconditionally, set the model property to the empty string (''). |
| 562 * |
| 563 * **Notes**: |
| 564 * - As prescribed by the HTML5 standard, [DateTime] values returned by the |
| 565 * `valueAsDate` IDL attribute are meant to be in UTC. |
| 566 * - As of the HTML5 Editor's Draft 29 March 2014, datetime-local is no longer |
| 567 * part of the standard. Other date related input are also at risk of being |
| 568 * dropped. |
| 569 */ |
| 570 |
| 571 @Decorator(selector: 'input[type=date][ng-model]', |
| 572 module: InputDateLike.moduleFactory) |
| 573 @Decorator(selector: 'input[type=time][ng-model]', |
| 574 module: InputDateLike.moduleFactory) |
| 575 @Decorator(selector: 'input[type=datetime][ng-model]', |
| 576 module: InputDateLike.moduleFactory) |
| 577 @Decorator(selector: 'input[type=datetime-local][ng-model]', |
| 578 module: InputDateLike.moduleFactory) |
| 579 @Decorator(selector: 'input[type=month][ng-model]', |
| 580 module: InputDateLike.moduleFactory) |
| 581 @Decorator(selector: 'input[type=week][ng-model]', |
| 582 module: InputDateLike.moduleFactory) |
| 583 class InputDateLike { |
| 584 static Module moduleFactory() => new Module()..factory(NgBindTypeForDateLike, |
| 585 (Injector i) => new NgBindTypeForDateLike(i.get(dom.Element))); |
| 586 final dom.InputElement inputElement; |
| 587 final NgModel ngModel; |
| 588 final Scope scope; |
| 589 NgBindTypeForDateLike ngBindType; |
| 590 |
| 591 InputDateLike(dom.Element this.inputElement, this.ngModel, this.scope, |
| 592 this.ngBindType) { |
| 593 if (inputElement.type == 'datetime-local') { |
| 594 ngBindType.idlAttrKind = NgBindTypeForDateLike.NUMBER; |
| 595 } |
| 596 ngModel.render = (value) { |
| 597 scope.rootScope.domWrite(() { |
| 598 if (!eqOrNaN(value, typedValue)) typedValue = value; |
| 599 }); |
| 600 }; |
| 601 inputElement |
| 602 ..onChange.listen(relaxFnArgs(processValue)) |
| 603 ..onInput.listen(relaxFnArgs(processValue)) |
| 604 ..onBlur.listen((e) { |
| 605 ngModel.markAsTouched(); |
| 606 }); |
| 607 } |
| 608 |
| 609 dynamic get typedValue => ngBindType.inputTypedValue; |
| 610 |
| 611 void set typedValue(dynamic value) { |
| 612 ngBindType.inputTypedValue = value; |
| 613 } |
| 614 |
| 615 void processValue() { |
| 616 var value = typedValue; |
| 617 // print("processValue: value=$value, model=${ngModel.viewValue}"); |
| 618 if (!eqOrNaN(value, ngModel.viewValue)) { |
| 619 scope.eval(() => ngModel.viewValue = value); |
| 620 } |
| 621 ngModel.validate(); |
| 282 } | 622 } |
| 283 } | 623 } |
| 284 | 624 |
| 285 class _UidCounter { | 625 class _UidCounter { |
| 286 static final int CHAR_0 = "0".codeUnitAt(0); | 626 static final int CHAR_0 = "0".codeUnitAt(0); |
| 287 static final int CHAR_9 = "9".codeUnitAt(0); | 627 static final int CHAR_9 = "9".codeUnitAt(0); |
| 288 static final int CHAR_A = "A".codeUnitAt(0); | 628 static final int CHAR_A = "A".codeUnitAt(0); |
| 289 static final int CHAR_Z = "Z".codeUnitAt(0); | 629 static final int CHAR_Z = "Z".codeUnitAt(0); |
| 290 List charCodes = [CHAR_0, CHAR_0, CHAR_0]; | 630 final charCodes = [CHAR_0, CHAR_0, CHAR_0]; |
| 291 | 631 |
| 292 String next() { | 632 String next() { |
| 293 for (int i = charCodes.length - 1; i >= 0; i--) { | 633 for (int i = charCodes.length - 1; i >= 0; i--) { |
| 294 int code = charCodes[i]; | 634 int code = charCodes[i]; |
| 295 if (code == CHAR_9) { | 635 if (code == CHAR_9) { |
| 296 charCodes[i] = CHAR_A; | 636 charCodes[i] = CHAR_A; |
| 297 return new String.fromCharCodes(charCodes); | 637 return new String.fromCharCodes(charCodes); |
| 298 } else if (code == CHAR_Z) { | 638 } else if (code == CHAR_Z) { |
| 299 charCodes[i] = CHAR_0; | 639 charCodes[i] = CHAR_0; |
| 300 } else { | 640 } else { |
| 301 charCodes[i] = code + 1; | 641 charCodes[i] = code + 1; |
| 302 return new String.fromCharCodes(charCodes); | 642 return new String.fromCharCodes(charCodes); |
| 303 } | 643 } |
| 304 } | 644 } |
| 305 charCodes.insert(0, CHAR_0); | 645 charCodes.insert(0, CHAR_0); |
| 306 return new String.fromCharCodes(charCodes); | 646 return new String.fromCharCodes(charCodes); |
| 307 } | 647 } |
| 308 } | 648 } |
| 309 | 649 |
| 310 final _uidCounter = new _UidCounter(); | 650 final _uidCounter = new _UidCounter(); |
| 311 | 651 |
| 312 /** | 652 /** |
| 313 * Use `ng-value` directive with `<input type="radio">` or `<option>` to | 653 * Usage: |
| 314 * allow binding to values other then strings. This is needed since the | 654 * |
| 315 * `value` attribute on DOM element `<input type="radio" value="foo">` can | 655 * <input type=radio ng-model=model [ng-value=expr]> |
| 316 * only be a string. With `ng-value` one can bind to any object. | 656 * |
| 657 * <option [ng-value=expr]>...</option> |
| 658 * |
| 659 * Example: |
| 660 * |
| 661 * <select ng-model="robot"> |
| 662 * <option ng-repeat="r in robots" ng-value="r">{{r.name}}</option> |
| 663 * </select> |
| 664 * |
| 665 * When present, the value of this `ng-value` one-way attribute is assigned to |
| 666 * the `ng-model` property when the corresponding radio element or option is |
| 667 * selected. Note that `expr` can be not any type; i.e., it is not restricted |
| 668 * to [String]. |
| 317 */ | 669 */ |
| 318 @NgDirective(selector: '[ng-value]') | 670 @Decorator(selector: 'input[type=radio][ng-model][ng-value]') |
| 671 @Decorator(selector: 'option[ng-value]') |
| 319 class NgValue { | 672 class NgValue { |
| 673 static Module _module = new Module()..type(NgValue); |
| 674 static Module moduleFactory() => _module; |
| 675 |
| 320 final dom.Element element; | 676 final dom.Element element; |
| 321 @NgOneWay('ng-value') | 677 var _value; |
| 322 var value; | |
| 323 | 678 |
| 324 NgValue(this.element); | 679 NgValue(this.element); |
| 325 | 680 |
| 326 readValue(dom.Element element) { | 681 @NgOneWay('ng-value') |
| 327 assert(this.element == null || element == this.element); | 682 void set value(val) { this._value = val; } |
| 328 return this.element == null ? (element as dynamic).value : value; | 683 dynamic get value => _value == null ? (element as dynamic).value : _value; |
| 329 } | |
| 330 } | 684 } |
| 331 | 685 |
| 332 /** | 686 /** |
| 333 * `ng-true-value` allows you to select any expression to be set to | 687 * Usage: |
| 334 * `ng-model` when checkbox is selected on `<input type="checkbox">`. | 688 * |
| 689 * <input type=checkbox |
| 690 * ng-model=model |
| 691 * [ng-true-value=expr]> |
| 692 * |
| 693 * The initial value of the expression bound to this directive is assigned to |
| 694 * the model when the input is checked. Note that the expression can be of any |
| 695 * type, not just [String]. Also see [InputCheckboxDirective], [NgFalseValue]. |
| 335 */ | 696 */ |
| 336 @NgDirective(selector: '[ng-true-value]') | 697 @Decorator(selector: 'input[type=checkbox][ng-model][ng-true-value]') |
| 337 class NgTrueValue { | 698 class NgTrueValue { |
| 338 final dom.Element element; | 699 final dom.Element element; |
| 339 @NgOneWay('ng-true-value') | 700 @NgOneWay('ng-true-value') |
| 340 var value; | 701 var value = true; |
| 341 | 702 |
| 342 NgTrueValue(this.element); | 703 NgTrueValue([this.element]); |
| 343 | 704 |
| 344 readValue(dom.Element element) { | 705 bool isValue(val) => element == null ? toBool(val) : val == value; |
| 345 assert(this.element == null || element == this.element); | |
| 346 return this.element == null ? true : value; | |
| 347 } | |
| 348 | |
| 349 isValue(dom.Element element, value) { | |
| 350 assert(this.element == null || element == this.element); | |
| 351 return this.element == null ? toBool(value) : value == this.value; | |
| 352 } | |
| 353 } | 706 } |
| 354 | 707 |
| 355 /** | 708 /** |
| 356 * `ng-false-value` allows you to select any expression to be set to | 709 * Usage: |
| 357 * `ng-model` when checkbox is deselected<input type="checkbox">`. | 710 * |
| 711 * <input type=checkbox |
| 712 * ng-model=model |
| 713 * [ng-false-value=expr]> |
| 714 * |
| 715 * The initial value of the expression bound to this directive is assigned to |
| 716 * the model when the input is unchecked. Note that the expression can be of any |
| 717 * type, not just [String]. Also see [InputCheckboxDirective], [NgTrueValue]. |
| 358 */ | 718 */ |
| 359 @NgDirective(selector: '[ng-false-value]') | 719 @Decorator(selector: 'input[type=checkbox][ng-model][ng-false-value]') |
| 360 class NgFalseValue { | 720 class NgFalseValue { |
| 361 final dom.Element element; | 721 final dom.Element element; |
| 362 @NgOneWay('ng-false-value') | 722 @NgOneWay('ng-false-value') |
| 363 var value; | 723 var value = false; |
| 364 | 724 |
| 365 NgFalseValue(this.element); | 725 NgFalseValue([this.element]); |
| 366 | |
| 367 readValue(dom.Element element) { | |
| 368 assert(this.element == null || element == this.element); | |
| 369 return this.element == null ? false : value; | |
| 370 } | |
| 371 } | 726 } |
| 372 | 727 |
| 373 /** | 728 /** |
| 374 * Usage: | 729 * Usage: |
| 375 * | 730 * |
| 376 * <input type="radio" ng-model="category"> | 731 * <input type="radio" ng-model="category"> |
| 377 * | 732 * |
| 378 * This creates a two way databinding between the expression specified in | 733 * This creates a two way databinding between the expression specified in |
| 379 * ng-model and the range input elements in the DOM. If the ng-model value is | 734 * ng-model and the range input elements in the DOM. If the ng-model value is |
| 380 * set to a value not corresponding to one of the radio elements, then none of | 735 * set to a value not corresponding to one of the radio elements, then none of |
| 381 * the radio elements will be check. Otherwise, only the corresponding input | 736 * the radio elements will be check. Otherwise, only the corresponding input |
| 382 * element in the group is checked. Likewise, when a radio button element is | 737 * element in the group is checked. Likewise, when a radio button element is |
| 383 * checked, the model is updated with its value. Radio buttons that have a | 738 * checked, the model is updated with its value. Radio buttons that have a |
| 384 * `name` attribute are left alone. Those that are missing the attribute will | 739 * `name` attribute are left alone. Those that are missing the attribute will |
| 385 * have a unique `name` assigned to them. This sequence goes `001`, `001`, ... | 740 * have a unique `name` assigned to them. This sequence goes `001`, `001`, ... |
| 386 * `009`, `00A`, `00Z`, `010`, … and so on using more than 3 characters for the | 741 * `009`, `00A`, `00Z`, `010`, and so on using more than 3 characters for the |
| 387 * name when the counter overflows. | 742 * name when the counter overflows. |
| 388 */ | 743 */ |
| 389 @NgDirective(selector: 'input[type=radio][ng-model]') | 744 @Decorator( |
| 390 class InputRadioDirective { | 745 selector: 'input[type=radio][ng-model]', |
| 746 module: NgValue.moduleFactory) |
| 747 class InputRadio { |
| 391 final dom.RadioButtonInputElement radioButtonElement; | 748 final dom.RadioButtonInputElement radioButtonElement; |
| 392 final NgModel ngModel; | 749 final NgModel ngModel; |
| 393 final NgValue ngValue; | 750 final NgValue ngValue; |
| 394 final Scope scope; | 751 final Scope scope; |
| 395 | 752 |
| 396 InputRadioDirective(dom.Element this.radioButtonElement, this.ngModel, | 753 InputRadio(dom.Element this.radioButtonElement, this.ngModel, |
| 397 this.scope, this.ngValue, NodeAttrs attrs) { | 754 this.scope, this.ngValue, NodeAttrs attrs) { |
| 398 // If there's no "name" set, we'll set a unique name. This ensures | 755 // If there's no "name" set, we'll set a unique name. This ensures |
| 399 // less surprising behavior about which radio buttons are grouped together. | 756 // less surprising behavior about which radio buttons are grouped together. |
| 400 if (attrs['name'] == '' || attrs['name'] == null) { | 757 if (attrs['name'] == '' || attrs['name'] == null) { |
| 401 attrs["name"] = _uidCounter.next(); | 758 attrs["name"] = _uidCounter.next(); |
| 402 } | 759 } |
| 403 ngModel.render = (value) { | 760 ngModel.render = (value) { |
| 404 radioButtonElement.checked = (value == ngValue.readValue(radioButtonElemen
t)); | 761 scope.rootScope.domWrite(() { |
| 762 radioButtonElement.checked = (value == ngValue.value); |
| 763 }); |
| 405 }; | 764 }; |
| 406 radioButtonElement.onClick.listen((_) { | 765 radioButtonElement |
| 407 if (radioButtonElement.checked) { | 766 ..onClick.listen((_) { |
| 408 ngModel.dirty = true; | 767 if (radioButtonElement.checked) ngModel.viewValue = ngValue.value; |
| 409 ngModel.viewValue = ngValue.readValue(radioButtonElement); | 768 }) |
| 410 } | 769 ..onBlur.listen((e) { |
| 411 }); | 770 ngModel.markAsTouched(); |
| 771 }); |
| 412 } | 772 } |
| 413 } | 773 } |
| 414 | 774 |
| 415 /** | 775 /** |
| 416 * Usage (span could be replaced with any element which supports text content, s
uch as `p`): | 776 * Usage (span could be replaced with any element which supports text content, s
uch as `p`): |
| 417 * | 777 * |
| 418 * <span contenteditable= ng-model="name"> | 778 * <span contenteditable= ng-model="name"> |
| 419 * | 779 * |
| 420 * This creates a two way databinding between the expression specified in | 780 * This creates a two way databinding between the expression specified in |
| 421 * ng-model and the html element in the DOM. If the ng-model value is | 781 * ng-model and the html element in the DOM. If the ng-model value is |
| 422 * `null`, it is treated as equivalent to the empty string for rendering | 782 * `null`, it is treated as equivalent to the empty string for rendering |
| 423 * purposes. | 783 * purposes. |
| 424 */ | 784 */ |
| 425 @NgDirective(selector: '[contenteditable][ng-model]') | 785 @Decorator(selector: '[contenteditable][ng-model]') |
| 426 class ContentEditableDirective extends InputTextLikeDirective { | 786 class ContentEditable extends InputTextLike { |
| 427 ContentEditableDirective(dom.Element inputElement, NgModel ngModel, | 787 ContentEditable(dom.Element inputElement, NgModel ngModel, Scope scope) |
| 428 Scope scope) | |
| 429 : super(inputElement, ngModel, scope); | 788 : super(inputElement, ngModel, scope); |
| 430 | 789 |
| 431 // The implementation is identical to InputTextLikeDirective but use innerHtml
instead of value | 790 // The implementation is identical to InputTextLike but use innerHtml instead
of value |
| 432 get typedValue => (inputElement as dynamic).innerHtml; | 791 String get typedValue => (inputElement as dynamic).innerHtml; |
| 433 set typedValue(String value) => | 792 void set typedValue(String value) { |
| 434 (inputElement as dynamic).innerHtml = (value == null) ? '' : value; | 793 (inputElement as dynamic).innerHtml = (value == null) ? '' : value; |
| 794 } |
| 435 } | 795 } |
| OLD | NEW |